fix(apicompat): repair tool_use/tool_result pairing on the Responses→Anthropic path#3012
Open
visa2 wants to merge 1 commit into
Open
fix(apicompat): repair tool_use/tool_result pairing on the Responses→Anthropic path#3012visa2 wants to merge 1 commit into
visa2 wants to merge 1 commit into
Conversation
…Anthropic path
When an OpenAI Chat Completions client targets an Anthropic-platform group,
ForwardAsChatCompletions converts the request CC → Responses → Anthropic
(ChatCompletionsToResponses → ResponsesToAnthropicRequest) before forwarding it
upstream. The Responses→Anthropic converter emits each function_call as its own
assistant message and each function_call_output as its own user message and
relies solely on mergeConsecutiveMessages to alternate roles. That is not enough
to satisfy Anthropic's tool-pairing invariants, so a trimmed or partial tool
history produces an upstream 400, e.g.:
tool_use_id found in tool_result blocks: call_00_...
Each tool_result block must have a corresponding tool_use block in the
previous message.
The failures this leaves unrepaired:
- orphan tool_result — a client that does sliding-window context management
keeps a recent tool result but drops the assistant tool_calls message that
announced it, so the tool_result has no matching tool_use;
- unanswered/dangling tool_use — a parallel call whose sibling result never
came back, or a call left dangling, which Anthropic also rejects.
Add normalizeAnthropicToolPairing, run between two merge passes: the first merge
groups parallel calls and their results; the pairing pass indexes every
tool_result by its tool_use id, keeps only answered tool_use blocks (dropping
unanswered/dangling calls, and the assistant message entirely when nothing else
remains) and re-emits the matching tool_result blocks as the immediately
following user message; standalone/orphan tool_results are dropped from their
original position; the second merge restores alternation. This mirrors
normalizeChatMessages on the Responses→Chat path.
Tested two ways: responses_to_anthropic_tool_pairing_test.go covers the repair
on direct Responses input (developer message between call and output, parallel
both-answered kept grouped, parallel one-unanswered dropped, orphan tool_result,
dangling call, single-call baseline); responses_to_anthropic_cc_chain_test.go
drives the real ChatCompletionsToResponses → ResponsesToAnthropicRequest chain
and reproduces the production 400 (orphan and unanswered-parallel) — both fail
without the repair and pass with it. The full apicompat suite stays green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When an OpenAI Chat Completions client targets an Anthropic-platform group,
ForwardAsChatCompletionsconverts the request CC → Responses → Anthropic (ChatCompletionsToResponses→ResponsesToAnthropicRequest) before forwarding it upstream. The sameResponsesToAnthropicRequestis used by the/v1/responses→ Anthropic path.That converter emits each
function_callas its own assistant message and eachfunction_call_outputas its own user message, relying solely onmergeConsecutiveMessagesto alternate roles. That is not enough to satisfy Anthropic's tool-pairing invariants, so a trimmed or partial tool history produces an upstream 400:Failure classes left unrepaired:
tool_callsmessage that announced it, so thetool_resulthas no matchingtool_use;Approach
Add
normalizeAnthropicToolPairing, run between two merge passes:tool_resultby itstool_useid, keeps only answeredtool_useblocks (dropping unanswered/dangling calls, and the assistant message entirely when nothing else remains), and re-emits the matchingtool_resultblocks as the immediately following user message; standalone/orphantool_results are dropped from their original position;This mirrors
normalizeChatMessageson the Responses→Chat path.Tests
responses_to_anthropic_tool_pairing_test.go— repair on direct Responses input (developer message between call and output, parallel both-answered kept grouped, parallel one-unanswered dropped, orphan tool_result, dangling call, single-call baseline).responses_to_anthropic_cc_chain_test.go— drives the realChatCompletionsToResponses → ResponsesToAnthropicRequestchain and reproduces the production 400 (orphan and unanswered-parallel). Both fail without the repair and pass with it.go build ./..., the fullapicompatsuite, andgolangci-lint run ./...(v2.9) are all green.Note on overlap
This touches
responses_to_anthropic_request.go, which #2916 also modifies (system from instructions/developer, polymorphic arguments/output, tool schema). The changes are orthogonal — #2916 does not add any tool_use/tool_result pairing repair — but depending on merge order one side may need a trivial rebase.