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.
This commit is contained in:
Mateo Wang
2026-05-29 13:55:06 -07:00
committed by GitHub
parent 68852ef165
commit a55817cbc6
2 changed files with 124 additions and 1 deletions
@@ -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"
@@ -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