From a55817cbc6df704d90a2151165e8b19e73da53fc Mon Sep 17 00:00:00 2001 From: Mateo Wang <277851410+mateo-berri@users.noreply.github.com> Date: Fri, 29 May 2026 13:55:06 -0700 Subject: [PATCH] fix(anthropic): stop injecting unsupported output_config.effort=xhigh for Claude Code on Sonnet/Opus 4.6 (#29304) * fix(anthropic): don't inject output_config.effort=xhigh on models without xhigh The legacy-thinking translator on the /v1/messages route mapped any thinking.budget_tokens >= 24000 to effort=xhigh and injected it into output_config without checking model support. Claude Code's default thinking budget (31999) hit this bucket, so Sonnet 4.6 (and Opus 4.6) on Bedrock/Vertex started returning 400 output_config.effort: Input should be 'low', 'medium', 'high' or 'max' Gate the xhigh choice on _supports_effort_level(model, "xhigh"), the same capability check the reasoning_effort path already uses. Models that advertise xhigh (Opus 4.7) keep it; everything else falls to high. Fixes #29282 * test(anthropic): pin Opus 4.6 in legacy-thinking xhigh-clamp regression test Opus 4.6 (bare, bedrock/invoke, vertex_ai) has supports_adaptive_thinking but no supports_xhigh_reasoning_effort, so it hits the same clamping path as Sonnet 4.6. It was named in the PR scope but lacked a pinned regression guard; add the three variants to the parametrize list. --- .../messages/transformation.py | 4 +- .../test_reasoning_effort_translation.py | 121 ++++++++++++++++++ 2 files changed, 124 insertions(+), 1 deletion(-) diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py index f94232fa45..3a2c09f218 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py @@ -230,6 +230,8 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): """Translate legacy ``thinking.type=enabled`` to adaptive for 4.6/4.7. Caller-provided ``output_config.effort`` is never overridden. """ + from litellm.llms.anthropic.chat.transformation import AnthropicConfig + if not AnthropicModelInfo._is_adaptive_thinking_model(model): return thinking = optional_params.get("thinking") @@ -237,7 +239,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): return budget = int(thinking.get("budget_tokens") or 0) - if budget >= 24000: + if budget >= 24000 and AnthropicConfig._supports_effort_level(model, "xhigh"): effort = "xhigh" elif budget >= 10000: effort = "high" diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_effort_translation.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_effort_translation.py index 54bf0c4ac0..09601a6581 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_effort_translation.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_effort_translation.py @@ -243,3 +243,124 @@ def test_reasoning_effort_in_supported_params(): assert "reasoning_effort" in config.get_supported_anthropic_messages_params( "claude-opus-4-7" ) + + +@pytest.mark.parametrize( + "model", + [ + "claude-sonnet-4-6", + "bedrock/invoke/us.anthropic.claude-sonnet-4-6", + "vertex_ai/claude-sonnet-4-6", + "claude-opus-4-6", + "bedrock/invoke/us.anthropic.claude-opus-4-6", + "vertex_ai/claude-opus-4-6", + ], +) +def test_legacy_thinking_high_budget_clamps_to_high_when_xhigh_unsupported(model): + """Claude Code sends ``thinking.budget_tokens=31999``; Sonnet 4.6 and Opus 4.6 + have no ``xhigh`` tier, so the translator must emit ``high`` rather than the + provider-invalid ``xhigh`` (regression for issue #29282).""" + config = AnthropicMessagesConfig() + optional_params = { + "max_tokens": 1024, + "thinking": {"type": "enabled", "budget_tokens": 31999}, + } + + result = config.transform_anthropic_messages_request( + model=model, + messages=[{"role": "user", "content": "Hello"}], + anthropic_messages_optional_request_params=optional_params, + litellm_params={}, + headers={}, + ) + + assert result.get("thinking") == {"type": "adaptive"} + assert result.get("output_config") == {"effort": "high"} + + +def test_legacy_thinking_high_budget_keeps_xhigh_when_supported(): + """Opus 4.7 advertises an ``xhigh`` tier, so the high-budget bucket keeps it.""" + config = AnthropicMessagesConfig() + optional_params = { + "max_tokens": 1024, + "thinking": {"type": "enabled", "budget_tokens": 31999}, + } + + result = config.transform_anthropic_messages_request( + model="claude-opus-4-7", + messages=[{"role": "user", "content": "Hello"}], + anthropic_messages_optional_request_params=optional_params, + litellm_params={}, + headers={}, + ) + + assert result.get("thinking") == {"type": "adaptive"} + assert result.get("output_config") == {"effort": "xhigh"} + + +@pytest.mark.parametrize( + "budget_tokens,expected_effort", + [ + (31999, "high"), + (24000, "high"), + (10000, "high"), + (9999, "medium"), + (5000, "medium"), + (4999, "low"), + (1024, "low"), + ], +) +def test_legacy_thinking_budget_buckets_on_sonnet_46(budget_tokens, expected_effort): + config = AnthropicMessagesConfig() + optional_params = { + "max_tokens": 1024, + "thinking": {"type": "enabled", "budget_tokens": budget_tokens}, + } + + result = config.transform_anthropic_messages_request( + model="claude-sonnet-4-6", + messages=[{"role": "user", "content": "Hello"}], + anthropic_messages_optional_request_params=optional_params, + litellm_params={}, + headers={}, + ) + + assert result.get("output_config") == {"effort": expected_effort} + + +def test_legacy_thinking_does_not_override_explicit_output_config(): + config = AnthropicMessagesConfig() + optional_params = { + "max_tokens": 1024, + "thinking": {"type": "enabled", "budget_tokens": 31999}, + "output_config": {"effort": "low"}, + } + + result = config.transform_anthropic_messages_request( + model="claude-sonnet-4-6", + messages=[{"role": "user", "content": "Hello"}], + anthropic_messages_optional_request_params=optional_params, + litellm_params={}, + headers={}, + ) + + assert result.get("output_config") == {"effort": "low"} + + +def test_legacy_thinking_left_untouched_on_non_adaptive_model(): + config = AnthropicMessagesConfig() + optional_params = { + "max_tokens": 1024, + "thinking": {"type": "enabled", "budget_tokens": 31999}, + } + + result = config.transform_anthropic_messages_request( + model="claude-opus-4-5", + messages=[{"role": "user", "content": "Hello"}], + anthropic_messages_optional_request_params=optional_params, + litellm_params={}, + headers={}, + ) + + assert result.get("thinking") == {"type": "enabled", "budget_tokens": 31999} + assert "output_config" not in result