现象
用户报告:用 App Transfer + DeepSeek 接 Codex Desktop,所有 API 200,但 apply_patch 工具调用两次都返回 aborted,前端没有 +/- diff UI。shell_command 正常,fetch 正常。
根因(已对照 openai/codex 上游源码验证,@ commit 000bf5c)
Codex 0.128 把 apply_patch 作为 freeform 工具注册:
codex-rs/protocol/src/openai_models.rs:202-206 — ApplyPatchToolType enum 只有 Freeform 一个变体(无 Function,社区提议 #14046 未合并)
codex-rs/core/src/tools/handlers/apply_patch_spec.rs — 工具 wire 形态是 ToolSpec::Freeform { format: { type:"grammar", syntax:"lark" } },description 明示 "do not wrap the patch in JSON"
codex-rs/core/src/tools/router.rs:92-130 — 响应侧严格按 wire item 类型路由:
ResponseItem::FunctionCall → ToolPayload::Function { arguments }
ResponseItem::CustomToolCall → ToolPayload::Custom { input }
codex-rs/core/src/tools/handlers/apply_patch.rs:324-328 — handler 硬要求 ToolPayload::Custom { input },否则立即返回 "apply_patch handler received unsupported payload" → 这就是用户看到的 aborted
而本仓 adapter 当前行为:
crates/adapters/src/responses/request/tools.rs:55-79 — 把 type:"custom" 工具降级成 type:"function" + {input: string}(为了让 DeepSeek/Kimi/MiMo 等 chat completions provider 能识别),OK
crates/adapters/src/responses/converter.rs:478-645 — DeepSeek 回来的 tool_calls[] 一律渲染成 output_item.type = "function_call",Codex CLI 这边按 router 表立刻 mismatch → abort
二级问题:
tools.rs:71-72 的 input 参数 description 被硬编码成 "Free-form input passed verbatim to the tool.",没有 V4A 格式提示,DeepSeek 不知道要按 *** Begin Patch ... 格式输出
- 即使模型懂格式,patch 文本嵌进 JSON 字符串值会触发大量转义,易破
方案
方案 A(切换 enum 到 Function)——验证后不可行。Codex 当前 enum 没有 Function 变体,声明 apply_patch_tool_type: "function" 会反序列化失败。
方案 B(adapter 双向桥接)——采纳:
- 请求侧
tools.rs:对 name == "apply_patch" 特判,把 input 参数 description 改成包含 V4A 格式简要说明(从 Codex CLI 上游 apply_patch_tool_instructions.md 抽核心),让 DeepSeek 知道 input 字段该填什么
- 响应侧
converter.rs:对 name == "apply_patch" 特判:
- 流式 delta:把
response.function_call_arguments.delta 改成 response.custom_tool_call_input.delta,并提取增量 JSON 中的 input 字段值(可能跨 chunk,需要累积+解析)
- 终结 item:
output_item.type = "custom_tool_call",input 字段挂裸文本 V4A patch
- 降级:如果累积 arguments 不是合法 JSON 或缺 input 字段,把整段 args 当 input(模型可能直接吐裸 V4A 而不包 JSON)
验证计划
- 加单测:request 侧 apply_patch 描述包含 V4A 关键字;response 侧 fixture 输入 chat
tool_calls(name=apply_patch, args={"input":"*** Begin Patch..."}),断言输出是 custom_tool_call 而非 function_call,且 input 字段是 raw V4A 文本
- 真机:本地切到 DeepSeek 配置,起 Codex Desktop,要求模型改一个 markdown / code 文件,确认 +/- diff UI 出现且 apply 成功
关联
现象
用户报告:用 App Transfer + DeepSeek 接 Codex Desktop,所有 API 200,但
apply_patch工具调用两次都返回aborted,前端没有 +/- diff UI。shell_command正常,fetch正常。根因(已对照 openai/codex 上游源码验证,@ commit 000bf5c)
Codex 0.128 把
apply_patch作为 freeform 工具注册:codex-rs/protocol/src/openai_models.rs:202-206—ApplyPatchToolTypeenum 只有Freeform一个变体(无Function,社区提议 #14046 未合并)codex-rs/core/src/tools/handlers/apply_patch_spec.rs— 工具 wire 形态是ToolSpec::Freeform { format: { type:"grammar", syntax:"lark" } },description 明示 "do not wrap the patch in JSON"codex-rs/core/src/tools/router.rs:92-130— 响应侧严格按 wire item 类型路由:ResponseItem::FunctionCall→ToolPayload::Function { arguments }ResponseItem::CustomToolCall→ToolPayload::Custom { input }codex-rs/core/src/tools/handlers/apply_patch.rs:324-328— handler 硬要求ToolPayload::Custom { input },否则立即返回"apply_patch handler received unsupported payload"→ 这就是用户看到的 aborted而本仓 adapter 当前行为:
crates/adapters/src/responses/request/tools.rs:55-79— 把type:"custom"工具降级成type:"function" + {input: string}(为了让 DeepSeek/Kimi/MiMo 等 chat completions provider 能识别),OKcrates/adapters/src/responses/converter.rs:478-645— DeepSeek 回来的tool_calls[]一律渲染成output_item.type = "function_call",Codex CLI 这边按 router 表立刻 mismatch → abort二级问题:
tools.rs:71-72的 input 参数 description 被硬编码成"Free-form input passed verbatim to the tool.",没有 V4A 格式提示,DeepSeek 不知道要按*** Begin Patch ...格式输出方案
方案 A(切换 enum 到 Function)——验证后不可行。Codex 当前 enum 没有 Function 变体,声明
apply_patch_tool_type: "function"会反序列化失败。方案 B(adapter 双向桥接)——采纳:
tools.rs:对name == "apply_patch"特判,把 input 参数 description 改成包含 V4A 格式简要说明(从 Codex CLI 上游apply_patch_tool_instructions.md抽核心),让 DeepSeek 知道 input 字段该填什么converter.rs:对name == "apply_patch"特判:response.function_call_arguments.delta改成response.custom_tool_call_input.delta,并提取增量 JSON 中的 input 字段值(可能跨 chunk,需要累积+解析)output_item.type = "custom_tool_call",input字段挂裸文本 V4A patch验证计划
tool_calls(name=apply_patch, args={"input":"*** Begin Patch..."}),断言输出是custom_tool_call而非function_call,且input字段是 raw V4A 文本关联
shell_command调用的前端 UI 后处理(识别 cat/sed/head 等模式)。不在本 issue 范围,如 DeepSeek 不出卡片再单独看 system prompt 引导。apply_patchbroken for Azure-hosted GPT-4.1 since v0.80.0 (regression from #593 fix) openai/codex#14046(Azure-hosted GPT-4.1 同样栽在 freeform↔chat completions 这个 gap)