fix(anthropic): move tool_result blocks first in mixed-content user messages - #8165
Conversation
…essages Fixes QwenLM#8161 When a Gemini Content for a user turn contains both a functionResponse and other parts (e.g. accompanying text), the converter emitted the resulting content blocks in whatever order the source parts happened to arrive in -- it never guaranteed tool_result blocks come first. If a text part preceded the functionResponse part within the same Content, the resulting Anthropic user message put text before tool_result. Live-verified against the real Anthropic Messages API: sending a user message shaped [{type:'text', ...}, {type:'tool_result', ...}] right after an assistant tool_use produces HTTP 400: "`tool_use` ids were found without `tool_result` blocks immediately after: <id>. Each `tool_use` block must have a corresponding `tool_result` block in the next message." Anthropic doesn't scan past a leading non-tool_result block to find the result later in the same message -- it must be first. Worse: cleanOrphanedToolCalls's own "seenNonToolResult" gate (a separate, pre-existing defensive check against exactly this ordering rule) reacts to the misordering by treating the tool_result as if it were never found at all, silently discarding BOTH the tool_result and its paired tool_use rather than fixing the order. So the previous behavior wasn't even a consistent 400 -- it was silent data loss of the tool call and its result. Fix: when a user-role message contains any tool_result blocks, a stable sort moves them ahead of any other content in that message, preserving the relative order of multiple tool_result blocks against each other. This runs before cleanOrphanedToolCalls, so its ordering gate now recognizes the pairing correctly instead of discarding it. Verification: - Rewrote the existing test that pinned the silent-discard behavior ("drops tool results that do not lead user content") to assert the correct reorder-and-preserve behavior instead. - Added a new test for relative-order preservation across multiple tool_result blocks being reordered together. - Full anthropicContentGenerator/ suite: 193 tests pass (was 192; net +1 test). - Live proxy verification: the previously-400ing wire shape now returns HTTP 200 with the reordered body. - tsc --noEmit -p packages/core/tsconfig.json and eslint clean for touched files.
|
✅ Qwen Triage finished — CI landed green on ✅ Qwen Triage 已完成 —— |
|
Thanks for the PR! Template: headings differ slightly from the template ( Problem: observed bug with solid evidence. Issue #8161 includes a live API reproduction — HTTP 400 from the real Anthropic Messages API with the exact error message. The issue was triaged and confirmed in source. The interaction with Direction: aligned. This is a straightforward ordering bug in the Anthropic converter — the converter emits blocks in Gemini part-iteration order, but the Anthropic API requires Size: 18 production lines ( Approach: the scope feels right — a stable sort in Risk: no elevated risk signals. Moving on to code review. 🔍 中文说明感谢贡献! 模板:标题与模板略有不同( 问题:已观测到的 bug,证据充分。Issue #8161 包含真实 API 复现——Anthropic Messages API 返回 HTTP 400 及确切错误信息。Issue 已经过 triage 并在源码中确认。与 方向:对齐。这是 Anthropic 转换器中的排序 bug——转换器按 Gemini part 迭代顺序输出块,但 Anthropic API 要求 规模:18 行生产代码( 方案:范围合理——在 风险:无升级风险信号。 进入代码审查 🔍 — Qwen Code · qwen3.8-max-preview Reviewed at |
Code ReviewIndependent proposal: given the problem (Anthropic requires Comparison with the diff: the PR does exactly this. The implementation is clean and correct:
Tests: the rewritten test ( No correctness bugs, security holes, regressions, or convention violations found. TestingFinal CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 CI is still running — the ubuntu unit test suite is in progress. macOS/Windows tests and integration tests are skipped pending fork-PR approval. The Sandboxed verification would settle the remaining gap: 中文说明代码审查独立方案: 给定问题(Anthropic 要求 与 diff 的比较: PR 完全这样做了。实现干净且正确:
测试: 重写的测试现在断言完整的消息序列(包括前面的 user 和 assistant 消息),比旧的断言更强。新的多 未发现正确性 bug、安全漏洞、回归或规范违反。 测试CI 仍在运行——ubuntu 单元测试套件进行中。macOS/Windows 测试和集成测试因 fork PR 需要审批而跳过。 沙盒验证可以解决剩余差距: — Qwen Code · qwen3.8-max-preview Reviewed at |
|
Confidence: 4/5 — clean, minimal fix that exactly matches the independent proposal; good tests; only reservation is CI hasn't settled yet. This is a textbook small bug fix. The problem is real (live-verified HTTP 400 from the Anthropic API), the root cause is correctly identified (part-iteration order in The PR carries no unrelated changes, no over-abstraction, no scope creep. If I had to maintain this in six months, the comment block and the test names would tell me exactly why the sort is there. Approval deferred until CI lands green on 中文说明置信度:4/5 —— 干净、最小化的修复,与独立方案完全吻合;测试良好;唯一的保留是 CI 尚未完成。 这是一个教科书式的小型 bug 修复。问题是真实的(Anthropic API 返回 HTTP 400 的实际验证),根因正确识别( PR 没有无关改动、没有过度抽象、没有范围蔓延。如果六个月后维护这段代码,注释块和测试名称会准确告诉我排序存在的原因。 批准推迟至 CI 在 — Qwen Code · qwen3.8-max-preview Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
— qwen3.8-max-preview via Qwen Code /review
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship — CI landed green after the review. ✅
Local verification report — PR #8165I verified this locally against real Anthropic-protocol servers rather than only reading the diff. Verdict: confirmed, recommend merge. Summary of what I found, for merge reference. Base for every A/B below: PR head 1. The mis-ordered shape is produced by qwen-code itself, not just by hand-built historyThe PR calls the "text part before Runtime-confirmed rather than only read: I drove the real The core TUI / non-interactive scheduler is not affected: 2. Real end-to-end A/B — production converter, real models, real networkHarness: the production
Reproduced identically on two real providers — This is the part that matters most for merge: on 3. Why the ordering requirement is real (probe sent straight to the server, qwen-code not involved)
That is on a strict server ( 4. Negative control on the PR's own testsReverting only
Restore → 73/73, and 193/193 for the whole 5. Randomized invariant + completeness of the fix3 000 synthetic histories with shuffled part order (7 489 tool calls), asserting "in every emitted user message, no
Coverage is complete rather than incidental: only three places in the converter build user-message content — 6. Scope claim checkedThe PR states the OpenAI/Chat path is unaffected. Confirmed by probe: the same Gemini history through 7. Gates
Non-blocking notes (not merge blockers)
中文说明PR #8165 本地验证报告我在本地针对真实的 Anthropic 协议服务端做了验证,而不是只读 diff。结论:问题与修复均已确认,建议合并。 以下是验证结果,供合并参考。 下文所有 A/B 的基准:PR head 1. 这种乱序形态是 qwen-code 自己产出的,并非只能靠手工构造历史PR 把「text part 排在 这不是只读代码得出的推断,而是运行时确认:我用仓库自带的 ACP 测试装置驱动了真实的 core 的 TUI / 非交互调度器不受影响: 2. 真实端到端 A/B——生产环境的转换器、真实模型、真实网络装置:从构建产物加载生产环境的
在两个真实 provider 上结果一致—— 这一点对是否合并最关键:在 3. 排序要求确实存在(直接打给服务端的探针,不经过 qwen-code)
这是在严格的服务端上( 4. 对 PR 自带测试做反向对照只把
恢复后 73/73,整个 5. 随机化不变量检查 + 修复的完备性3 000 个随机 part 顺序的合成历史(7 489 次工具调用),断言*「在每一条输出的 user 消息中,
修复的覆盖是完备的、而非碰巧:转换器中只有三处构造 user 消息内容—— 6. 影响范围声明已核实PR 声称 OpenAI/Chat 路径不受影响。已用探针确认:同样的 Gemini 历史经 7. 各项门禁
非阻塞备注(都不是合并阻塞项)
🤖 Claude Code · Claude Opus 5 |
|
Released in v0.21.3. |



What this PR does
Moves
tool_resultblocks ahead of any other content within a user-role message in the Anthropic converter, whenever atool_resultis present in that message.Why it's needed
When a Gemini
Contentfor a user turn contains both afunctionResponseand other parts (e.g. accompanying text), the converter emitted the resulting content blocks in whatever order the source parts happened to arrive in — it never guaranteedtool_resultblocks come first. If a text part preceded thefunctionResponsepart within the sameContent, the resulting Anthropic user message put text beforetool_result.Live-verified against the real Anthropic Messages API: sending a user message shaped
[{type:'text', ...}, {type:'tool_result', ...}]right after an assistanttool_useproduces HTTP 400:`tool_use` ids were found without `tool_result` blocks immediately after: <id>. Each `tool_use` block must have a corresponding `tool_result` block in the next message.Anthropic doesn't scan past a leading non-tool_resultblock to find the result later in the same message — it must be first.Worse:
cleanOrphanedToolCalls's ownseenNonToolResultgate (a separate, pre-existing defensive check against exactly this ordering rule) reacts to the misordering by treating thetool_resultas if it were never found at all — silently discarding both thetool_resultand its pairedtool_userather than fixing the order. The previous behavior wasn't even a consistent 400, it was silent data loss of the tool call and its result — confirmed by the existing test this PR rewrites, which was nameddrops tool results that do not lead user contentand pinned exactly that discard as expected.Reviewer Test Plan
How to verify
npx vitest run packages/core/src/core/anthropicContentGenerator/converter.test.ts— see the rewrittenreorders a tool_result ahead of other content in the same message rather than dropping ittest (wasdrops tool results that do not lead user content, now asserts reorder-and-preserve instead of silent discard) and the newpreserves relative order among multiple tool_result blocks when reordering ahead of texttest.functionResponseGeminiContent, confirmed the resulting message reorders to[tool_result, text], and sent that body to the real API — HTTP 200 (previously HTTP 400 for the unreordered shape, confirmed separately).Evidence (Before & After)
N/A — no UI surface; behavior change is in the outbound Anthropic request body. See the HTTP 400 → 200 transcript above and the rewritten test's before/after message shapes.
Tested on
Environment (optional)
Unit tests only (
vitest); live verification via a real HTTPS call to Anthropic's Messages API through a corporate LiteLLM proxy in front of Vertex AI (no local sandbox/Docker involved).Risk & Scope
tool_result, which changes the wire block order for that message.Array.prototype.sortis spec-guaranteed stable since ES2019, so relative order among blocks of the same type is preserved.Linked Issues
Fixes #8161
中文说明
本 PR 做了什么
在 Anthropic 转换器中,只要某条 user 角色消息里出现了
tool_result,就把tool_result块移到该消息其他内容之前。为什么需要这个改动
当某个 user 轮次的 Gemini
Content同时包含functionResponse和其他 part(例如附带的文本)时,转换器会按源 part 到达的原始顺序输出对应的内容块——从不保证tool_result块排在最前。如果文本 part 在同一个Content中出现在functionResponsepart 之前,最终生成的 Anthropic user 消息就会把文本排在tool_result之前。已针对真实 Anthropic Messages API 做了 live 验证:在一个 assistant
tool_use之后,发送形如[{type:'text', ...}, {type:'tool_result', ...}]的 user 消息会产生 HTTP 400:`tool_use` ids were found without `tool_result` blocks immediately after: <id>. Each `tool_use` block must have a corresponding `tool_result` block in the next message.。Anthropic 不会跳过一个排在最前的非tool_result块去寻找同一消息里靠后的结果——它必须排在最前。更糟的是:
cleanOrphanedToolCalls自身的seenNonToolResult门控(一个针对该排序规则的、独立存在的既有防御检查)在遇到顺序错误时,会把该tool_result当作根本没有出现过,从而静默地把该tool_result及其配对的tool_use一并丢弃,而不是修正顺序。此前的行为甚至连稳定的 400 都算不上,而是工具调用及其结果的静默数据丢失——本 PR 重写的既有测试(原名drops tool results that do not lead user content)恰好把这种丢弃行为断言为"预期",印证了这一点。Reviewer 测试计划
如何验证
npx vitest run packages/core/src/core/anthropicContentGenerator/converter.test.ts——查看重写后的测试reorders a tool_result ahead of other content in the same message rather than dropping it(原名drops tool results that do not lead user content,现在断言的是重排并保留,而非静默丢弃),以及新增的preserves relative order among multiple tool_result blocks when reordering ahead of text测试。functionResponse"的 GeminiContent驱动真实转换器,确认生成的消息被重排为[tool_result, text],并将该请求体发往真实 API——HTTP 200(此前对未重排的形态单独确认过是 HTTP 400)。证据(前后对比)
不适用——没有 UI 界面;行为变化体现在发往 Anthropic 的出站请求体中。见上方 HTTP 400 → 200 的记录,以及重写测试中的前后消息形态。
测试环境
运行环境(可选)
仅单元测试(
vitest);live 验证是通过企业 LiteLLM 代理(Vertex AI 后端)对 Anthropic Messages API 发起的真实 HTTPS 调用(未涉及本地沙箱/Docker)。风险与范围
tool_result的 user 消息都会重新排序(而非仅过滤)内容块,改变该消息在 wire 上的块顺序。Array.prototype.sort自 ES2019 起规范保证稳定,因此同类型块之间的相对顺序会被保留。关联 Issue
Fixes #8161