TL;DR — Anthropic's tool-call contract is strict: every tool_use block needs exactly one matching tool_result with the same id, in the next user turn. Break the pairing and you get a 400 on the step after the tool call, never on the call itself. Through a proxy this is almost always the translation layer — converting to another provider's tool format and back loses ids, duplicates results, or reorders blocks. Find the failing turn by logging the outbound body, not the error.
The errors
Several messages, one underlying cause:
400 — multiple tool_result blocks for a single tool_use id
400 — Invalid 'input': value did not match any expected variant
400 — tool_use ids were found without tool_result blocks immediately after
All share the same signature: the first tool call succeeds, the next request fails. Nothing in your code changed between them — the conversation state did. That is the same shape as the DeepSeek reasoning_content 400, and for the same underlying reason.
The contract
Anthropic requires, on the turn after a tool call:
- every
tool_useblock has exactly onetool_resultwith a matchingtool_use_id - those results come immediately in the following user message
- no orphans in either direction — no result without a call, no call without a result
Strict, and deliberately so: the model has to be able to reconstruct what happened. Which is also why a lossy round-trip through another format breaks it.
Why proxies break it
Converting Anthropic tool calls into OpenAI's tool_calls shape and back is not lossless:
- Ids are regenerated. OpenAI's
tool_call_idand Anthropic'stool_use_idare different namespaces; a translation that mints a new id on the way back orphans the result. - Parallel tool calls get flattened. Several
tool_useblocks in one turn become a sequence, and results come back merged or duplicated against one id. - Blocks get reordered so results no longer sit immediately after their calls.
- Newer block types are not understood. A client feature the proxy does not model yet — deferred tool loading and tool references are the current example — produces a result the validator cannot classify, which surfaces as
value did not match any expected variant. Tracked upstream: LiteLLM #39741, #16711.
Finding it
The error names a symptom, not a turn. Log the outbound request body on failure and check, in order:
- Count
tool_useids in the assistant turn, counttool_resultids in the next user turn — they must match exactly, no extras, no misses. - Compare the id strings, not the counts. Regenerated ids give you equal counts and zero matches.
- Confirm results sit in the immediately following message.
- Look for two results carrying the same id — the parallel-call flattening case.
uses = {b["id"] for b in assistant["content"] if b.get("type") == "tool_use"}
results = [b["tool_use_id"] for b in following["content"] if b.get("type") == "tool_result"]
assert uses == set(results), f"orphans: {uses ^ set(results)}"
assert len(results) == len(set(results)), "duplicate tool_result ids"
Run that assertion in your proxy before forwarding and the bug reports itself at the point of corruption instead of as a 400 two layers away.
Fixes
- Preserve ids verbatim across translation. Map between namespaces if you must, but keep the mapping — never mint fresh ids on the return path.
- Do not flatten parallel tool calls. If the upstream cannot express them, fail loudly rather than silently merging.
- Never truncate history between a
tool_useand itstool_result. This is the most common self-inflicted version: a context-trimming step drops the assistant turn and orphans the result. Compact whole turns — the same rule as in prompt is too long. - Do not retry. The history is malformed; it will be malformed next time too.
Prevention
- Assert the pairing in CI with a recorded multi-tool conversation. This class of bug appears on the second step, so a single-turn smoke test never catches it.
- Alert on 400s that occur specifically after a tool call — that ratio is the health metric for a translation layer, and it is invisible in an overall error rate.
Part of the LLM API Error Reference — errors indexed by their exact strings.