Skip to content

fix(aibridge/intercept/messages): convert enabled thinking to adaptive for Bedrock Opus 4.7+#25335

Merged
dannykopping merged 4 commits into
mainfrom
dk/aibridge-bedrock-opus-4-7-adaptive-only-thinking
May 15, 2026
Merged

fix(aibridge/intercept/messages): convert enabled thinking to adaptive for Bedrock Opus 4.7+#25335
dannykopping merged 4 commits into
mainfrom
dk/aibridge-bedrock-opus-4-7-adaptive-only-thinking

Conversation

@dannykopping
Copy link
Copy Markdown
Contributor

@dannykopping dannykopping commented May 14, 2026

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_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. Pinning back to Opus 4.6 is the only operator workaround today.

This is the counterpart to the adaptive -> enabled conversion added in coder/aibridge#225 for older Bedrock models.

Behavior

  • New bedrockModelRequiresAdaptiveThinking() helper matches Opus 4.7 (covers us.anthropic.claude-opus-4-7, ARN-style application inference profile names that include the model ID, etc.).
  • New RequestPayload.convertEnabledThinkingForBedrock() rewrites thinking: {type: enabled, budget_tokens: N} to thinking: {type: adaptive}. The budget hint is dropped; an explicit output_config.effort from 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).
  • removeUnsupportedBedrockFields learns a variadic exemptFields parameter. Adaptive-only models support output_config natively (no beta flag required), so augmentRequestForBedrock exempts that field for those models.
  • Bedrock Opus 4.7 accepts output_config.effort but rejects output_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 drops output_config.format after 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-14 for adaptive-only models; best evidence is that Opus 4.7 still accepts those flags, so this PR is a no-op there.

Decision log
  • bedrockModelSupportsAdaptiveThinking now also returns true for adaptive-only models. That keeps the existing convertAdaptiveThinkingForBedrock branch from running on Opus 4.7 (which would otherwise be incorrect; adaptive is the supported native type there), and the new convertEnabledThinkingForBedrock runs only for adaptive-only models via the explicit bedrockModelRequiresAdaptiveThinking switch case. The two model sets are disjoint by construction.
  • The reverse conversion does not derive output_config.effort from budget_tokens / max_tokens. The two thinking shapes encode different intents (enabled+budget is "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.format is stripped only for adaptive-only models. Other Bedrock models either don't get output_config through 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.
  • I chose variadic exemptFields ...string over passing the model down to removeUnsupportedBedrockFields, to keep that function focused on stripping and to localise the model-aware policy in augmentRequestForBedrock.

Tests

  • Unit (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.
  • Integration (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.

Copy link
Copy Markdown
Contributor Author

@dannykopping dannykopping left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-review: ✅

I validated the reported bug and this fix.

Noticed and raised #25339 and #25356 as well while testing, but it's pre-existing so I'm deferring for now.

Also raised #25356; this kludge-y approach is quickly running out of road.

…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.
@dannykopping dannykopping force-pushed the dk/aibridge-bedrock-opus-4-7-adaptive-only-thinking branch from 87036a4 to dea0f38 Compare May 14, 2026 20:14
@dannykopping dannykopping marked this pull request as ready for review May 14, 2026 20:15
@dannykopping dannykopping merged commit c6ab379 into main May 15, 2026
31 checks passed
@dannykopping dannykopping deleted the dk/aibridge-bedrock-opus-4-7-adaptive-only-thinking branch May 15, 2026 08:11
@github-actions github-actions Bot locked and limited conversation to collaborators May 15, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: Bedrock models that only support adaptive thinking (Opus 4.7+) reject thinking.type: "enabled" requests

2 participants