Repair unpaired tool calls before strict Responses upstreams see them - #2447
JunkaiWang-TheoPhy wants to merge 1 commit into
Conversation
Codex 的 app-server 可能在并行工具批次的最后一个 function_call_output 落盘前发出下一次 sampling 请求(openai/codex#44604),请求体里因此带着没有输出的 function_call。DeepSeek 的 Responses 上游逐个校验配对,直接以 400 `No tool output found for tool call ...` 拒掉整轮。 注入型输入是同一规则的另一面:上游把 standalone 命名 `function_call_output`(automation、 跨线程投递)当外部上下文支持(openai/codex#39782、#39791),但严格上游要求 call_id,而只补 call_id 仍然会被拒——没有配对 function_call 时返回 `No tool call found for tool output with call_id ...`(已做 wire 级复现)。 本补丁在 provider gateway 出口做配对归一化:缺输出的 call 之后插入一条显式失败的占位结果 (不是空成功);孤立的 standalone 输出补出共享同一 call_id 的 function_call 并保留原内容; 配对完整时字节透传,非 api.deepseek.com 的 provider 不受影响。逻辑独立成 provider_gateway_tool_pairing.go,provider_gateway.go 只增加一个调用点。 Constraint: 严格上游按请求校验配对,修复必须发生在请求离开网关之前 Rejected: 只补 call_id | 无配对 function_call 时上游仍以 `No tool call found for tool output` 拒绝 Rejected: 空字符串占位 output | 会让模型误以为工具成功,掩盖真实状态 Confidence: high Scope-risk: narrow Directive: 接入新的严格上游时复用 providerGatewayNormalizeToolCallPairing Tested: go test -count=1 . -run TestProviderGateway;5 个新用例(缺输出注入、standalone 配对、第三方不改写、完整请求字节一致、host 判定) Not-tested: /responses/compact 与 websocket 路径;Kimi/Azure 等其他严格上游
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
补充一份 Windows 真机对照(2026-09-21),针对本 PR 的「output without call → 合成配对」一类: 环境:Windows / Codex 桌面端 26.903.9818.0 / Cockpit Tools v1.3.57(sidecar 网关)/ 严格 Responses 上游。 用与桌面端真实注入一致的形态,直接对 /v1/responses 做 A/B:
旁证:v1.3.57 的 cockpit-cliproxy.exe 内可见顺序修复诊断(provider_gateway_tool_call_outputs_relocated),未见配对修复相关标记。 实际影响:跨窗口消息 / 注入型 standalone 输出(send_message_to_thread、automation 等)在严格上游上仍是「目标窗口被唤醒、但模型看不到正文」。 当前 PR 显示 CONFLICTING,能否在最新 main 上 rebase 后合并?需要完整复现脚本我可以补充。 |
补充:跨窗口消息在 chat_completions 链路下仍会丢正文(Windows 真机 + 实际中转站,v1.3.57/1.3.58)接上面那份对照报告,再补一个部署态的关键变体——它解释了很多用户反馈的「只有部分窗口能发、收了看不到」: 现象
为什么 #2447 的现有实现覆盖不到
我在真机上的最小修复(实测通过,供参考/合并)
真机验证(Windows,Codex Desktop 26.903.9818.0 + Cockpit 1.3.57/1.3.58,第三方中转)
另外附一个容易被误判的小坑:「完全配对就跳过」的判断必须把无 建议把该修复并入 #2447(或独立成 PR)后,随新版本发布——目前 1.3.58 对这类「chat 链路 + 注入消息」的组合仍会丢正文。 (如需要,我可以补一份最小复现脚本与逐字节请求/响应 dump。) |
催办 + 1.3.59 实测:这个问题至今仍会丢跨窗口消息正文(含两条可选修复路径)维护者你好,又打扰了。1.3.59 发布后我们在真机上重新验证了一遍,结论如下,供决策: 1.3.59 现状(真机实测)
请在下列两条路径中任选其一(都行,我们只希望默认可用)
真机验证数据(供参考)
复现极简:向 如果需要,我可以把在 1.3.59 上实测通过的最小补丁(约 240 行、带单测思路)整理成 PR 直接提交。谢谢! |
Part of #2433 (split out of #2434 for reviewability).
Problem
Codex's app-server can serialize the next sampling request before the last
function_call_outputof a parallel tool batch reaches conversation history (openai/codex#44604). The request then carries afunction_callwith no output, and DeepSeek rejects the whole turn:The same rule has a second face: injected inputs (automations,
send_message_to_thread,create_thread) arrive as a standalone namedfunction_call_output— a shape upstream supports deliberately (openai/codex#39782, #39791) but strict providers reject for a different reason:400 input: missing field 'call_id'. And adding only acall_iddoes not fix it — with no matchingfunction_callthe upstream answersNo tool call found for tool output with call_id ...(reproduced at the wire level on macOS againsthttps://api.deepseek.com).Change
One repair pass on the provider gateway's request path, scoped to
api.deepseek.com:tool result unavailable: the local agent did not commit an output for this call). A placeholder rather than an empty success, so the model can decide whether to retry.function_callthe injected output never had (reusingname/namespace, sharing onecall_id) and keep the original content instead of dropping it.The logic lives in a new
provider_gateway_tool_pairing.go;provider_gateway.gogains only the call site plus a diagnostic event (provider_gateway_tool_call_pairing_repaired).Verification
go test -count=1 . -run TestProviderGateway— 5 new cases: missing output injected, standalone output paired, other providers byte-identical, complete requests byte-identical, host predicate.Not covered
/responses/compactand websocket paths; other strict upstreams (Kimi, Azure) are not verified — the predicate stays scoped toapi.deepseek.comto keep the blast radius small.Note
This PR is independent of #2448 and will be independent of the reasoning-text fix once that PR is opened. They share no functions.