#7285·sub2api

[Bug] DeepSeek 的 /responses 接受但静默忽略 input[].additional_tools,Codex (Responses Lite) 工具调用退化为 DSML 文本

Author: ranxi2001Created Sep 17, 2026Updated Sep 17, 2026

用户故事

我是一个用 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-flashgpt-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 的回复里,终端/文件读取完全不发生。

复现步骤

  1. 在 sub2api 建 platform=openaitype=apikey 的账号,base_url=https://api.deepseek.com(不带 /v1)。
  2. 配好模型映射(gpt-5.6-soldeepseek-flash 之类),让 Codex 的请求能命中该账号。
  3. 确认该账号 extra.openai_responses_supportedtrue(自动探测的默认结论)。
  4. 用 Codex 0.154 发一个需要调用本地 shell 的任务,例如「用你的 exec 工具跑 echo hello」。
  5. 观察:模型返回上面的 DSML 文本,工具不执行。

分析

1. Codex 0.154 用 Responses Lite 声明工具

抓包看 Codex 发到 sub2api 的 /v1/responses 请求,顶层没有 tools 字段,工具声明在 input[] 里,形如:

json
{
  "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,所以不是路径问题):

工具声明的形状 上游返回 模型是否真的调用工具
顶层 toolsfunction 200 ✅ 正常返回 function_call
顶层 toolsnamespacefunction 200 ✅ 正常返回 function_call
顶层 toolscustom 400 只允许 apply_patch,其它 custom 直接拒绝
顶层 toolsnamespacecustom 400 Currently custom tools are not allowed inside a namespace
input[].additional_toolscustom 200 ❌ 被忽略,模型不知道有工具
input[].additional_toolsfunction 200 ❌ 被忽略
input[].additional_toolsnamespacefunction 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 工具;
  • 回程再还原成带 namespacecustom_tool_call

我这边实测这条路是通的(前提是带上 #7279 / #7241 里那个 namespace 内 custom 子工具的修复——没有它,namespace 里的 exec 会在转换时被静默丢弃,那就是 #7241 的 functions__exec 现象)。

修复方向

按「尽量不需要用户手工配置」的目标,我建议下面任一条(也可组合):

  1. 让探测具备语义openai_apikey_responses_probe 除了看 /responses 的 HTTP 状态,再发一个带 input[].additional_tools(或顶层 custom)的最小请求,确认上游真的解析了工具声明。只要发现「200 但工具被忽略」,就落 openai_responses_supported=false,自动改走 chat 桥,用户无需任何手工开关。
  2. 原生 Responses 路径补上提升逻辑:在原生 CN Responses 分支(openai_gateway_forward.gonativeDeepSeekResponses 那段)也做一次 liftResponsesAdditionalTools + custom → function 降级,并对回程做还原,与 Anthropic 路径的 adaptResponsesClientToolsForAnthropic 对齐。这样 platform=deepseek 的账号也能直接用 Codex。
  3. 补回归测试:覆盖「input[].additional_toolsnamespacecustom」在原生 Responses 路径上的行为,避免以后只测 chat 桥这一侧。

补充一点实测事实,可能对文档/探测都有用:DeepSeek 的 /responses 端点存在,且 /responses/v1/responses 均返回 200,但它只认顶层 tools,且顶层 custom 只允许 apply_patch(其余 400),namespace 内不允许 custom(400)。所以对 Codex 而言,DeepSeek 走 chat 桥(工具摊平为 function)才是可靠路径。