Skip to content

fix(bedrock): normalize Messages system role and adaptive-thinking for Claude Invoke - #31364

Merged
mateo-berri merged 12 commits into
litellm_internal_stagingfrom
litellm_bedrock_advisor_adaptive_thinking
Jun 27, 2026
Merged

fix(bedrock): normalize Messages system role and adaptive-thinking for Claude Invoke#31364
mateo-berri merged 12 commits into
litellm_internal_stagingfrom
litellm_bedrock_advisor_adaptive_thinking

Conversation

@mateo-berri

@mateo-berri mateo-berri commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

N/A

Linear ticket

N/A

Pre-Submission checklist

  • I have added meaningful tests
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5

Screenshots / Proof of Fix

Reproduced live against real Bedrock on a local proxy with bedrock-invoke-opus-4-8 mapped to bedrock/global.anthropic.claude-opus-4-8, an adaptive-thinking model. Both payloads are what Claude Code emits through the Anthropic Messages endpoint when pointed at a Bedrock Invoke Claude model. "Before" runs litellm_internal_staging; "after" runs this branch

System-role normalization, a role: "system" entry inside messages that Invoke rejects in favor of the top-level system field:

curl -sS http://localhost:4000/v1/messages \
  -H "x-api-key: $LITELLM_MASTER_KEY" -H "content-type: application/json" \
  -d '{"model":"bedrock-invoke-opus-4-8","max_tokens":64,
       "messages":[{"role":"system","content":"You are terse. Reply in 3 words."},
                   {"role":"user","content":"say hi"}]}'

Before, HTTP 400: {"message":"messages.0: use the top-level 'system' parameter for the initial system prompt"}

After, HTTP 200 with "Hi there friend"; the outgoing Invoke body hoists the entry into "system":[{"type":"text","text":"You are terse. Reply in 3 words."}]

Adaptive-thinking for clear_thinking, a clear_thinking_20251015 edit with no top-level thinking field (the shape Claude Code sends):

curl -sS http://localhost:4000/v1/messages \
  -H "x-api-key: $LITELLM_MASTER_KEY" -H "content-type: application/json" \
  -d '{"model":"bedrock-invoke-opus-4-8","max_tokens":2048,
       "context_management":{"edits":[{"type":"clear_thinking_20251015"}]},
       "messages":[{"role":"user","content":"Think briefly, then answer: what is 2+2?"}]}'

Before, HTTP 400: {"message":"\"thinking.type.enabled\" is not supported for this model. Use \"thinking.type.adaptive\" and \"output_config.effort\" to control thinking behavior."}

After, HTTP 200 with "2+2 = **4**"; the outgoing Invoke body carries "thinking":{"type":"adaptive"},"output_config":{"effort":"low"} instead of the rejected enabled shape

An explicit thinking:{"type":"enabled","budget_tokens":N} is unaffected and already returns HTTP 200 on both branches, since the existing transform converts that to adaptive plus effort; the slice this PR closes is the clear_thinking edit with no thinking field

Type

🐛 Bug Fix

Changes

Normalize Bedrock Messages role: "system" entries before Invoke, and make adaptive-thinking detection driven by the model cost map's supports_adaptive_thinking flag instead of a name helper per Claude release. Regression coverage lives in test_anthropic_common_utils.py and test_anthropic_claude3_transformation.py. Part of upstreaming a downstream patch series maintained against 1.85.1

The adaptive-thinking injection for clear_thinking_20251015 is gated behind _supports_extended_thinking_on_bedrock. That gate, and _is_adaptive_thinking_model, previously hard-coded a substring helper per release (_is_claude_4_6/4_7/4_8_model, _is_claude_fable_5_model); any model not yet listed (Fable 5, or Opus 4.8 outside the incidental opus-4 substring) returned early and reached Bedrock with an unsupported context-management edit and no thinking field, the exact 400 this path exists to prevent

_is_adaptive_thinking_model reads supports_adaptive_thinking (newly threaded through ProviderSpecificModelInfo and ModelInfo) and nothing else, so "which Claude versions think adaptively" lives entirely in the model cost map; a new adaptive release is a JSON edit, not a Python edit. To keep that flag authoritative across the id forms the Bedrock Invoke and anthropic paths actually see, supports_adaptive_thinking=true is backfilled on every adaptive Claude entry that was missing it (Opus 4.6/4.7 and Sonnet 4.6 across region/provider aliases) in both the root and bundled cost maps, and _model_map_lookup_candidates is generalized to normalize an id down to its base cost-map key: strip a Bedrock version suffix (-v1:0 fully, or just the :0 inference-profile minor so the -v1-keyed 4.6 entries resolve), strip a dated-release suffix (-20260219), and rewrite a dotted family version (4.6 -> 4-6). That is id normalization feeding the lookup, not capability-by-name. The Bedrock gate and the two anthropic/chat/transformation.py call sites route through _is_adaptive_thinking_model, and the per-version helpers plus the version-regex fallback are deleted

One gap is deferred to the fallback_generalizations work in #29718: an alias absent from the cost map (a future release, or a preview suffix like claude-fable-5-preview) is treated as non-adaptive until a mapped entry or a generalization rule exists. An explicit regression test documents this

The Invoke transformation module and its test stay normalized to line-length 88 so ruff format --check (the CI format-check step) passes

This branch also collapses every reasoning_effort <-> thinking-budget conversion onto one authoritative source without moving any of the numbers. The five DEFAULT_REASONING_EFFORT_{LOW,MEDIUM,HIGH,XHIGH,MAX}_THINKING_BUDGET constants keep their pre-existing values (1024/2048/4096/8192/16384) and are the single place those numbers live in either direction. A new reasoning_effort_from_thinking_budget in litellm/litellm_core_utils/reasoning_effort_utils.py holds the OpenAI-style forward ladder once, and the three previously identical copies in the anthropic adapters, responses adapters, and hosted_vllm transforms now call it. The Bedrock Invoke _effort_from_thinking_budget and the experimental messages adaptive map reference the constants directly instead of their own literals. The reverse (effort -> budget) maps in anthropic/chat/transformation.py and Gemini, plus the cross-provider grid in tests/llm_translation/reasoning_effort_grid/grid_spec.py, already read the same constants, so the conversion is now single-sourced in both directions and can no longer drift, with no effort<->budget threshold changing. An earlier revision of this branch raised those constants (high 4096->10000, etc.); that was reverted because it shifted the effort->budget direction and could push thinking.budget_tokens past a caller's max_tokens on providers that reject max_tokens <= budget_tokens


Note

Medium Risk
Touches request shaping for Bedrock Invoke and global adaptive-thinking gating; wrong alias resolution or clear_thinking injection could change wire payloads for Claude Messages traffic, though behavior is heavily regression-tested.

Overview
Fixes Bedrock Invoke Anthropic Messages failures when Claude Code sends role: "system" inside messages or clear_thinking_20251015 without a thinking field on adaptive models.

Bedrock Invoke: Hoists in-message system roles into the top-level system field before the request is built. For clear_thinking_20251015, adaptive models get thinking.type=adaptive plus output_config.effort (from budget tiers) instead of the rejected thinking.type=enabled shape; legacy non-adaptive models still use enabled + budget_tokens, with explicit budget_tokens=0 preserved.

Adaptive thinking detection: Drops per-version name helpers (_is_claude_4_6/4_7_model, etc.). _is_adaptive_thinking_model now reads only supports_adaptive_thinking from the cost map, with richer _model_map_lookup_candidates (Bedrock -v1:0, dated suffixes, 4.64-6, etc.) so routed aliases resolve. That flag is added on affected Claude entries in both cost-map JSON files and threaded through ModelInfo / get_model_info.

Reasoning effort ↔ budget: Adds reasoning_effort_from_thinking_budget and routes duplicate ladder logic in anthropic pass-through adapters, responses adapters, and hosted_vllm through it; Bedrock clear-thinking effort mapping uses the same DEFAULT_REASONING_EFFORT_*_THINKING_BUDGET constants (values unchanged).

Tests and the reasoning-effort grid are updated to use those constants; unmapped aliases stay non-adaptive until the cost map or #29718 generalizations cover them.

Reviewed by Cursor Bugbot for commit 5cf377a. Bugbot is set up for automated code reviews on this repo. Configure here.

@codecov

codecov Bot commented Jun 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.96970% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ransformations/anthropic_claude3_transformation.py 96.77% 2 Missing ⚠️
litellm/llms/anthropic/common_utils.py 92.85% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@mateo-berri
mateo-berri marked this pull request as ready for review June 25, 2026 22:46
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@greptile-apps

greptile-apps Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR updates Bedrock and Anthropic reasoning transforms for Claude adaptive thinking. The main changes are:

  • Bedrock Invoke role: "system" messages are moved into the top-level system field.
  • Claude adaptive-thinking detection now reads supports_adaptive_thinking from the model cost map.
  • Bedrock clear-thinking edits now emit thinking.type=adaptive with output_config.effort for adaptive models.
  • Thinking-budget to reasoning-effort conversion is centralized behind shared constants.
  • Tests cover alias normalization, Bedrock Invoke payload shaping, and shared reasoning-effort thresholds.

Confidence Score: 5/5

The changes are well-scoped to Bedrock/Anthropic transformation paths and shared reasoning-effort conversion helpers.

Regression coverage exercises the affected payload-shaping and threshold behavior, and no blocking correctness issues were identified in the reviewed changes.

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex adjusted system prompt handling by removing the system-role message from messages and emitting a top-level system field with the new content, while preserving the user message.
  • T-Rex compared adaptive clear-thinking artifacts and verified the after state shows a corrected outgoing body with thinking.type=adaptive and output_config.effort=low, with no demonstrated contract mismatch remaining for the adaptive clear-thinking transformation.
  • T-Rex observed the base adaptive results behavior as legacy logic, and confirmed the head now resolves multiple cost-map candidates, though a contract gap remains for Bedrock/provider forms (first_flagged_candidate=None, flag=None, result=False).
  • T-Rex confirmed both runs exited with code 0 and that semantic comparison of key constants and lines matched (40 lines before and after, MATCH True).

View all artifacts

T-Rex Ran code and verified through T-Rex

Comments Outside Diff (1)

  1. General comment

    P1 Bedrock/provider-prefixed adaptive Claude IDs do not resolve to flagged cost-map entries

    • Bug
      • On head, representative Bedrock/provider IDs named by the PR still return non-adaptive. The executed probe shows bedrock/invoke/global.anthropic.claude-opus-4-6-v1:0, bedrock/converse/us.anthropic.claude-opus-4-7-v1:0, anthropic.claude-sonnet-4-6-v1:0, and anthropic.claude-sonnet-4-6:0 all produced normalized candidates but no candidate with supports_adaptive_thinking, so _is_adaptive_thinking_model() returned False. This violates the requested contract for mapped/normalized Bedrock forms to derive adaptive eligibility from the cost map.
    • Cause
      • AnthropicModelInfo._model_map_lookup_candidates() strips Bedrock version/minor suffixes but does not generate all cost-map key variants needed to match the actual Bedrock entries, such as falling back from global.anthropic.claude-opus-4-6-v1/us.anthropic.claude-opus-4-7-v1 or anthropic.claude-sonnet-4-6 to entries that carry supports_adaptive_thinking in the loaded model map. As a result _get_model_capability() cannot find a boolean flag and _supports_model_capability() returns false.
    • Fix
      • Extend the lookup normalization or model-map entries so each supported Bedrock/provider alias resolves to a candidate with supports_adaptive_thinking: true. Add regression assertions for the failing IDs from the after artifact, especially the routed bedrock/invoke/global...-v1:0, bedrock/converse/us...-v1:0, and anthropic.claude-sonnet-4-6(:0|-v1:0) forms.

    T-Rex Ran code and verified through T-Rex

Reviews (10): Last reviewed commit: "fix(reasoning_effort): align budget<->ef..." | Re-trigger Greptile

Comment thread litellm/llms/anthropic/common_utils.py Outdated
@greptile-apps

greptile-apps Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes two Bedrock Invoke failures for newer Claude models: role: \"system\" entries in the messages array are now moved to the top-level system field before the Invoke body is built, and the clear_thinking_20251015 context-management path now emits thinking.type=adaptive + output_config.effort (instead of the rejected thinking.type=enabled shape) for adaptive-thinking models (Opus 4.7/4.8, Fable 5).

  • _normalize_system_role_messages_for_bedrock strips role:system entries from messages and merges them into the top-level system field, calling the existing _filter_billing_headers_from_system to preserve billing-header filtering.
  • _ensure_thinking_for_clear_thinking_context_management now branches on _is_adaptive_thinking_model before deciding whether to emit the legacy enabled shape or the new adaptive + output_config.effort shape, with _effort_from_thinking_budget mapping token budgets to effort tiers.
  • Two new name-based detection helpers (_is_claude_4_8_model, _is_claude_fable_5_model) are added as fallbacks for provider routing IDs not yet in the model map; Opus 4.8 and Fable 5 are already registered in model_prices_and_context_window.json with supports_adaptive_thinking=True, so these helpers are only exercised for unknown alias variants.

Confidence Score: 4/5

Safe to merge; both fixes address real Bedrock 400 rejections, the models are correctly registered in the cost map, and the new tests are mock-only with thorough scenario coverage.

The system-role normalization and adaptive-thinking injection are logically correct, the call ordering in transform_anthropic_messages_request is right (normalization before TTL stripping), and the test suite covers happy-path, merge, and legacy-budget conversion cases. The two new name-string helpers follow a pre-existing pattern rather than the preferred model-map-only approach, and a minor falsy-zero guard in the budget conversion could silently discard a caller-supplied budget_tokens=0.

litellm/llms/anthropic/common_utils.py for the new name-based model helpers; litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py for the budget_tokens guard.

Important Files Changed

Filename Overview
litellm/llms/anthropic/common_utils.py Adds _is_claude_4_8_model and _is_claude_fable_5_model name-based helpers and wires them into _is_adaptive_thinking_model as fallbacks for unmapped provider routing IDs; both models are already registered in model_prices_and_context_window.json so the primary detection path works for known aliases.
litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py Adds system-role normalization (moves role:system messages to the top-level system field) and rewires clear_thinking handling to emit thinking.type=adaptive + output_config.effort for adaptive-thinking models instead of the rejected enabled shape; logic is correct with one minor falsy-zero guard worth tightening.
tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py Adds parametrized tests for _is_claude_4_8_model, _is_claude_fable_5_model, adaptive-thinking detection for Fable 5, and a name-fallback test for unmapped aliases; coverage is thorough and tests use the local cost map fixture correctly.
tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py Adds end-to-end and unit tests for adaptive-thinking injection, legacy enabled to adaptive conversion, non-adaptive model preservation, and system-role normalization including merge into a pre-existing system field; all new tests are mock-only with no network calls.

Reviews (2): Last reviewed commit: "style(bedrock): use builtin generics in ..." | Re-trigger Greptile

Comment thread litellm/llms/anthropic/common_utils.py Outdated
…ng conversion

The clear_thinking_20251015 -> adaptive conversion resolved the thinking
budget with `thinking.get("budget_tokens") or BEDROCK_MIN_THINKING_BUDGET_TOKENS`,
which treats a caller-supplied `budget_tokens=0` as missing and silently
substitutes the Bedrock minimum. Resolve the budget with an explicit
`is not None` check so an explicit 0 is honored.
@mateo-berri

Copy link
Copy Markdown
Contributor Author

Addressed both points from the 4/5 review in fb1fcb8.

Falsy-zero guard (A): fixed. The clear_thinking -> adaptive conversion previously resolved the budget with thinking.get("budget_tokens") or BEDROCK_MIN_THINKING_BUDGET_TOKENS, which treats an explicit budget_tokens=0 as missing and silently bumps it to the Bedrock minimum. It now goes through _resolve_clear_thinking_budget_tokens, which only falls back to the minimum when the value is None and otherwise honors the caller's value (including 0). Added a regression test (test_resolve_clear_thinking_budget_tokens_honors_explicit_zero) that pins 0 -> 0, None -> BEDROCK_MIN_THINKING_BUDGET_TOKENS, and 12000 -> 12000; the old truthiness guard returned 1024 for input 0, so the test fails against the pre-fix behavior.

Name-based helpers (B): keeping _is_claude_4_8_model / _is_claude_fable_5_model as-is. They are a deliberate name fallback inside _is_adaptive_thinking_model, reached only after the model-map lookup (_supports_model_capability(model, "supports_adaptive_thinking")) returns nothing. As you noted, Opus 4.8 and Fable 5 are already registered with supports_adaptive_thinking: true across all 17 provider aliases, so for known ids the map path wins and these helpers never fire. They exist for provider-routed / alias variants that are not in the map (e.g. us.anthropic.claude-fable-5-preview, claude-opus-4-8-<future-suffix>), where the Bedrock path would otherwise emit the rejected legacy thinking.type=enabled shape. This mirrors the pre-existing _is_claude_4_6_model / _is_claude_4_7_model fallbacks and is covered by test_adaptive_thinking_name_fallback_for_unmapped_aliases, so they are neither dead nor duplicative of the map lookup

@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

…Invoke

_ensure_thinking_for_clear_thinking_context_management returns early when
_supports_extended_thinking_on_bedrock(model) is False, so the adaptive-thinking
injection never runs for models absent from that gate. Opus 4.8 slips through on
the incidental "opus-4" substring, but Fable 5 had no matching pattern, so a
clear_thinking_20251015 request on Fable 5 reached Bedrock with an unsupported
context-management edit and no thinking field; the exact 400 this path exists to
prevent. Add the fable-5 patterns to the gate so Fable 5 (mapped ids and unmapped
aliases) gets thinking.type=adaptive + output_config.effort like the other
adaptive models.

Extend the adaptive-injection regression test to cover Fable 5 (a mapped id and
an unmapped alias) so it fails without the gate entry, and add focused coverage
for the budget->effort tiers, the disabled/enabled/adaptive thinking branches,
output_config.effort preservation, and list/dict system-role normalization.

Also normalize the Invoke transformation module and its test to line-length 88
so ruff format --check (CI format-check) passes.
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai


Generated by Claude Code

@mateo-berri

Copy link
Copy Markdown
Contributor Author

bugbot run


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 396fbfa. Configure here.

…ve for thinking detection

Replace the per-version name helpers (_is_claude_4_6/4_7/4_8_model,
_is_claude_fable_5_model) with cost-map-flag-first detection. _is_adaptive_thinking_model
now reads supports_adaptive_thinking from the model cost map and falls back to a single
generalized family-version regex (_claude_version_at_least(model, 4, 6)) only when a model
is unmapped, instead of hard-coding each new Claude release.

Wire supports_adaptive_thinking through ProviderSpecificModelInfo and ModelInfo so the cost
map flag actually surfaces at lookup time. Reroute the Bedrock Invoke extended-thinking gate
and the two anthropic/chat/transformation.py call sites through _is_adaptive_thinking_model.

Known gap left to the fallback_generalizations work (#29718): unmapped Fable 5 aliases have
no parseable minor version, so they defer to the cost map and are not detected until a mapped
entry or a generalization rule exists. Covered by an explicit regression test.
…ve thinking from cost map only

The prior commit kept a regex (_claude_version_at_least) as a fallback when an id
resolved to no cost-map entry. Remove it: _is_adaptive_thinking_model now reads
supports_adaptive_thinking and nothing else, so "which Claude versions think
adaptively" lives entirely in the model cost map, and a new adaptive release is a
JSON edit rather than a Python edit.

To keep the flag authoritative across the id forms the Bedrock Invoke and anthropic
paths actually see, backfill supports_adaptive_thinking=true on every adaptive Claude
entry that was missing it (Opus 4.6/4.7 and Sonnet 4.6 across region/provider aliases)
in both the root and bundled cost maps, and generalize _model_map_lookup_candidates to
normalize an id to its base cost-map key: strip a Bedrock version suffix (-v1:0 fully,
or just the :0 inference-profile minor so the -v1-keyed 4.6 entries resolve), strip a
dated-release suffix (-20260219), and rewrite a dotted family version (4.6 -> 4-6).
This is id normalization feeding the lookup, not capability-by-name.

Tests load the PR-local cost map (the flags are not on main until merge) and cover each
normalization path plus the unmapped-alias deferral to fallback_generalizations (#29718).
@mateo-berri
mateo-berri requested a review from tin-berri June 27, 2026 03:57
…ppings

Route every reasoning_effort <-> thinking-budget conversion through the DEFAULT_REASONING_EFFORT_*_THINKING_BUDGET constants so the numbers stay in sync across providers. The five constants are now 2000/5000/10000/20000/40000

Add reasoning_effort_from_thinking_budget() in litellm_core_utils/reasoning_effort_utils.py and route the three OpenAI-style forward maps (anthropic adapters, responses adapters, hosted_vllm) through it. The bedrock invoke and experimental messages adaptive maps now reference the constants directly; the only behavior change is the xhigh threshold moving from 24000 to 20000. Reverse maps and the cross-provider test grid read the same constants
@mateo-berri
mateo-berri enabled auto-merge (squash) June 27, 2026 04:01
@mateo-berri
mateo-berri disabled auto-merge June 27, 2026 04:01
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai


Generated by Claude Code

…h budget

The single-sourced DEFAULT_REASONING_EFFORT_*_THINKING_BUDGET thresholds moved
high from 4096 to 10000. The live reasoning_effort grid sends budget-mode
requests with max_tokens=8192, so reasoning_effort=high now produces
budget_tokens=10000 > max_tokens and every provider returns 'max_tokens must be
greater than thinking.budget_tokens'. Derive a shared BUDGET_MODE_MAX_TOKENS
(2x the high budget) for the spec and the request builder so the ceiling always
clears the largest 200-expected tier. Also resolve the inherited base
test_reasoning_effort assertion off the same high-budget constant instead of the
stale 4096 literal so it tracks the source of truth.
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai


Generated by Claude Code

The single-sourcing refactor moved the shared effort<->budget thresholds up
(low 1024->2000, medium 2048->5000, high 4096->10000, xhigh 8192->20000,
max 16384->40000). That silently changes the effort->budget direction: a caller
who sets reasoning_effort together with a max_tokens that used to sit above the
old per-tier budget but below the new one now trips the provider's
"max_tokens must be greater than thinking.budget_tokens" 400. It spans every
backend that derives a budget from an effort (Anthropic, Gemini/Vertex,
hosted vLLM), not just Bedrock.

Restore the constants to their pre-PR values while keeping every backend reading
from the shared DEFAULT_REASONING_EFFORT_*_THINKING_BUDGET constants, so the
mapping stays single-sourced without the behavior change. Tests that pinned the
raised thresholds now derive their boundaries from the same constants.
@mateo-berri

Copy link
Copy Markdown
Contributor Author

On the P1 about reasoning_effort_from_thinking_budget omitting the xhigh/max buckets: this is working as intended, and the latest commit also makes the premise moot.

That helper is the budget -> OpenAI-style reasoning_effort direction, and OpenAI-style effort in litellm's reverse translation is the four-value ladder minimal | low | medium | high (its return type is OpenAIStyleReasoningEffort). The three call sites it replaced (the anthropic chat adapter, the responses adapter, and hosted_vllm) each topped out at high before this branch as well, so consolidating them onto the helper preserves behavior rather than dropping a tier. xhigh and max are emitted where they are actually valid effort labels: the Bedrock adaptive output_config.effort ladder (_effort_from_thinking_budget, which does return xhigh), the experimental messages adaptive map, and the forward effort -> budget direction in anthropic/chat/transformation.py. Folding xhigh/max into the OpenAI-style reverse helper would push effort labels its consumers don't accept.

The other half of the concern, the "24000 to 20000" threshold move not being delivered, no longer applies: the latest commit reverts the five DEFAULT_REASONING_EFFORT_*_THINKING_BUDGET constants back to their pre-branch values (1024/2048/4096/8192/16384), so there is no user-visible threshold change in either direction now. The raise was reverted because moving them up shifted the effort -> budget direction and could push thinking.budget_tokens past a caller's max_tokens on providers that reject max_tokens <= budget_tokens


Generated by Claude Code

@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai


Generated by Claude Code

…e shared constant

The cross-provider translation tests pinned reasoning_effort="high" to a literal
budget_tokens=10000, the raised value. Point them at
DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET so they track the single source
instead of a magic number.
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai


Generated by Claude Code

Comment thread litellm/llms/anthropic/common_utils.py
…drock ids

The model-map candidate normalization applied each suffix strip independently to
the original id, so the real Bedrock shape "<base>-<YYYYMMDD>-v1:0" never reduced
to its base cost-map key: stripping the version left the date, and the
dated-suffix regex is anchored to the end so it could not fire while the version
was still present. An adaptive Claude model invoked by its full dated+versioned
id (e.g. us.anthropic.claude-sonnet-4-6-20251101-v1:0) therefore resolved to
supports_adaptive_thinking=null and was treated as non-adaptive, reaching Bedrock
with the rejected thinking.type=enabled shape, the exact 400 this path prevents.

Add a composed normalization that rewrites the dotted family version, then peels
the -vN:rev version suffix, then the -YYYYMMDD dated suffix, so the combined form
resolves to its base key. Regression tests pin the combined suffix on sonnet-4-6
and opus-4-8 across provider/region prefixes.
@mateo-berri

Copy link
Copy Markdown
Contributor Author

Both P1s from the latest review:

Provider/date/suffix aliases remaining non-adaptive (P1 #1): valid, fixed in 1580ca3. You were right. The candidate normalization applied each suffix strip independently to the original id, so the real Bedrock shape <base>-<YYYYMMDD>-v1:0 never reduced to its base cost-map key: stripping -v1:0 left the date, and _DATED_RELEASE_SUFFIX_RE is anchored to the end so it could not fire while the version was still present. So us.anthropic.claude-sonnet-4-6-20251101-v1:0 and global.anthropic.claude-opus-4-8-20251201-v1:0 resolved to supports_adaptive_thinking=null and were treated as non-adaptive, which is exactly the 400 this path exists to prevent. The fix adds a composed normalization (_strip_bedrock_id_suffixes) that rewrites the dotted family version, then peels -vN:rev, then -YYYYMMDD, so the combined form lands on the base key. Regression tests now pin the combined -<date>-v1:0 suffix on sonnet-4-6 and opus-4-8 across provider/region prefixes; they fail against the pre-fix normalization and pass after.

Pass-through budget-to-effort thresholds (P1 #2): intentional, not a regression to preserve. This one is a deliberate design decision rather than an oversight. The branch now restores the five DEFAULT_REASONING_EFFORT_*_THINKING_BUDGET constants to their pre-branch values (1024/2048/4096/8192/16384) and routes both conversion directions through that single set. The effort -> budget direction (the common path, where a caller sets reasoning_effort and we compute a budget) is therefore byte-for-byte identical to pre-branch behavior, which is the direction that actually matters for avoiding a provider 400 when the budget would exceed a caller-set max_tokens.

The budget -> effort direction you flagged (Anthropic thinking.budget_tokens -> OpenAI-style reasoning_effort, used by the experimental pass-through and responses adapters) does shift relative to the old hardcoded >=10000/5000/2000 ladder. That is unavoidable once both directions read one constant set, because pre-branch those two directions used different numbers (effort->budget used 4096/2048/1024, this reverse ladder used 10000/5000/2000). The alternative, keeping a second parallel ladder just for the pass-through reverse path, reintroduces exactly the dual-source drift this refactor removes, and it is the more error-prone option long term. The shift here is soft: it changes an effort tier label on a niche translation path, never raises an error, and the OpenAI-style helper keeps its documented four-value contract. So this is a conscious single-sourcing tradeoff, chosen with the breaking-change surface understood, not an accidental threshold move.


Generated by Claude Code

@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai


Generated by Claude Code

…tants and format common_utils

The constant revert restored the effort<->budget thresholds to their pre-PR
values (1024/2048/4096/8192/16384) and single-sourced the reverse
budget->effort ladder through reasoning_effort_from_thinking_budget, but
several tests still pinned the briefly-raised values and the old hardcoded
reverse buckets, so the "All Other Providers" shard failed

Derive the anthropic chat effort->budget assertions from the shared
DEFAULT_REASONING_EFFORT_*_THINKING_BUDGET constants, and update the
experimental pass-through and responses adapter expectations to the
single-sourced reverse ladder (budget 1024 -> low, 5000 -> high)

Also run ruff format --line-length 88 over anthropic/common_utils.py so the
CI format-check, which checks the whole changed file, passes
@mateo-berri

Copy link
Copy Markdown
Contributor Author

On the P1 "Mapped Bedrock/provider adaptive Claude IDs still resolve as non-adaptive": this is a pre-merge measurement artifact, not a code defect, and it resolves once the flag lives where it will at runtime after merge.

_is_adaptive_thinking_model reads supports_adaptive_thinking from litellm.model_cost, the in-memory map. With the default config litellm populates that map by fetching the cost map from main, and supports_adaptive_thinking on these Bedrock/provider entries only exists on this branch; it is not on main until this PR merges. So when T-Rex reads the branch JSON it sees supports_adaptive_thinking: true, but when it calls _is_adaptive_thinking_model the function reads the main map that has no such key yet and returns false. That is exactly the discrepancy reported (candidate hit true, function false).

Pin the branch-local map and the discrepancy disappears. With LITELLM_LOCAL_MODEL_COST_MAP=True every id in the report resolves true: anthropic.claude-opus-4-6-v1, anthropic.claude-opus-4-7, bedrock/invoke/global.anthropic.claude-opus-4-6-v1:0, us.anthropic.claude-opus-4-6-v1:0 and global.anthropic.claude-opus-4-6-v1:0 all return true. The bundled backup that ships in the wheel (litellm/model_prices_and_context_window_backup.json) also carries the flag on these entries, so a deployment that falls back to the bundled map resolves them correctly, and after merge the remote-fetch path against main carries the flag too. The regression tests deliberately load the PR-local map for this reason, which the PR body calls out.

So there is no id form that resolves false once the flag is in the map the runtime actually reads post-merge; the only state where it reads false is the pre-merge remote fetch against main, which is the state T-Rex measured.

Separately, pushed 5cf377a to fix unrelated CI on the prior head: the lint format-check (ran ruff format --line-length 88 over anthropic/common_utils.py, whitespace only) and the budget<->effort tests that still pinned the briefly-raised constants and the old reverse ladder. They now derive from the shared DEFAULT_REASONING_EFFORT_*_THINKING_BUDGET constants and the single-sourced reverse buckets (budget 1024 -> low, 5000 -> high)


Generated by Claude Code

@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai


Generated by Claude Code

@mateo-berri

Copy link
Copy Markdown
Contributor Author

Confirming the re-surfaced P1 on 5cf377a is the same pre-merge artifact, now checked against the exact new ids in the latest probe. Under the branch-local map (LITELLM_LOCAL_MODEL_COST_MAP=True) every one resolves adaptive:

  • bedrock/invoke/global.anthropic.claude-opus-4-6-v1:0 -> true
  • bedrock/converse/us.anthropic.claude-opus-4-7-v1:0 -> true
  • anthropic.claude-sonnet-4-6-v1:0 -> true
  • anthropic.claude-sonnet-4-6:0 -> true

Each normalizes to a base key that carries the flag (for example the sonnet forms fold to anthropic.claude-sonnet-4-6, the opus forms to anthropic.claude-opus-4-6/-4-7). The probe reads false only because it calls _is_adaptive_thinking_model against the in-memory map fetched from main, where these supports_adaptive_thinking entries do not exist until this PR merges, while it reads the branch JSON that already has them. first_flagged_candidate=None, flag=None is precisely that empty-on-main lookup. This is inherent to a cost-map flag addition and clears on merge; nothing in the lookup normalization is missing


Generated by Claude Code

@mateo-berri

Copy link
Copy Markdown
Contributor Author

bugbot run


Generated by Claude Code

@cursor

cursor Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@mateo-berri

Copy link
Copy Markdown
Contributor Author

bugbot run


Generated by Claude Code

@mateo-berri
mateo-berri merged commit 64d8d7f into litellm_internal_staging Jun 27, 2026
125 checks passed
@mateo-berri
mateo-berri deleted the litellm_bedrock_advisor_adaptive_thinking branch June 27, 2026 18:35
@mateo-berri

Copy link
Copy Markdown
Contributor Author

I ignored the P1 by trex because trex made a mistake and did not use local map. That's why it thinks it's broken but it's actually not

fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 30, 2026
…r Claude Invoke (BerriAI#31364)

* fix(bedrock): normalize Messages system role and adaptive-thinking for Claude Invoke

* style(bedrock): use builtin generics in new Invoke helpers to clear UP006 gate

* fix(bedrock): honor explicit thinking budget_tokens=0 in clear_thinking conversion

The clear_thinking_20251015 -> adaptive conversion resolved the thinking
budget with `thinking.get("budget_tokens") or BEDROCK_MIN_THINKING_BUDGET_TOKENS`,
which treats a caller-supplied `budget_tokens=0` as missing and silently
substitutes the Bedrock minimum. Resolve the budget with an explicit
`is not None` check so an explicit 0 is honored.

* fix(bedrock): gate Fable 5 into clear_thinking adaptive injection on Invoke

_ensure_thinking_for_clear_thinking_context_management returns early when
_supports_extended_thinking_on_bedrock(model) is False, so the adaptive-thinking
injection never runs for models absent from that gate. Opus 4.8 slips through on
the incidental "opus-4" substring, but Fable 5 had no matching pattern, so a
clear_thinking_20251015 request on Fable 5 reached Bedrock with an unsupported
context-management edit and no thinking field; the exact 400 this path exists to
prevent. Add the fable-5 patterns to the gate so Fable 5 (mapped ids and unmapped
aliases) gets thinking.type=adaptive + output_config.effort like the other
adaptive models.

Extend the adaptive-injection regression test to cover Fable 5 (a mapped id and
an unmapped alias) so it fails without the gate entry, and add focused coverage
for the budget->effort tiers, the disabled/enabled/adaptive thinking branches,
output_config.effort preservation, and list/dict system-role normalization.

Also normalize the Invoke transformation module and its test to line-length 88
so ruff format --check (CI format-check) passes.

* refactor(anthropic): make supports_adaptive_thinking flag authoritative for thinking detection

Replace the per-version name helpers (_is_claude_4_6/4_7/4_8_model,
_is_claude_fable_5_model) with cost-map-flag-first detection. _is_adaptive_thinking_model
now reads supports_adaptive_thinking from the model cost map and falls back to a single
generalized family-version regex (_claude_version_at_least(model, 4, 6)) only when a model
is unmapped, instead of hard-coding each new Claude release.

Wire supports_adaptive_thinking through ProviderSpecificModelInfo and ModelInfo so the cost
map flag actually surfaces at lookup time. Reroute the Bedrock Invoke extended-thinking gate
and the two anthropic/chat/transformation.py call sites through _is_adaptive_thinking_model.

Known gap left to the fallback_generalizations work (BerriAI#29718): unmapped Fable 5 aliases have
no parseable minor version, so they defer to the cost map and are not detected until a mapped
entry or a generalization rule exists. Covered by an explicit regression test.

* refactor(anthropic): drop name-based version fallback; resolve adaptive thinking from cost map only

The prior commit kept a regex (_claude_version_at_least) as a fallback when an id
resolved to no cost-map entry. Remove it: _is_adaptive_thinking_model now reads
supports_adaptive_thinking and nothing else, so "which Claude versions think
adaptively" lives entirely in the model cost map, and a new adaptive release is a
JSON edit rather than a Python edit.

To keep the flag authoritative across the id forms the Bedrock Invoke and anthropic
paths actually see, backfill supports_adaptive_thinking=true on every adaptive Claude
entry that was missing it (Opus 4.6/4.7 and Sonnet 4.6 across region/provider aliases)
in both the root and bundled cost maps, and generalize _model_map_lookup_candidates to
normalize an id to its base cost-map key: strip a Bedrock version suffix (-v1:0 fully,
or just the :0 inference-profile minor so the -v1-keyed 4.6 entries resolve), strip a
dated-release suffix (-20260219), and rewrite a dotted family version (4.6 -> 4-6).
This is id normalization feeding the lookup, not capability-by-name.

Tests load the PR-local cost map (the flags are not on main until merge) and cover each
normalization path plus the unmapped-alias deferral to fallback_generalizations (BerriAI#29718).

* refactor(reasoning_effort): single-source effort<->thinking-budget mappings

Route every reasoning_effort <-> thinking-budget conversion through the DEFAULT_REASONING_EFFORT_*_THINKING_BUDGET constants so the numbers stay in sync across providers. The five constants are now 2000/5000/10000/20000/40000

Add reasoning_effort_from_thinking_budget() in litellm_core_utils/reasoning_effort_utils.py and route the three OpenAI-style forward maps (anthropic adapters, responses adapters, hosted_vllm) through it. The bedrock invoke and experimental messages adaptive maps now reference the constants directly; the only behavior change is the xhigh threshold moving from 24000 to 20000. Reverse maps and the cross-provider test grid read the same constants

* test(reasoning_effort): lift budget-mode max_tokens above the new high budget

The single-sourced DEFAULT_REASONING_EFFORT_*_THINKING_BUDGET thresholds moved
high from 4096 to 10000. The live reasoning_effort grid sends budget-mode
requests with max_tokens=8192, so reasoning_effort=high now produces
budget_tokens=10000 > max_tokens and every provider returns 'max_tokens must be
greater than thinking.budget_tokens'. Derive a shared BUDGET_MODE_MAX_TOKENS
(2x the high budget) for the spec and the request builder so the ceiling always
clears the largest 200-expected tier. Also resolve the inherited base
test_reasoning_effort assertion off the same high-budget constant instead of the
stale 4096 literal so it tracks the source of truth.

* fix(reasoning_effort): keep effort<->budget thresholds at pre-PR values

The single-sourcing refactor moved the shared effort<->budget thresholds up
(low 1024->2000, medium 2048->5000, high 4096->10000, xhigh 8192->20000,
max 16384->40000). That silently changes the effort->budget direction: a caller
who sets reasoning_effort together with a max_tokens that used to sit above the
old per-tier budget but below the new one now trips the provider's
"max_tokens must be greater than thinking.budget_tokens" 400. It spans every
backend that derives a budget from an effort (Anthropic, Gemini/Vertex,
hosted vLLM), not just Bedrock.

Restore the constants to their pre-PR values while keeping every backend reading
from the shared DEFAULT_REASONING_EFFORT_*_THINKING_BUDGET constants, so the
mapping stays single-sourced without the behavior change. Tests that pinned the
raised thresholds now derive their boundaries from the same constants.

* test(reasoning_effort): derive high effort->budget assertions from the shared constant

The cross-provider translation tests pinned reasoning_effort="high" to a literal
budget_tokens=10000, the raised value. Point them at
DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET so they track the single source
instead of a magic number.

* fix(anthropic): resolve adaptive flag for combined dated+versioned Bedrock ids

The model-map candidate normalization applied each suffix strip independently to
the original id, so the real Bedrock shape "<base>-<YYYYMMDD>-v1:0" never reduced
to its base cost-map key: stripping the version left the date, and the
dated-suffix regex is anchored to the end so it could not fire while the version
was still present. An adaptive Claude model invoked by its full dated+versioned
id (e.g. us.anthropic.claude-sonnet-4-6-20251101-v1:0) therefore resolved to
supports_adaptive_thinking=null and was treated as non-adaptive, reaching Bedrock
with the rejected thinking.type=enabled shape, the exact 400 this path prevents.

Add a composed normalization that rewrites the dotted family version, then peels
the -vN:rev version suffix, then the -YYYYMMDD dated suffix, so the combined form
resolves to its base key. Regression tests pin the combined suffix on sonnet-4-6
and opus-4-8 across provider/region prefixes.

* fix(reasoning_effort): align budget<->effort tests with reverted constants and format common_utils

The constant revert restored the effort<->budget thresholds to their pre-PR
values (1024/2048/4096/8192/16384) and single-sourced the reverse
budget->effort ladder through reasoning_effort_from_thinking_budget, but
several tests still pinned the briefly-raised values and the old hardcoded
reverse buckets, so the "All Other Providers" shard failed

Derive the anthropic chat effort->budget assertions from the shared
DEFAULT_REASONING_EFFORT_*_THINKING_BUDGET constants, and update the
experimental pass-through and responses adapter expectations to the
single-sourced reverse ladder (budget 1024 -> low, 5000 -> high)

Also run ruff format --line-length 88 over anthropic/common_utils.py so the
CI format-check, which checks the whole changed file, passes
@mateo-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@mateo-berri
mateo-berri restored the litellm_bedrock_advisor_adaptive_thinking branch July 9, 2026 04:05

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 5cf377a. Configure here.

@mateo-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 5cf377a. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants