Skip to content

fix: DeepSeek reasoning_content 处理和多轮对话兼容性 - #3278

Closed
doushen wants to merge 1 commit into
router-for-me:devfrom
doushen:main
Closed

fix: DeepSeek reasoning_content 处理和多轮对话兼容性#3278
doushen wants to merge 1 commit into
router-for-me:devfrom
doushen:main

Conversation

@doushen

@doushen doushen commented May 8, 2026

Copy link
Copy Markdown

Summary

修复 Codex CLI + DeepSeek 多轮对话失败问题。

核心修复

  • reasoning item 空 text 处理:转换为 [reasoning unavailable] placeholder,满足 DeepSeek V4 要求
  • reasoning.effort 映射:正确映射到 reasoning_effort 字段
  • reasoning_content 透传:在 Responses API → Chat Completions 转换中保留 reasoning_content

其他改进

  • 添加 /responses 根路径路由支持 wire_api="responses"
  • 注册 DeepSeek V4 Pro/Flash 模型到 registry
  • 处理嵌套和扁平格式的 tool 定义(Codex CLI 发送嵌套格式)
  • 跳过空名称的 tool 和空 role 的 message

Test Plan

  • 第一次请求(无历史)→ 成功
  • 第二次请求(带 reasoning + 有内容)→ 成功
  • 第二次请求(带 reasoning + 空 text)→ 成功
  • Streaming 模式 → 成功

Files Changed

File Changes
internal/translator/openai/openai/responses/openai_openai-responses_request.go reasoning 转换逻辑
internal/translator/codex/openai/responses/codex_openai-responses_request.go reasoning_content → reasoning 转换
internal/translator/codex/openai/chat-completions/codex_openai_request.go reasoning_content 处理
internal/registry/models/models.json DeepSeek 模型注册
internal/api/server.go /responses 路路由

修复 Codex CLI + DeepSeek 多轮对话失败问题:
- reasoning item 空 text 转换为 "[reasoning unavailable]" placeholder
- reasoning.effort 正确映射到 reasoning_effort 字段
- 添加 reasoning_content 透传和转换逻辑

其他改进:
- 添加 /responses 根路径路由支持 wire_api="responses"
- 注册 DeepSeek V4 Pro/Flash 模型到 registry
- 处理嵌套和扁平格式的 tool 定义
- 跳过空名称的 tool 和空 role 的 message
@github-actions
github-actions Bot changed the base branch from main to dev May 8, 2026 00:33
@github-actions

github-actions Bot commented May 8, 2026

Copy link
Copy Markdown

This pull request targeted main.

The base branch has been automatically changed to dev.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for DeepSeek V4 models, including reasoning/thinking capabilities, and adds compatibility routes for the Codex CLI. Key changes include updating the model registry, implementing reasoning_content handling for multi-turn conversations in both chat completions and responses translators, and refining tool conversion logic. Feedback focuses on reducing code duplication in route registration and tool processing, as well as simplifying JSON manipulation using sjson features and string formatting.

Comment thread internal/api/server.go
Comment on lines +377 to +379
s.engine.GET("/responses", AuthMiddleware(s.accessManager), openaiResponsesHandlers.ResponsesWebsocket)
s.engine.POST("/responses", AuthMiddleware(s.accessManager), openaiResponsesHandlers.Responses)
s.engine.POST("/responses/compact", AuthMiddleware(s.accessManager), openaiResponsesHandlers.Compact)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

您在这里添加的路由与 L361-L363 和 L370-L372 处的路由注册逻辑几乎完全相同。这造成了代码重复,未来如果需要修改这些路由(例如,添加新的中间件),将需要在三个地方同步修改,容易出错。

为了提高代码的可维护性,建议将这部分重复的路由注册逻辑提取到一个公共的辅助函数中。

Comment on lines +216 to +220
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

这里的 JSON 构建过程略显繁琐。为了使代码更简洁易读,可以考虑使用 fmt.Sprintf 配合 %q 格式化动词来直接生成 reasoningPart 的 JSON 字符串。%q 会为字符串正确地添加引号并处理转义,可以有效防止注入问题。

(请注意,这需要导入 fmt 包。)

Suggested change
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)
reasoningPartJSON := fmt.Sprintf(`{"type":"reasoning","summary":[{"type":"summary_text","text":%q}]}`, rc.String())
msg, _ = sjson.SetRawBytes(msg, "content.-1", []byte(reasoningPartJSON))

Comment on lines +137 to +154
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

这部分用于向 content 数组添加 reasoning 对象的逻辑有些复杂。您手动检查了数组是否存在,如果不存在则创建,然后获取数组长度来计算新元素的索引。

sjson 库支持使用 .-1 路径来向数组末尾追加元素,并且会自动创建不存在的父级对象或数组。利用这个特性可以大大简化代码。

Suggested change
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)
// Add reasoning item to content
reasoningItem := []byte(`{"type":"reasoning","summary":[{"type":"summary_text","text":""}]}`)
reasoningItem, _ = sjson.SetBytes(reasoningItem, "summary.0.text", rc.String())
contentPath := fmt.Sprintf("input.%d.content.-1", i)
result, _ = sjson.SetRawBytes(result, contentPath, reasoningItem)

Comment on lines +224 to 255
// 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")
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

if/else 块中的代码存在大量重复。两个分支都在处理从 toolnestedFunction 中提取 name, description, parameters 的逻辑,并且都包含了确保 parameters.typeobject 的检查。

为了消除重复并提高代码的可读性,建议重构此部分。可以先确定属性的来源(nestedFunctiontool 本身),然后对该来源执行一次提取和检查逻辑。

			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")
			}

@luispater

Copy link
Copy Markdown
Collaborator

你提交了过多与修复无关的代码,并且项目主体代码中不允许出现中文注释。

请清理PR的代码后重新PR。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants