Skip to content

fix(apply_patch): non-GPT provider 走 chat-completions 时 diff UI 失败 / aborted #235

@Cmochance

Description

@Cmochance

现象

用户报告:用 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-206ApplyPatchToolType 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::FunctionCallToolPayload::Function { arguments }
    • ResponseItem::CustomToolCallToolPayload::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 双向桥接)——采纳:

  1. 请求侧 tools.rs:对 name == "apply_patch" 特判,把 input 参数 description 改成包含 V4A 格式简要说明(从 Codex CLI 上游 apply_patch_tool_instructions.md 抽核心),让 DeepSeek 知道 input 字段该填什么
  2. 响应侧 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)

验证计划

  1. 加单测:request 侧 apply_patch 描述包含 V4A 关键字;response 侧 fixture 输入 chat tool_calls(name=apply_patch, args={"input":"*** Begin Patch..."}),断言输出是 custom_tool_call 而非 function_call,且 input 字段是 raw V4A 文本
  2. 真机:本地切到 DeepSeek 配置,起 Codex Desktop,要求模型改一个 markdown / code 文件,确认 +/- diff UI 出现且 apply 成功

关联

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions