[Bug] DeepSeek 的 /responses 接受但静默忽略 input[].additional_tools,Codex (Responses Lite) 工具调用退化为 DSML 文本
用户故事
我是一个用 Codex 的普通用户。最近官方 GPT 模型经常满载,我看到 sub2api 支持「把 Codex 用的 GPT 模型名映射到其它上游」,就想把 DeepSeek 接进来给 Codex 用:
- 建一个
platform=openai的 API Key 账号,base_url指向https://api.deepseek.com; - 用模型映射把
gpt-5.6-sol/gpt-5.6-luna映射到deepseek-flash,gpt-6-astra/gpt-6映射到deepseek-v4-pro; - 账号创建时被自动探测标记为
extra.openai_responses_supported = true,于是按原生 Responses 协议转发到上游/responses。
结果:普通对话一切正常,但只要让 Codex 读文件、跑命令,工具就完全失效。
模型不会返回真正的工具调用,而是把「调用」当正文吐出来(现象与 #5536 完全一致):
<||DSML||tool_calls>
<||DSML||invoke name="exec_command">
<||DSML||parameter name="cmd" string="true">pwd && rg --files -g '!node_modules' | sed -n '1,200p'</||DSML||parameter>
</||DSML||invoke>
</||DSML||tool_calls>这段 markup 会原样显示在 Codex 的回复里,终端/文件读取完全不发生。
复现步骤
- 在 sub2api 建
platform=openai、type=apikey的账号,base_url=https://api.deepseek.com(不带/v1)。 - 配好模型映射(
gpt-5.6-sol→deepseek-flash之类),让 Codex 的请求能命中该账号。 - 确认该账号
extra.openai_responses_supported为true(自动探测的默认结论)。 - 用 Codex 0.154 发一个需要调用本地 shell 的任务,例如「用你的 exec 工具跑
echo hello」。 - 观察:模型返回上面的 DSML 文本,工具不执行。
分析
1. Codex 0.154 用 Responses Lite 声明工具
抓包看 Codex 发到 sub2api 的 /v1/responses 请求,顶层没有 tools 字段,工具声明在 input[] 里,形如:
{
"input": [
{
"type": "additional_tools",
"role": "developer",
"tools": [
{ "type": "namespace", "name": "functions",
"tools": [ { "type": "custom", "name": "exec", "...": "..." } ] },
{ "type": "namespace", "name": "collaboration",
"tools": [ { "type": "custom", "name": "spawn", "...": "..." } ] }
]
}
]
}即 namespace 里嵌着 custom 工具。这是 Responses Lite 的形态。
2. 根因:DeepSeek 的 /responses 会接受 additional_tools,但静默忽略它
直接用同一个 key 打 https://api.deepseek.com/v1/responses 做对照实验(/responses 与 /v1/responses 两个路径都存在且都返回 200,所以不是路径问题):
| 工具声明的形状 | 上游返回 | 模型是否真的调用工具 |
|---|---|---|
顶层 tools 放 function |
200 | ✅ 正常返回 function_call |
顶层 tools 放 namespace → function |
200 | ✅ 正常返回 function_call |
顶层 tools 放 custom |
400 | 只允许 apply_patch,其它 custom 直接拒绝 |
顶层 tools 放 namespace → custom |
400 | Currently custom tools are not allowed inside a namespace |
input[].additional_tools 放 custom |
200 | ❌ 被忽略,模型不知道有工具 |
input[].additional_tools 放 function |
200 | ❌ 被忽略 |
input[].additional_tools 放 namespace→function |
200 | ❌ 被忽略 |
最后三行是问题的核心:上游对 additional_tools 返回 200,但完全不解析其中的工具声明,模型看到的工具列表是空的,于是只能「假装」调用——把调用写成正文 markup(DSML / <tool_call>{...}</tool_call>)。
也就是说 additional_tools 不是「协议错误」,而是静默降级,HTTP 层面完全看不出来。
3. 为什么流量会走到这条路上
shouldForwardOpenAIResponsesViaRawChatCompletions() 对 platform=openai 的账号只看 openai_compat.ShouldUseResponsesAPI(account.Extra),而后者源自账号探测标记 openai_responses_supported。探测只验证 /responses 是否可用(HTTP 状态),探不出「接受但不解析 additional_tools」这种语义差异,于是账号被判为「支持 Responses」,请求走了原生 Responses 路径,additional_tools 被原样转发给上游,然后被静默丢弃。
platform=deepseek 的账号同理:UsesNativeCNResponses() 为真时也走原生 /responses,所以 #5536 里「协议选 response 协议」的配置必然踩同一个坑。
4. 对照:chat 回退路径其实是好的
把同一个账号的 extra.openai_responses_mode 设成 force_chat_completions 后,请求改走 Responses → Chat Completions 桥:
apicompat.EffectiveResponsesTools()会把input[].additional_tools提升为顶层工具;namespaceChildrenToChatTools()把 namespace 子工具摊平成 Chat function 工具;- 回程再还原成带
namespace的custom_tool_call。
我这边实测这条路是通的(前提是带上 #7279 / #7241 里那个 namespace 内 custom 子工具的修复——没有它,namespace 里的 exec 会在转换时被静默丢弃,那就是 #7241 的 functions__exec 现象)。
修复方向
按「尽量不需要用户手工配置」的目标,我建议下面任一条(也可组合):
- 让探测具备语义:
openai_apikey_responses_probe除了看/responses的 HTTP 状态,再发一个带input[].additional_tools(或顶层custom)的最小请求,确认上游真的解析了工具声明。只要发现「200 但工具被忽略」,就落openai_responses_supported=false,自动改走 chat 桥,用户无需任何手工开关。 - 原生 Responses 路径补上提升逻辑:在原生 CN Responses 分支(
openai_gateway_forward.go里nativeDeepSeekResponses那段)也做一次liftResponsesAdditionalTools+custom → function降级,并对回程做还原,与 Anthropic 路径的adaptResponsesClientToolsForAnthropic对齐。这样platform=deepseek的账号也能直接用 Codex。 - 补回归测试:覆盖「
input[].additional_tools里namespace嵌custom」在原生 Responses 路径上的行为,避免以后只测 chat 桥这一侧。
补充一点实测事实,可能对文档/探测都有用:DeepSeek 的 /responses 端点存在,且 /responses 与 /v1/responses 均返回 200,但它只认顶层 tools,且顶层 custom 只允许 apply_patch(其余 400),namespace 内不允许 custom(400)。所以对 Codex 而言,DeepSeek 走 chat 桥(工具摊平为 function)才是可靠路径。
Source: Wei-Shaw/sub2api