fix(executors): don't inject thinking when tool_choice forces a tool (native Claude) - #4389
Conversation
There was a problem hiding this comment.
Code Review
This pull request implements a guard to prevent Anthropic API errors by treating forced tool choices as an implicit 'thinking: off', and includes unit tests to verify this behavior. The review feedback is valid and important, noting that 'tb.output_config' must also be deleted when thinking is disabled to prevent 400 Bad Request errors from Anthropic.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
commented
Jun 20, 2026
|
Thanks for the review — good catch on Also corrected the |
…(native Claude)
The Claude Code wire-image emulation in base.ts injects thinking:{type:"adaptive"}
for non-Haiku Claude models. Anthropic rejects thinking (enabled/adaptive) when
tool_choice forces a specific tool ({type:"any"|"tool"}) with a hard 400
"Thinking may not be enabled when tool_choice forces tool use". So Opus/Sonnet
calls that pin a tool (e.g. Claude Code's message_user) failed.
Treat forced tool_choice as an implicit thinking: off (effThinking) so thinking
is stripped only when forced and the adaptive injection is preserved otherwise.
Also covers client-sent thinking + forced tool_choice. Haiku unaffected.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
…hinking guard (diegosouzapw#4389) Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
5ad1551 to
05b095b
Compare
commented
Jun 20, 2026
|
Merged into |
What
Don't inject
thinkingon native Claude OAuth whentool_choiceforces a tool.Why
The Claude Code wire-image emulation in
open-sse/executors/base.tsinjectsthinking:{type:"adaptive"}for non-Haiku Claude models. But Anthropic rejectsthinking(enabled/adaptive) whentool_choiceforces a specific tool ({type:"any"|"tool"}):So any Opus/Sonnet call that pins a tool — e.g. Claude Code's
message_user, or agent harnesses that force a tool — gets a hard 400. There's already adisableThinkingIfToolChoiceForcedhelper (from the v3.8.3 wire-image work), but it runs earlier in the pipeline and is undone by this later injection.How
toolChoiceForced(tool_choiceis"any"or{type:"any"|"tool"}) and deriveeffThinking = toolChoiceForced ? "off" : headerThinking.effThinkingin the three thinking branches, so forced tool_choice takes the existingoffpath (stripsthinking+context_management) and skips the default adaptive injection. Thinking is preserved for normal calls; this also covers client-sentthinking+ forced tool_choice; Haiku is unaffected.tests/unit/claude-thinking-tool-choice-guard.test.ts(forced → nothinking; non-forced → adaptivethinkingpreserved), using the same executor harness as the existing#4307test.Notes
Observed live: Opus 4.8 / Sonnet 4.6 + forced
tool_choicewent 400 → 200 after this, while normal calls keepthinking:{type:adaptive}. Happy to revise if you'd prefer a different approach (e.g. folding this intodisableThinkingIfToolChoiceForcedat a single chokepoint).