mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-07-09 05:57:56 +02:00
Add files via upload
This commit is contained in:
@@ -9,6 +9,9 @@ package openai
|
||||
// Stream: Claude SSE (event: content_block_delta / message_delta) → OpenAI SSE 格式
|
||||
// Auth: Bearer → x-api-key
|
||||
// Tools: OpenAI tools[] → Claude tools[] (input_schema)
|
||||
//
|
||||
// Extended thinking: 顶层 `thinking` 从 OpenAI 请求体透传;响应中 `thinking` block 映射为
|
||||
// `reasoning_content`(可读前缀 + 内部 JSON 尾缀以保留 signature,供多轮工具续跑;UI 用 openai.DisplayReasoningContent 剥离)。
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -38,6 +41,7 @@ type claudeRequest struct {
|
||||
Messages []claudeMessage `json:"messages"`
|
||||
Tools []claudeTool `json:"tools,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
Thinking json.RawMessage `json:"thinking,omitempty"`
|
||||
}
|
||||
|
||||
type claudeMessage struct {
|
||||
@@ -76,6 +80,10 @@ type claudeContentBlock struct {
|
||||
// text block
|
||||
Text string `json:"text,omitempty"`
|
||||
|
||||
// thinking block (extended thinking)
|
||||
Thinking string `json:"thinking,omitempty"`
|
||||
Signature string `json:"signature,omitempty"`
|
||||
|
||||
// tool_use block (assistant 返回)
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
@@ -176,7 +184,13 @@ func convertOpenAIToClaude(payload interface{}) (*claudeRequest, error) {
|
||||
|
||||
// tool_calls (assistant 消息中包含工具调用)
|
||||
if role == "assistant" {
|
||||
rc, _ := mm["reasoning_content"].(string)
|
||||
_, thinkingReplay := parseClaudeReasoningAssistantBlocks(rc)
|
||||
|
||||
var blocks []claudeContentBlock
|
||||
for _, tb := range thinkingReplay {
|
||||
blocks = append(blocks, tb)
|
||||
}
|
||||
if content != "" {
|
||||
blocks = append(blocks, claudeContentBlock{Type: "text", Text: content})
|
||||
}
|
||||
@@ -290,6 +304,13 @@ func convertOpenAIToClaude(payload interface{}) (*claudeRequest, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Extended thinking (Anthropic top-level); merged from Eino ExtraFields / admin extras.
|
||||
if th, ok := oai["thinking"]; ok && th != nil {
|
||||
if raw, err := json.Marshal(th); err == nil && len(raw) > 0 && string(raw) != "null" {
|
||||
req.Thinking = json.RawMessage(raw)
|
||||
}
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
@@ -318,9 +339,12 @@ func claudeToOpenAIResponseJSON(claudeBody []byte) ([]byte, error) {
|
||||
|
||||
var textContent string
|
||||
var toolCalls []interface{}
|
||||
var thinkingBlocks []claudeContentBlock
|
||||
|
||||
for _, block := range cr.Content {
|
||||
switch block.Type {
|
||||
case "thinking":
|
||||
thinkingBlocks = append(thinkingBlocks, block)
|
||||
case "text":
|
||||
textContent += block.Text
|
||||
case "tool_use":
|
||||
@@ -344,6 +368,18 @@ func claudeToOpenAIResponseJSON(claudeBody []byte) ([]byte, error) {
|
||||
if len(toolCalls) > 0 {
|
||||
message["tool_calls"] = toolCalls
|
||||
}
|
||||
if len(thinkingBlocks) > 0 {
|
||||
var parts []string
|
||||
for _, tb := range thinkingBlocks {
|
||||
if strings.TrimSpace(tb.Thinking) != "" {
|
||||
parts = append(parts, tb.Thinking)
|
||||
}
|
||||
}
|
||||
rc := appendClaudeReasoningRoundTrip(strings.Join(parts, "\n\n"), thinkingBlocks)
|
||||
if rc != "" {
|
||||
message["reasoning_content"] = rc
|
||||
}
|
||||
}
|
||||
|
||||
choice := map[string]interface{}{
|
||||
"index": 0,
|
||||
@@ -901,8 +937,16 @@ func (rt *claudeRoundTripper) RoundTrip(req *http.Request) (*http.Response, erro
|
||||
|
||||
reader := bufio.NewReader(resp.Body)
|
||||
blockToToolIndex := make(map[int]int)
|
||||
blockIndexToType := make(map[int]string)
|
||||
nextToolIndex := 0
|
||||
|
||||
type thinkingAcc struct {
|
||||
text strings.Builder
|
||||
sig strings.Builder
|
||||
}
|
||||
thinkingByIndex := make(map[int]*thinkingAcc)
|
||||
var finishedThinking []claudeContentBlock
|
||||
|
||||
for {
|
||||
line, readErr := reader.ReadString('\n')
|
||||
if readErr != nil {
|
||||
@@ -947,6 +991,11 @@ func (rt *claudeRoundTripper) RoundTrip(req *http.Request) (*http.Response, erro
|
||||
blockIdx := int(blockIdxFlt)
|
||||
cb, _ := event["content_block"].(map[string]interface{})
|
||||
bt, _ := cb["type"].(string)
|
||||
blockIndexToType[blockIdx] = bt
|
||||
|
||||
if bt == "thinking" {
|
||||
thinkingByIndex[blockIdx] = &thinkingAcc{}
|
||||
}
|
||||
|
||||
if bt == "tool_use" {
|
||||
id, _ := cb["id"].(string)
|
||||
@@ -986,7 +1035,35 @@ func (rt *claudeRoundTripper) RoundTrip(req *http.Request) (*http.Response, erro
|
||||
delta, _ := event["delta"].(map[string]interface{})
|
||||
dt, _ := delta["type"].(string)
|
||||
|
||||
if dt == "text_delta" {
|
||||
if dt == "thinking_delta" {
|
||||
tPart, _ := delta["thinking"].(string)
|
||||
if tPart != "" {
|
||||
if acc := thinkingByIndex[blockIdx]; acc != nil {
|
||||
acc.text.WriteString(tPart)
|
||||
}
|
||||
oaiChunk := map[string]interface{}{
|
||||
"choices": []map[string]interface{}{
|
||||
{
|
||||
"delta": map[string]interface{}{
|
||||
"reasoning_content": tPart,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
b, _ := json.Marshal(oaiChunk)
|
||||
if !writeLine("data: " + string(b) + "\n\n") {
|
||||
pw.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
} else if dt == "signature_delta" {
|
||||
sigPart, _ := delta["signature"].(string)
|
||||
if sigPart != "" {
|
||||
if acc := thinkingByIndex[blockIdx]; acc != nil {
|
||||
acc.sig.WriteString(sigPart)
|
||||
}
|
||||
}
|
||||
} else if dt == "text_delta" {
|
||||
text, _ := delta["text"].(string)
|
||||
oaiChunk := map[string]interface{}{
|
||||
"choices": []map[string]interface{}{
|
||||
@@ -1031,6 +1108,21 @@ func (rt *claudeRoundTripper) RoundTrip(req *http.Request) (*http.Response, erro
|
||||
}
|
||||
}
|
||||
|
||||
case "content_block_stop":
|
||||
blockIdxFlt, _ := event["index"].(float64)
|
||||
blockIdx := int(blockIdxFlt)
|
||||
bt := blockIndexToType[blockIdx]
|
||||
if bt == "thinking" {
|
||||
if acc := thinkingByIndex[blockIdx]; acc != nil {
|
||||
finishedThinking = append(finishedThinking, claudeContentBlock{
|
||||
Type: "thinking",
|
||||
Thinking: acc.text.String(),
|
||||
Signature: acc.sig.String(),
|
||||
})
|
||||
delete(thinkingByIndex, blockIdx)
|
||||
}
|
||||
}
|
||||
|
||||
case "message_delta":
|
||||
d, _ := event["delta"].(map[string]interface{})
|
||||
if sr, ok := d["stop_reason"].(string); ok {
|
||||
@@ -1051,6 +1143,25 @@ func (rt *claudeRoundTripper) RoundTrip(req *http.Request) (*http.Response, erro
|
||||
}
|
||||
|
||||
case "message_stop":
|
||||
if len(finishedThinking) > 0 {
|
||||
suffix := appendClaudeReasoningRoundTrip("", finishedThinking)
|
||||
if strings.TrimSpace(suffix) != "" {
|
||||
oaiChunk := map[string]interface{}{
|
||||
"choices": []map[string]interface{}{
|
||||
{
|
||||
"delta": map[string]interface{}{
|
||||
"reasoning_content": suffix,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
b, _ := json.Marshal(oaiChunk)
|
||||
if !writeLine("data: " + string(b) + "\n\n") {
|
||||
pw.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
writeLine("data: [DONE]\n\n")
|
||||
pw.Close()
|
||||
return
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package openai
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// claudeReasoningRoundTripSep separates human-readable reasoning from a JSON payload of
|
||||
// Anthropic thinking blocks (with signatures) for multi-turn extended thinking + tools.
|
||||
// Not shown in UI (see DisplayReasoningContent).
|
||||
const claudeReasoningRoundTripSep = "\n---CSAI_CLAUDE_THINKING_BLOCKS---\n"
|
||||
|
||||
// DisplayReasoningContent returns reasoning text suitable for the UI (strips internal
|
||||
// Claude round-trip JSON suffix). Safe for DeepSeek/plain reasoning strings (no-op).
|
||||
func DisplayReasoningContent(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
i := strings.LastIndex(s, claudeReasoningRoundTripSep)
|
||||
if i < 0 {
|
||||
return s
|
||||
}
|
||||
return strings.TrimSpace(s[:i])
|
||||
}
|
||||
|
||||
func appendClaudeReasoningRoundTrip(display string, blocks []claudeContentBlock) string {
|
||||
var payload []map[string]string
|
||||
for _, b := range blocks {
|
||||
if b.Type != "thinking" {
|
||||
continue
|
||||
}
|
||||
payload = append(payload, map[string]string{
|
||||
"type": b.Type,
|
||||
"thinking": b.Thinking,
|
||||
"signature": b.Signature,
|
||||
})
|
||||
}
|
||||
if len(payload) == 0 {
|
||||
return strings.TrimSpace(display)
|
||||
}
|
||||
js, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return strings.TrimSpace(display)
|
||||
}
|
||||
d := strings.TrimSpace(display)
|
||||
if d == "" {
|
||||
return claudeReasoningRoundTripSep + string(js)
|
||||
}
|
||||
return d + claudeReasoningRoundTripSep + string(js)
|
||||
}
|
||||
|
||||
// parseClaudeReasoningAssistantBlocks extracts Anthropic thinking blocks from an OpenAI-style
|
||||
// reasoning_content string. When no suffix is present, blocks is nil (caller must not invent signatures).
|
||||
func parseClaudeReasoningAssistantBlocks(reasoningContent string) (display string, blocks []claudeContentBlock) {
|
||||
reasoningContent = strings.TrimSpace(reasoningContent)
|
||||
if reasoningContent == "" {
|
||||
return "", nil
|
||||
}
|
||||
idx := strings.LastIndex(reasoningContent, claudeReasoningRoundTripSep)
|
||||
if idx < 0 {
|
||||
return reasoningContent, nil
|
||||
}
|
||||
display = strings.TrimSpace(reasoningContent[:idx])
|
||||
jsonPart := strings.TrimSpace(reasoningContent[idx+len(claudeReasoningRoundTripSep):])
|
||||
var arr []struct {
|
||||
Type string `json:"type"`
|
||||
Thinking string `json:"thinking"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(jsonPart), &arr); err != nil {
|
||||
return reasoningContent, nil
|
||||
}
|
||||
for _, x := range arr {
|
||||
if x.Type != "thinking" {
|
||||
continue
|
||||
}
|
||||
blocks = append(blocks, claudeContentBlock{Type: "thinking", Thinking: x.Thinking, Signature: x.Signature})
|
||||
}
|
||||
return display, blocks
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package openai
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDisplayReasoningContent(t *testing.T) {
|
||||
raw := "hello" + claudeReasoningRoundTripSep + `[{"type":"thinking","thinking":"x","signature":"sig"}]`
|
||||
if d := DisplayReasoningContent(raw); d != "hello" {
|
||||
t.Fatalf("got %q", d)
|
||||
}
|
||||
if DisplayReasoningContent("plain") != "plain" {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendParseClaudeReasoningRoundTrip(t *testing.T) {
|
||||
blocks := []claudeContentBlock{
|
||||
{Type: "thinking", Thinking: "a", Signature: "s1"},
|
||||
{Type: "thinking", Thinking: "b", Signature: "s2"},
|
||||
}
|
||||
s := appendClaudeReasoningRoundTrip("sum", blocks)
|
||||
if !strings.Contains(s, claudeReasoningRoundTripSep) {
|
||||
t.Fatal("missing sep")
|
||||
}
|
||||
display, back := parseClaudeReasoningAssistantBlocks(s)
|
||||
if display != "sum" || len(back) != 2 {
|
||||
t.Fatalf("display=%q len=%d", display, len(back))
|
||||
}
|
||||
if back[0].Signature != "s1" || back[1].Thinking != "b" {
|
||||
t.Fatalf("%+v", back)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertOpenAIToClaude_AssistantReasoningReplay(t *testing.T) {
|
||||
rc := appendClaudeReasoningRoundTrip("vis", []claudeContentBlock{
|
||||
{Type: "thinking", Thinking: "t1", Signature: "sig1"},
|
||||
})
|
||||
payload := map[string]interface{}{
|
||||
"model": "claude-3-5-sonnet-latest",
|
||||
"messages": []interface{}{
|
||||
map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": "out",
|
||||
"reasoning_content": rc,
|
||||
},
|
||||
},
|
||||
}
|
||||
req, err := convertOpenAIToClaude(payload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(req.Messages) != 1 {
|
||||
t.Fatalf("messages=%d", len(req.Messages))
|
||||
}
|
||||
blocks := req.Messages[0].Content.Blocks
|
||||
if len(blocks) < 2 {
|
||||
t.Fatalf("blocks=%d", len(blocks))
|
||||
}
|
||||
if blocks[0].Type != "thinking" || blocks[0].Signature != "sig1" {
|
||||
t.Fatalf("first block %+v", blocks[0])
|
||||
}
|
||||
foundText := false
|
||||
for _, b := range blocks {
|
||||
if b.Type == "text" && b.Text == "out" {
|
||||
foundText = true
|
||||
}
|
||||
}
|
||||
if !foundText {
|
||||
t.Fatalf("blocks=%+v", blocks)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeToOpenAIResponseJSON_Thinking(t *testing.T) {
|
||||
claudeBody := []byte(`{
|
||||
"id":"msg_1","type":"message","role":"assistant","model":"x","stop_reason":"end_turn",
|
||||
"content":[
|
||||
{"type":"thinking","thinking":"step","signature":"sigx"},
|
||||
{"type":"text","text":"hi"}
|
||||
]
|
||||
}`)
|
||||
oai, err := claudeToOpenAIResponseJSON(claudeBody)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var wrap map[string]interface{}
|
||||
if err := json.Unmarshal(oai, &wrap); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
choices := wrap["choices"].([]interface{})
|
||||
ch0 := choices[0].(map[string]interface{})
|
||||
msg := ch0["message"].(map[string]interface{})
|
||||
rc, _ := msg["reasoning_content"].(string)
|
||||
if !strings.Contains(rc, "step") || !strings.Contains(rc, claudeReasoningRoundTripSep) {
|
||||
t.Fatalf("reasoning_content=%q", rc)
|
||||
}
|
||||
if msg["content"] != "hi" {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user