-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
fix: DeepSeek reasoning_content 处理和多轮对话兼容性 #3278
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -10,6 +10,7 @@ import ( | |||||||||||||||
| "strconv" | ||||||||||||||||
| "strings" | ||||||||||||||||
|
|
||||||||||||||||
| log "github.com/sirupsen/logrus" | ||||||||||||||||
| "github.com/tidwall/gjson" | ||||||||||||||||
| "github.com/tidwall/sjson" | ||||||||||||||||
| ) | ||||||||||||||||
|
|
@@ -28,6 +29,16 @@ import ( | |||||||||||||||
| // - []byte: The transformed request data in OpenAI Responses API format | ||||||||||||||||
| func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream bool) []byte { | ||||||||||||||||
| rawJSON := inputRawJSON | ||||||||||||||||
|
|
||||||||||||||||
| // DEBUG: 打印原始请求 | ||||||||||||||||
| log.Debugf("codex chat-completions: raw input JSON (first 2000 chars): %s", func() string { | ||||||||||||||||
| s := string(rawJSON) | ||||||||||||||||
| if len(s) > 2000 { | ||||||||||||||||
| return s[:2000] + "..." | ||||||||||||||||
| } | ||||||||||||||||
| return s | ||||||||||||||||
| }()) | ||||||||||||||||
|
|
||||||||||||||||
| // Start with empty JSON object | ||||||||||||||||
| out := []byte(`{"instructions":""}`) | ||||||||||||||||
|
|
||||||||||||||||
|
|
@@ -197,6 +208,19 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b | |||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // Handle reasoning_content for assistant messages (DeepSeek requires passing back in multi-turn) | ||||||||||||||||
| // Convert reasoning_content string to reasoning type with summary | ||||||||||||||||
| if role == "assistant" { | ||||||||||||||||
| rc := m.Get("reasoning_content") | ||||||||||||||||
| if rc.Exists() && rc.Type == gjson.String && rc.String() != "" { | ||||||||||||||||
| reasoningPart := []byte(`{}`) | ||||||||||||||||
| reasoningPart, _ = sjson.SetBytes(reasoningPart, "type", "reasoning") | ||||||||||||||||
| reasoningPart, _ = sjson.SetRawBytes(reasoningPart, "summary", []byte(`[{"type":"summary_text","text":""}]`)) | ||||||||||||||||
| reasoningPart, _ = sjson.SetBytes(reasoningPart, "summary.0.text", rc.String()) | ||||||||||||||||
| msg, _ = sjson.SetRawBytes(msg, "content.-1", reasoningPart) | ||||||||||||||||
|
Comment on lines
+216
to
+220
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这里的 JSON 构建过程略显繁琐。为了使代码更简洁易读,可以考虑使用 (请注意,这需要导入
Suggested change
|
||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // Don't emit empty assistant messages when only tool_calls | ||||||||||||||||
| // are present — Responses API needs function_call items | ||||||||||||||||
| // directly, otherwise call_id matching fails (#2132). | ||||||||||||||||
|
|
@@ -356,6 +380,16 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b | |||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| out, _ = sjson.SetBytes(out, "store", false) | ||||||||||||||||
|
|
||||||||||||||||
| // DEBUG: 打印翻译后的请求 | ||||||||||||||||
| log.Debugf("codex chat-completions: translated output JSON (first 2000 chars): %s", func() string { | ||||||||||||||||
| s := string(out) | ||||||||||||||||
| if len(s) > 2000 { | ||||||||||||||||
| return s[:2000] + "..." | ||||||||||||||||
| } | ||||||||||||||||
| return s | ||||||||||||||||
| }()) | ||||||||||||||||
|
|
||||||||||||||||
| return out | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -11,6 +11,15 @@ import ( | |||||||||||||||||||||||||||||||||||||||||||||||||
| func ConvertOpenAIResponsesRequestToCodex(modelName string, inputRawJSON []byte, _ bool) []byte { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| rawJSON := inputRawJSON | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // DEBUG: 打印原始请求 | ||||||||||||||||||||||||||||||||||||||||||||||||||
| log.Debugf("codex responses: raw input JSON (first 2000 chars): %s", func() string { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| s := string(rawJSON) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if len(s) > 2000 { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return s[:2000] + "..." | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return s | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }()) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| inputResult := gjson.GetBytes(rawJSON, "input") | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if inputResult.Type == gjson.String { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| input, _ := sjson.SetBytes([]byte(`[{"type":"message","role":"user","content":[{"type":"input_text","text":""}]}]`), "0.content.0.text", inputResult.String()) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -40,8 +49,19 @@ func ConvertOpenAIResponsesRequestToCodex(modelName string, inputRawJSON []byte, | |||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Convert role "system" to "developer" in input array to comply with Codex API requirements. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| rawJSON = convertSystemRoleToDeveloper(rawJSON) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Convert reasoning_content to reasoning type for DeepSeek multi-turn compatibility. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| rawJSON = convertReasoningContentToReasoning(rawJSON) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| rawJSON = normalizeCodexBuiltinTools(rawJSON) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // DEBUG: 打印翻译后的请求 | ||||||||||||||||||||||||||||||||||||||||||||||||||
| log.Debugf("codex responses: translated output JSON (first 2000 chars): %s", func() string { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| s := string(rawJSON) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if len(s) > 2000 { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return s[:2000] + "..." | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return s | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }()) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return rawJSON | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -85,6 +105,61 @@ func convertSystemRoleToDeveloper(rawJSON []byte) []byte { | |||||||||||||||||||||||||||||||||||||||||||||||||
| return result | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // convertReasoningContentToReasoning traverses the input array and converts | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // reasoning_content in assistant messages to reasoning type with summary. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // DeepSeek requires passing reasoning_content back in multi-turn conversations. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| func convertReasoningContentToReasoning(rawJSON []byte) []byte { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| inputResult := gjson.GetBytes(rawJSON, "input") | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if !inputResult.IsArray() { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return rawJSON | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| inputArray := inputResult.Array() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| result := rawJSON | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| for i := 0; i < len(inputArray); i++ { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| rolePath := fmt.Sprintf("input.%d.role", i) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| role := gjson.GetBytes(result, rolePath).String() | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Only process assistant messages | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if role != "assistant" { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Check if reasoning_content exists | ||||||||||||||||||||||||||||||||||||||||||||||||||
| rcPath := fmt.Sprintf("input.%d.reasoning_content", i) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| rc := gjson.GetBytes(result, rcPath) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if !rc.Exists() || rc.Type != gjson.String || rc.String() == "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Get content array | ||||||||||||||||||||||||||||||||||||||||||||||||||
| contentPath := fmt.Sprintf("input.%d.content", i) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| contentResult := gjson.GetBytes(result, contentPath) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if !contentResult.IsArray() { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Create content array if it doesn't exist | ||||||||||||||||||||||||||||||||||||||||||||||||||
| result, _ = sjson.SetRawBytes(result, contentPath, []byte(`[]`)) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Add reasoning item to content | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Find the last index of content array | ||||||||||||||||||||||||||||||||||||||||||||||||||
| contentArray := gjson.GetBytes(result, contentPath).Array() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| lastIdx := len(contentArray) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Create reasoning summary item | ||||||||||||||||||||||||||||||||||||||||||||||||||
| reasoningItemPath := fmt.Sprintf("input.%d.content.%d", i, lastIdx) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| reasoningItem := []byte(`{"type":"reasoning","summary":[{"type":"summary_text","text":""}]}`) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| reasoningItem, _ = sjson.SetBytes(reasoningItem, "summary.0.text", rc.String()) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| result, _ = sjson.SetRawBytes(result, reasoningItemPath, reasoningItem) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+137
to
+154
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这部分用于向
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Delete the original reasoning_content field | ||||||||||||||||||||||||||||||||||||||||||||||||||
| result, _ = sjson.DeleteBytes(result, rcPath) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return result | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // normalizeCodexBuiltinTools rewrites legacy/preview built-in tool variants to the | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // stable names expected by the current Codex upstream. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| func normalizeCodexBuiltinTools(rawJSON []byte) []byte { | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,7 @@ import ( | |
| // - []byte: The transformed request data in OpenAI chat completions format | ||
| func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inputRawJSON []byte, stream bool) []byte { | ||
| rawJSON := inputRawJSON | ||
|
|
||
| // Base OpenAI chat completions template with default values | ||
| out := []byte(`{"model":"","messages":[],"stream":false}`) | ||
|
|
||
|
|
@@ -67,6 +68,10 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu | |
| case "message", "": | ||
| // Handle regular message conversion | ||
| role := item.Get("role").String() | ||
| // Skip items with empty role - these are invalid messages | ||
| if role == "" { | ||
| return true | ||
| } | ||
| if role == "developer" { | ||
| role = "user" | ||
| } | ||
|
|
@@ -109,6 +114,14 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu | |
| message, _ = sjson.SetBytes(message, "content", content.String()) | ||
| } | ||
|
|
||
| // Handle reasoning_content in message (for DeepSeek thinking mode) | ||
| // When assistant message has reasoning_content, pass it through | ||
| if role == "assistant" { | ||
| if rc := item.Get("reasoning_content"); rc.Exists() && rc.String() != "" { | ||
| message, _ = sjson.SetBytes(message, "reasoning_content", rc.String()) | ||
| } | ||
| } | ||
|
|
||
| out, _ = sjson.SetRawBytes(out, "messages.-1", message) | ||
|
|
||
| case "function_call": | ||
|
|
@@ -145,6 +158,38 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu | |
| } | ||
|
|
||
| out, _ = sjson.SetRawBytes(out, "messages.-1", toolMessage) | ||
|
|
||
| case "reasoning": | ||
| // Handle reasoning item conversion for DeepSeek thinking mode | ||
| // DeepSeek requires reasoning_content to be passed back in subsequent requests | ||
| // The reasoning item has a summary array with text content | ||
| summary := item.Get("summary") | ||
| if summary.Exists() && summary.IsArray() { | ||
| // Concatenate all summary text parts | ||
| var reasoningText strings.Builder | ||
| summary.ForEach(func(_, summaryItem gjson.Result) bool { | ||
| if summaryItem.Get("type").String() == "summary_text" { | ||
| text := summaryItem.Get("text").String() | ||
| if text != "" { | ||
| reasoningText.WriteString(text) | ||
| } | ||
| } | ||
| return true | ||
| }) | ||
|
|
||
| // DeepSeek V4 requires reasoning_content even if empty | ||
| // When Codex CLI sends empty reasoning text, fill with placeholder | ||
| reasoningStr := reasoningText.String() | ||
| if reasoningStr == "" { | ||
| reasoningStr = "[reasoning unavailable]" | ||
| } | ||
|
|
||
| // Create assistant message with reasoning_content for DeepSeek | ||
| // DeepSeek expects: {"role":"assistant","content":"","reasoning_content":"..."} | ||
| reasoningMessage := []byte(`{"role":"assistant","content":"","reasoning_content":""}`) | ||
| reasoningMessage, _ = sjson.SetBytes(reasoningMessage, "reasoning_content", reasoningStr) | ||
| out, _ = sjson.SetRawBytes(out, "messages.-1", reasoningMessage) | ||
| } | ||
| } | ||
|
|
||
| return true | ||
|
|
@@ -173,18 +218,45 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu | |
| chatTool := []byte(`{"type":"function","function":{}}`) | ||
|
|
||
| // Convert tool structure from responses format to chat completions format | ||
| // Handle both flat format {"name": "xxx"} and nested format {"function": {"name": "xxx"}} | ||
| function := []byte(`{"name":"","description":"","parameters":{}}`) | ||
|
|
||
| if name := tool.Get("name"); name.Exists() { | ||
| function, _ = sjson.SetBytes(function, "name", name.String()) | ||
| } | ||
|
|
||
| if description := tool.Get("description"); description.Exists() { | ||
| function, _ = sjson.SetBytes(function, "description", description.String()) | ||
| // Try nested format first (Codex CLI sends {"type": "function", "function": {"name": "xxx"}}) | ||
| nestedFunction := tool.Get("function") | ||
| if nestedFunction.Exists() && nestedFunction.IsObject() { | ||
| if name := nestedFunction.Get("name"); name.Exists() { | ||
| function, _ = sjson.SetBytes(function, "name", name.String()) | ||
| } | ||
| if description := nestedFunction.Get("description"); description.Exists() { | ||
| function, _ = sjson.SetBytes(function, "description", description.String()) | ||
| } | ||
| if parameters := nestedFunction.Get("parameters"); parameters.Exists() { | ||
| function, _ = sjson.SetRawBytes(function, "parameters", []byte(parameters.Raw)) | ||
| } | ||
| // Ensure parameters has type: object (required by most providers) | ||
| if !gjson.GetBytes(function, "parameters.type").Exists() { | ||
| function, _ = sjson.SetBytes(function, "parameters.type", "object") | ||
| } | ||
| } else { | ||
| // Fall back to flat format {"type": "function", "name": "xxx"} | ||
| if name := tool.Get("name"); name.Exists() { | ||
| function, _ = sjson.SetBytes(function, "name", name.String()) | ||
| } | ||
| if description := tool.Get("description"); description.Exists() { | ||
| function, _ = sjson.SetBytes(function, "description", description.String()) | ||
| } | ||
| if parameters := tool.Get("parameters"); parameters.Exists() { | ||
| function, _ = sjson.SetRawBytes(function, "parameters", []byte(parameters.Raw)) | ||
| } | ||
| // Ensure parameters has type: object (required by most providers) | ||
| if !gjson.GetBytes(function, "parameters.type").Exists() { | ||
| function, _ = sjson.SetBytes(function, "parameters.type", "object") | ||
| } | ||
| } | ||
|
Comment on lines
+224
to
255
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
为了消除重复并提高代码的可读性,建议重构此部分。可以先确定属性的来源( var functionSource gjson.Result
nestedFunction := tool.Get("function")
if nestedFunction.Exists() && nestedFunction.IsObject() {
functionSource = nestedFunction
} else {
functionSource = tool
}
if name := functionSource.Get("name"); name.Exists() {
function, _ = sjson.SetBytes(function, "name", name.String())
}
if description := functionSource.Get("description"); description.Exists() {
function, _ = sjson.SetBytes(function, "description", description.String())
}
if parameters := functionSource.Get("parameters"); parameters.Exists() {
function, _ = sjson.SetRawBytes(function, "parameters", []byte(parameters.Raw))
}
// Ensure parameters has type: object (required by most providers)
if !gjson.GetBytes(function, "parameters.type").Exists() {
function, _ = sjson.SetBytes(function, "parameters.type", "object")
} |
||
|
|
||
| if parameters := tool.Get("parameters"); parameters.Exists() { | ||
| function, _ = sjson.SetRawBytes(function, "parameters", []byte(parameters.Raw)) | ||
| // Skip tools with empty names (invalid for most providers) | ||
| if gjson.GetBytes(function, "name").String() == "" { | ||
| return true | ||
| } | ||
|
|
||
| chatTool, _ = sjson.SetRawBytes(chatTool, "function", function) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
您在这里添加的路由与 L361-L363 和 L370-L372 处的路由注册逻辑几乎完全相同。这造成了代码重复,未来如果需要修改这些路由(例如,添加新的中间件),将需要在三个地方同步修改,容易出错。
为了提高代码的可维护性,建议将这部分重复的路由注册逻辑提取到一个公共的辅助函数中。