Skip to content

fix(apicompat): 补齐 Codex 0.14x 工具链经 chat 回退桥的转换(custom/tool_search/namespace)#3989

Open
xlplbo wants to merge 9 commits into
Wei-Shaw:mainfrom
xlplbo:fix/codex-mcp-tools-bridge
Open

fix(apicompat): 补齐 Codex 0.14x 工具链经 chat 回退桥的转换(custom/tool_search/namespace)#3989
xlplbo wants to merge 9 commits into
Wei-Shaw:mainfrom
xlplbo:fix/codex-mcp-tools-bridge

Conversation

@xlplbo

@xlplbo xlplbo commented Jul 10, 2026

Copy link
Copy Markdown

概述

补齐 Codex 0.14x 客户端经 chat completions 回退桥(仅支持 /v1/chat/completions 的第三方上游)的工具链转换,共 4 个提交:

  • fix(apicompat): responses→chat 桥支持 custom 工具,修复 Codex exec 丢失
  • feat(apicompat): 添加 tool_search 支持,增强工具调用处理逻辑
  • fix(apicompat): 回程还原 namespace 子工具调用,修复 Codex MCP 工具 unsupported call
  • fix(apicompat): namespace 摊平名撞名时显式拒绝请求

问题背景

Codex 0.14x 把核心执行工具 exec 改为 type=custom、把 MCP 工具声明为 type=namespace,并对 gpt-5.4+ 模型名启用 tool_search 延迟加载。此前回退桥会丢弃这些工具类型、或只做去程摊平而不做回程还原,表现为:

  • exec 工具丢失,模型无法执行任何命令;
  • MCP 工具调用全部被 codex 判为 unsupported call: mcp__<server>__<tool>(回程 function_call 项缺独立 namespace 字段,codex 按 namespace+name 路由失败)。

主要改动

  • 去程:custom 工具降级为 function 透传;tool_search 降级为同名代理 function;namespace 子工具摊平为 <namespace>__<name>(超 64 字符截断加 sha256 短哈希);
  • 回程:按名字/映射把调用还原为 custom_tool_call / tool_search_call(wire 层强制 execution=client)/ 带 namespace 字段的 function_call
  • 摊平名与顶层工具撞名、或跨 namespace 摊平出同名时,在请求转换阶段显式返回 400 并点名冲突双方(歧义由摊平转换制造且不可消除,静默降级会把调用还原到错误工具);
  • ResponsesOutput 新增 Namespace 字段,同步 wire 层白名单与序列化断言。

验证

  • go test -tags=unit ./... 全绿;golangci-lint 0 issues;
  • 真实 codex CLI 0.144.1 + stdio MCP server 经网关端到端验证(apikey 账号 + force_chat_completions,上游 glm-5.2):
    • 模型名 glm-5.2(namespace 全量内联模式):MCP 工具发现、调用、结果回传全链路成功;
    • 模型名 gpt-5.5(tool_search 延迟加载模式):tool_search 代理调用 → 客户端检索 → 工具注入 → 调用成功;
    • exec custom 工具链回归通过。

🤖 Generated with Claude Code

https://claude.ai/code/session_01NWQyEgFKKbdve67G6qCoAU

xlplbo and others added 5 commits July 7, 2026 16:23
Codex 0.14x 的核心执行工具 exec 是 type=custom(freeform)工具,
responsesToolsToChatTools 丢弃所有非 function 工具,导致模型工具列表中
没有 exec、无法执行任何命令;SSE 序列化层 responsesItemWire 也缺少
custom_tool_call 分支,即使产出该类型 item,call_id/name/input 也会被
白名单剥离。

- custom 工具降级为单一 input:string 参数的 function 工具
- 回程(非流式+流式)按名字还原 custom_tool_call 项/事件
- 历史 custom_tool_call / custom_tool_call_output 项转换(此前被跳过)
- responsesItemWire 补 custom_tool_call 分支
- tools 全部被丢弃时不再转发 tool_choice(修复上游 400
  "'tool_choice' is only allowed when 'tools' are specified")

feat(apicompat): chat 桥补齐 tool_search/namespace/字符串工具声明

对齐参考实现(cc-switch transform_codex_chat.rs)的工具类型覆盖:
- tool_search → 固定 function 代理工具(query 必填/limit 可选),
  历史项 tool_search_call/tool_search_output 一并转换
- namespace → 子 function 工具摊平为 <namespace>__<name>,
  超 64 字符截断加 sha256 短哈希;历史项带 namespace 字段时同规则摊平
- tools: ["name"] 字符串简写按 custom 工具解析

fix(apicompat): 工具调用宣告延迟到类型可判定,兼容 name 迟到的上游

output_item.added 的 custom/function 判定依赖工具名。名字晚于首个分片
到达时,此前会按 function_call 宣告造成 added/done 项类型不一致。
改为存在 custom 工具且名字未到时延迟宣告,宣告时补发已累积的参数增量;
请求无 custom 工具时行为不变。

PR 标题与正文(可直接粘贴)

标题:fix(apicompat): responses→chat 桥丢弃 custom 工具导致 Codex 无法执行命令

正文:

## 问题

OpenAI APIKey 账号接入仅支持 /v1/chat/completions 的第三方上游
(DeepSeek/GLM/Kimi 等)时,Codex 0.14x 客户端文本对话正常,但无法执行
任何命令:exec 调用全部 "aborted",spawn_agent 派出的子智能体永远卡在
running,多轮后模型自述"环境无法执行文件系统命令"放弃。

## 根因

Codex 0.14x 的核心执行工具 exec 是 Responses API 的 custom(freeform)
工具,而非普通 function:

    {"type": "custom", "name": "exec", "format": {"type": "grammar", ...}}

两处缺陷叠加:

1. `responsesToolsToChatTools` 丢弃所有非 function 工具
   (chatcompletions_responses_bridge.go),模型工具列表中从未出现 exec;
2. SSE 序列化 `responsesItemWire` 按 item 类型白名单重组字段,无
   custom_tool_call 分支,call_id/name/input 会被剥离
   (responses_stream_event_wire.go)。

次生缺陷:工具全部被丢弃时 tool_choice 仍被转发,部分上游报 400
"'tool_choice' is only allowed when 'tools' are specified"。

## 改动

(三个提交的要点清单,见各提交说明)

设计与 cc-switch 的 Codex chat 转换层(transform_codex_chat.rs /
streaming_codex_chat.rs)对齐:custom 工具降级为 {"input": string}
function 工具;custom 调用的参数增量不可流式还原为自由文本,缓冲至
收尾一次性下发 custom_tool_call_input.delta/done。

## 测试

- 新增 17 个单元测试:请求工具转换、tool_choice 兜底、历史项转换、
  非流式/流式回程还原、wire 序列化字段完整性、name 迟到宣告、命名摊平
- `go test ./internal/pkg/apicompat/...` 与 service 相关测试全绿
- 真机验证:Codex CLI 0.142.5 → 本桥 → GLM/DeepSeek 系 chat-only 上游,
  exec 实际执行命令并多轮续作正常

## 影响面

仅影响 "APIKey 账号 + 判定不支持 Responses" 的 chat 直转路径:

- OAuth 账号与透传路径不经过本桥(透传为字节级原文转发)
- CC→Responses 反方向仅构造 ResponsesRequest,新增字段带 omitempty
  且不赋值,序列化输出不变
- 探测逻辑未动,路由判定不变
- 共享类型改动均为增量容错(新字段 omitempty;UnmarshalJSON 只新增
  字符串工具的解析成功路径)
解决 backend/internal/service/openai_gateway_responses_chat_fallback.go 冲突:
将本地 tool_search/custom 工具改动(customTools/toolSearch 穿参与流状态标记)
移植到上游重构后的 CC 公共管线;同步适配上游新增调用点
openai_gateway_messages_chat_fallback.go 的 ChatCompletionsResponseToResponses 签名。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8zMzeFAumSddraEw1FUeG
Codex 0.14x 将 MCP 工具声明为 namespace 工具,chat 桥去程摊平为
"<namespace>__<name>" function 工具后,回程仅原样回传平铺名的
function_call 项;codex 按 namespace+name 路由查不到该名字,所有
MCP 工具调用被判为 unsupported call。

- NamespaceToolNames 构建摊平名 →(namespace, 子工具名)反查表
  (摊平名超长带截断哈希,无法按字符串切分还原)
- 非流式/流式回程命中映射时还原为裸子工具名 + namespace 字段,
  ResponsesOutput 新增 Namespace 字段并同步 wire 层白名单
- 回退桥入口将映射与 CustomTools/ToolSearchDeclared 一并穿入

已在测试机经 codex exec + MCP server 端到端验证。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWQyEgFKKbdve67G6qCoAU
摊平名与顶层 function/custom 工具撞名、或跨 namespace 摊平出同名时,
chat 上游无法按 namespace 区分调用归属:此前重复声明照发上游、回程
固定命中其中一条映射,调用可能被还原到错误工具。这类请求在原生
Responses 上游合法,歧义由摊平转换制造且不可消除,改为在请求转换
阶段直接报错(网关返回 400 invalid_request_error 并点名冲突双方);
同一 (namespace, 子工具) 的重复声明去重后不拒绝。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWQyEgFKKbdve67G6qCoAU
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA. ✅
Posted by the CLA Assistant Lite bot.

@xlplbo

xlplbo commented Jul 10, 2026

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

github-actions Bot added a commit that referenced this pull request Jul 10, 2026
xlplbo and others added 3 commits July 10, 2026 20:46
tool_search 服务端工具降级后的代理 function 不能改名(codex 的模型侧按
tool_search 这个名字调用),与客户端声明的同名 function/custom 工具无法
区分:去程产生重复声明,回程会把普通工具的调用劫持还原成 tool_search_call。
与 namespace 摊平撞名的处理一致,在请求转换阶段显式报错(网关 400);
重复声明 type=tool_search 去重后只产出一个代理,不拒绝。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWQyEgFKKbdve67G6qCoAU
此前只检查转换后是否还剩任意工具,不校验 tool_choice 指向的工具是否幸存:
强制选择被丢弃的服务端工具(如 web_search)或指向不存在名字时,选择项被
原样转发,chat 上游因 tool_choice 指向未声明工具而 400。改为具名选择项仅
在目标存在于转换后工具集时转发,服务端工具类选择项随工具本身丢弃;
"auto" 等字符串形式保持原样转发。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWQyEgFKKbdve67G6qCoAU
@xlplbo xlplbo force-pushed the fix/codex-mcp-tools-bridge branch from e48d4bc to e2b68d1 Compare July 10, 2026 13:07
tool_search 工具未被丢弃而是降级为同名 function 代理,此前 tool_choice 过滤
把它连同其他服务端工具类型一起静默丢弃,强制工具搜索被退化为自动选择,
违反调用方明确指定的工具选择语义。改为映射到指向代理的 function 选择;
未声明 type=tool_search 时无可指向的代理,维持丢弃。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWQyEgFKKbdve67G6qCoAU
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.

1 participant