fix(aibridge/intercept/messages): convert enabled thinking to adaptive for Bedrock Opus 4.7+#25335
Merged
dannykopping merged 4 commits intoMay 15, 2026
Conversation
205f0c1 to
87036a4
Compare
…e for Bedrock Opus 4.7+ Claude Opus 4.7 (and future adaptive-only Bedrock models) reject the legacy thinking.type "enabled" + budget_tokens shape with a 400. Claude Code falls back to that shape when it cannot read the upstream model's capability metadata, which is exactly the case when AI Bridge sits between the client and Bedrock. This is the symmetric counterpart to the adaptive -> enabled conversion added in coder/aibridge#225 for older Bedrock models. The new conversion is gated on a bedrockModelRequiresAdaptiveThinking helper that matches Opus 4.7 model IDs (and ARN-style application inference profile names that include the model ID). The effort level is derived from the original budget_tokens / max_tokens ratio using the midpoints of the forward mapping's anchor ratios, so a payload that round-trips through both conversions lands on the same effort level it started with. An explicit output_config.effort already present in the request is preserved. Adaptive-only models support output_config natively (no beta flag), so the field-strip pass is updated to exempt output_config for those models via a new variadic exemptFields parameter on removeUnsupportedBedrockFields. Fixes coder/aibridge#280
…ersions The original enabled -> adaptive conversion derived an output_config.effort label from budget_tokens / max_tokens by snapping to the midpoints of an invented anchor table. Both directions then shared the table, which forced extra machinery (sorted anchors, init-time panic guard, runtime invariant test, round-trip tests) to keep the two halves consistent. The reverse mapping isn't real: there's no canonical relationship between a nominal effort label and a token budget, and the platform's adaptive thinking already has well-defined behavior when no effort hint is provided. Fabricating one papers over information loss with more invented data. Reverse direction now just rewrites the shape: drop budget_tokens, flip the type to "adaptive". An explicit output_config.effort from the caller is preserved because we never touch that field; no effort is fabricated when absent. Forward direction keeps a plain map[string]float64 for the effort -> ratio mapping with a const default ratio, since it has to invent a number (the "enabled" shape requires budget_tokens) and the effort hint is the only signal we have. No more anchor struct, no more init-time panic, no shared state between directions. A header comment flags the whole block as a temporary shim; a planned native Bedrock provider removes the impedance mismatch and lets us delete all of this.
…ock Opus 4.7+ Bedrock's adaptive-only models accept output_config.effort but reject output_config.format (structured outputs) with a 400: output_config.format: Extra inputs are not permitted The generic field-strip pass (removeUnsupportedBedrockFields) operates at top-level granularity and exempts the whole output_config object for these models so that effort can pass through, which lets format slip past too. Add a targeted pass that drops output_config.format after the top-level strip when the model requires adaptive thinking. Scoped narrowly: other Bedrock models either don't get output_config through at all (top-level strip handles them) or only accept it via a beta flag that may imply broader feature support.
…nkingForBedrock comment lint/emdash flagged a U+2014 in the doc comment. Use a comma instead per the project rule.
87036a4 to
dea0f38
Compare
evgeniy-scherbina
approved these changes
May 14, 2026
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
Disclaimer: implemented by a Coder Agent using Claude Opus 4.6/4.7
Fixes coder/aibridge#280.
Claude Opus 4.7 (and future adaptive-only Bedrock models) reject the legacy
thinking.type: "enabled"+budget_tokensshape with a 400. Claude Code falls back to that shape when it cannot read the upstream model's capability metadata, which is exactly the case when AI Bridge sits between the client and Bedrock. Pinning back to Opus 4.6 is the only operator workaround today.This is the counterpart to the
adaptive -> enabledconversion added in coder/aibridge#225 for older Bedrock models.Behavior
bedrockModelRequiresAdaptiveThinking()helper matches Opus 4.7 (coversus.anthropic.claude-opus-4-7, ARN-style application inference profile names that include the model ID, etc.).RequestPayload.convertEnabledThinkingForBedrock()rewritesthinking: {type: enabled, budget_tokens: N}tothinking: {type: adaptive}. The budget hint is dropped; an explicitoutput_config.effortfrom the caller is preserved naturally because we never touch that field. We deliberately do not derive an effort label from the budget (see decision log).removeUnsupportedBedrockFieldslearns a variadicexemptFieldsparameter. Adaptive-only models supportoutput_confignatively (no beta flag required), soaugmentRequestForBedrockexempts that field for those models.output_config.effortbut rejectsoutput_config.format(structured outputs) with the same "Extra inputs are not permitted" 400. The generic strip pass operates at top-level granularity only, so a small targeted pass dropsoutput_config.formatafter the top-level strip for adaptive-only models.The whole Bedrock thinking-type shim block carries a header comment flagging it as temporary; a planned native Bedrock provider removes the impedance mismatch and lets us delete it.
Out of scope
The issue calls out a possible follow-up around
Anthropic-Beta: interleaved-thinking-2025-05-14for adaptive-only models; best evidence is that Opus 4.7 still accepts those flags, so this PR is a no-op there.Decision log
bedrockModelSupportsAdaptiveThinkingnow also returnstruefor adaptive-only models. That keeps the existingconvertAdaptiveThinkingForBedrockbranch from running on Opus 4.7 (which would otherwise be incorrect;adaptiveis the supported native type there), and the newconvertEnabledThinkingForBedrockruns only for adaptive-only models via the explicitbedrockModelRequiresAdaptiveThinkingswitch case. The two model sets are disjoint by construction.output_config.effortfrombudget_tokens / max_tokens. The two thinking shapes encode different intents (enabled+budgetis "give me exactly N tokens,"adaptive[+effort]is "model, pick a budget, optionally biased") and there is no canonical mapping between them. An earlier draft of this PR derived effort via midpoints of an invented anchor table; it was symmetric-looking but lossy and required a lot of scaffolding (sorted anchors, init-time invariant guard, round-trip tests) to keep two halves consistent. The reverse direction now just rewrites the shape, which is honest about the information loss and matches platform-defined adaptive behavior when no effort hint is present.output_config.formatis stripped only for adaptive-only models. Other Bedrock models either don't getoutput_configthrough at all (top-level strip handles them) or accept it via a beta flag that may imply broader feature support. Easy to widen if the same 400 shows up elsewhere.variadic exemptFields ...stringover passing the model down toremoveUnsupportedBedrockFields, to keep that function focused on stripping and to localise the model-aware policy inaugmentRequestForBedrock.Tests
TestRequestPayloadConvertAdaptiveThinkingForBedrock,TestRequestPayloadConvertEnabledThinkingForBedrock): no-op paths, effort-driven forward conversion across the four effort buckets, sub-1024 budget disables thinking, reverse direction drops budget and flips type, explicit effort preservation.TestAugmentRequestForBedrock_AdaptiveThinking): Opus 4.7 enabled→adaptive conversion, adaptive no-op, no-thinking no-op, explicit effort preservation, `output_config` exemption from the beta gate, ARN-style application inference profile names, and `output_config.format` stripped while `effort` is kept.go test ./aibridge/...+CGO_ENABLED=1 go test -race ./aibridge/intercept/messages/...clean.Ready for review.