Repository navigation
fix(protocol): carry tool_choice across every cross-protocol request translation - #209
Merged
Merged
Conversation
…translation map_tool_choice() was called from anthropic_to_openai_chat_req only, so every other cross-protocol request translation dropped the field: responses->chat, responses->anthropic, chat->anthropic, chat->responses and anthropic->responses all lost a client's tool_choice (measured through the real translators). A request that requires a tool call, or names a function, therefore reached the upstream with no tool preference at all - a model may answer in prose and an agent client waits forever for a tool call it never receives. It is not a deliberate lossy mapping: map_tool_choice exists precisely for this field, and the omission stayed invisible because none of the six tool_choice references in the tree is a test. The shape knowledge now has a single home: a protocol-agnostic ToolChoice IR with parse_tool_choice(from, raw) / render_tool_choice(to, &tc), plus copy_tool_choice() as the shared tail of every request translator. All four cross directions call it (chat->anthropic, chat->responses, responses->chat, anthropic->chat; map_tool_choice is deleted - it had no other caller), and the two chained directions (responses->anthropic = responses->chat . chat->anthropic; anthropic->responses = anthropic->chat . chat->responses) are fixed by the same rings. A shape the source protocol cannot mean is left absent rather than copied verbatim, so a value the target protocol does not accept is never invented and "absent" stays distinguishable from an explicit null. +3 tests (202 -> 205 passed): a per-direction matrix with each expected value written out literally, the two chained directions asserted at the composition entry (transform_request), and a negative control for the absent/unrecognised shapes. fmt + clippy clean.
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.
Summary
tool_choicewas carried across protocols by exactly one direction.map_tool_choice()was called only from
anthropic_to_openai_chat_req, so every other cross-protocol requesttranslation silently dropped the field:
responses -> chat,responses -> anthropic,chat -> anthropic,chat -> responsesandanthropic -> responsesall lost it. Measuredthrough the real translators: a request that asks for
tool_choice: "required", or names afunction, arrives upstream with no tool preference at all — so a model may answer in prose and
an agent client waits forever for a tool call it never gets.
This is not a lossy mapping by intent:
map_tool_choiceexists for this field, which isproof the author wants it translated; the other directions simply have no counterpart. The
omission was invisible because
grep -rn tool_choice src/returns six lines and none of themis a test.
Reachability (real
determine_forwarding_protocol, all 12 shipped[[plans]]): every plan hasan
openai_chatendpoint, so a chat inbound is always a passthrough — but a responsesinbound is converted on 11 of 12 plans (7 through the
responses -> chat -> anthropicchain, 4 through
responses -> chat).Related Issue
No issue exists for this; the finding came from a routine sweep of the request-side
translation. Nothing is fabricated or linked.
Changes
src/protocol.rsonly: the shape knowledge fortool_choicenow has a single home.ToolChoice(
None/Auto/Required/Function(name)) withparse_tool_choice(from, raw)andrender_tool_choice(to, &tc), pluscopy_tool_choice(body, from, to, result)— the sharedtail every request translator now calls.
chat -> anthropic,chat -> responses,responses -> chat, andanthropic -> chat(which previously usedmap_tool_choice;map_tool_choiceis now deleted — it had no other caller).responses -> anthropic=responses -> chat∘chat -> anthropic;anthropic -> responses=anthropic -> chat∘chat -> responses)are fixed by the same rings.
"none"|"auto"|"required"/{"type":"function","function":{"name":X}};anthropic
{"type":"none"|"auto"|"any"}/{"type":"tool","name":X};responses
"none"|"auto"|"required"/{"type":"function","name":X}.{"type":"allowed_tools"}, an unknownscalar, a named tool with no
name) is left absent instead of being copied verbatim —the old
_ => clone()arm could forward a value the target protocol does not accept, andomitting the key keeps "absent" distinguishable from an explicit
null.ui/, no i18n keys)Tests
cargo test全部通过 — 205 passed (was 202)cargo fmt --check通过Added three tests:
tool_choice_survives_every_cross_protocol_direction— per direction, three scalars plus onenamed tool, each expected value written out literally (not derived from the implementation),
so a missing ring cannot be masked by a sibling.
tool_choice_survives_the_two_chained_directions— the two chains asserted at the compositionentry point (
transform_request), because a chain's benefit is a composition and is notvisible from the individual functions.
tool_choice_is_not_invented— negative control: notool_choicein the source ⇒ no key inthe target; unrecognised shape ⇒ absent, never fabricated.
Teeth proof (each mutation applied alone, its own derived expected red set, restore in
finallywith an mtime refresh): baselinepassed=205 red=[]; removing any one of the fourcall sites ⇒
passed=203with exactly the two tests above red, and the failure output is thedefect (
assertion left == right failed: responses->chat 源 "auto"; left: Null, right: String("auto")). An unrecognised shape is asserted by construction, not by omission.Checklist
fix/request-tool-choice-cross-protocol)fix(protocol): ...)