Skip to content

fix(vendor-channels): 修复 zhipu→anthropic 通道 tool_use/tool_result 配对漏洞#236

Merged
ThreeFish-AI merged 2 commits into
feature/1.x.xfrom
ThreeFish-AI/fix-tool-use-result-pairing
May 11, 2026
Merged

fix(vendor-channels): 修复 zhipu→anthropic 通道 tool_use/tool_result 配对漏洞#236
ThreeFish-AI merged 2 commits into
feature/1.x.xfrom
ThreeFish-AI/fix-tool-use-result-pairing

Conversation

@ThreeFish-AI
Copy link
Copy Markdown
Owner

背景

zhipu → anthropic 通道流式请求偶发 400:

messages.3: `tool_use` ids were found without `tool_result` blocks immediately after: toolu_normalized_2.

同一请求带 rewritten_N_srvtoolu_ids + misplaced_tool_result_relocated没有 orphaned_tool_use_repaired——转换层主观认为已配对、Anthropic 仍判定结构不合规。Failover 至 zhipu 后请求成功,证明上游消息体没坏,问题出在跨供应商转换过程引入了不一致。

根因

_rewrite_srvtoolu_ids 旧实现是单遍循环同时承担两件事:Case A 改写 assistant 端的 server_tool_use + srvtoolu_* ID、Case B 同步改写任意位置 tool_result.tool_use_id。Case B 依赖 id_map 已被 Case A 填入。

GLM-5 流式响应偶发将 inline tool_result 输出在对应 server_tool_use 之前(同 assistant content 内乱序),或将 tool_result 放在更早的 user 消息而对应 tool_use 在更晚的 assistant 消息。两种乱序下单遍扫描遍历到 tool_resultid_map 还是空 → tool_use_id 漏改、停留在 srvtoolu_*。随后 enforce_anthropic_tool_pairing 以旧 ID 作 extracted_tool_results 的 key、用新 ID 去查不命中,misplaced tool_result 被默默丢弃,最终 Anthropic 400。

修复

  1. _rewrite_srvtoolu_ids 改两遍扫描:Pass 1 仅遍历 assistant 收集 id_map(按出现顺序分配,保持序号兼容);Pass 2 全量遍历改写任意 tool_result.tool_use_id。以"先建表、后改写"次序消除时序耦合。
  2. 新增 _enforce_pairing_sanity_pass 独立 helper:作为 enforce_anthropic_tool_pairing 主循环末尾的纵深防御层,仅做检测 + 合成 is_error=True 占位(不剥离、不重定位),命中追加 pairing_sanity_repaired 并 WARNING。在主循环未来重构时仍能稳定守住 Anthropic 配对约束。
  3. 回归测试覆盖三类场景:同 assistant content 内乱序、跨消息边界 tool_result 早于 tool_use、端到端复现日志故障形态;并新增 TestEnforcePairingSanityPass 独立测试套件确保兜底分支具备正向回归保护。

实现要点

  • Pass 1 / Pass 2 注释中显式标注次序意图与单遍漏改的失败模式,规避未来"无意优化合并"重新引入 bug。
  • Sanity helper 单独抽出而非内嵌主路径:避免主路径快速通道让 sanity 分支永远走不到正向测试(历史教训:9061cd0 曾实现两遍扫描+sanity,但 2bac9a7 因缺乏对兜底路径的独立单测被连带回滚)。
  • adaptations 标签 pairing_sanity_repaired 与主循环 orphaned_tool_use_repaired 分离,便于运维按层归因。
  • 同步在 docs/issue.md 记录根因、处理方式、后续防范与同类问题回滚红线(grep _enforce_pairing_sanity_pass / Pass 1 / Pass 2)。

验证

  • uv run pytest tests/test_vendor_channels.py:84 passed
  • 新增 10 个用例覆盖两遍扫描、sanity 兜底与端到端配对不变量

🤖 Generated with Claude Code, CodeX, Gemini
Co-Authored-By: Aurelius Huangthreefish.ai@gmail.com

生产日志暴露 Anthropic 400 错误:channel transition 已应用 `rewritten_N_srvtoolu_ids`
+ `misplaced_tool_result_relocated` + `stripped_M_thinking_blocks` 但**未出现**
`orphaned_tool_use_repaired` 标签,Anthropic 仍报 `messages.x: tool_use ids were
found without tool_result blocks immediately after`。

根因: `_rewrite_srvtoolu_ids` 单遍循环中 Case A (assistant 端 `server_tool_use` →
`tool_use` ID 改写) 与 Case B (任意 `tool_result.tool_use_id` 同步改写) 共用一遍
for 循环,Case B 依赖 `id_map` 已被 Case A 填入。GLM-5 偶发将 inline tool_result
块输出在对应 server_tool_use **之前** (同 assistant content 内乱序) 或在更早的
user 消息中, 单遍遍历到 tool_result 时 id_map 还是空 → 漏改; 后续 enforce 主循环
extracted_tool_results 以旧 ID 作 key、tool_use_ids 是新 ID, 错位致默默丢弃 →
对应 user 槽位实际仍然缺位。

修复:
1. `_rewrite_srvtoolu_ids` 改为两遍扫描: Pass 1 仅扫 assistant 收集 id_map (序号
   分配次序与单遍一致, 保持已有断言), Pass 2 全量改写 tool_result.tool_use_id, 消
   除时序耦合。
2. 新增独立 helper `_enforce_pairing_sanity_pass` 作为 enforce 主循环末尾的纵深防
   御, 仅检测+合成 is_error 占位, 不剥离/不重定位; 命中追加 `pairing_sanity_repaired`
   adaptation 与 WARNING 日志, 便于运维定位。
3. 新增 11 个测试: 2 个两遍扫描乱序回归 + 8 个 sanity helper 单测覆盖各边界 +
   1 个 prepare_zhipu_to_anthropic 端到端复现日志故障形态。
4. `docs/issue.md` 沉淀故障档案与历史教训 (commit 9061cd0 引入该修复曾被 2bac9a7
   作为副产物连带回滚, 提醒后续 revert vendor_channels 时不要连带回滚 sanity
   helper 与两遍扫描)。

测试: tests/ 全量 1438 通过, 零回归。

🤖 Generated with [Claude Code](https://github.com/claude), [CodeX](https://openai.com), [Gemini](https://github.com/apps/gemini-code-assist)
Co-Authored-By: Aurelius Huang<threefish.ai@gmail.com>
…/fix-tool-use-result-pairing

# Conflicts:
#	docs/issue.md
@ThreeFish-AI ThreeFish-AI merged commit 4d295bb into feature/1.x.x May 11, 2026
6 checks passed
@ThreeFish-AI ThreeFish-AI deleted the ThreeFish-AI/fix-tool-use-result-pairing branch May 12, 2026 13:24
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