diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py index 35495d59610..8fc76886008 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py @@ -212,6 +212,11 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): ), status_code=400, ) + mapped_effort = ( + AnthropicMessagesConfig._normalize_xhigh_effort_for_max_model( + model, mapped_effort + ) + ) gate_error = AnthropicConfig._validate_effort_for_model( model, mapped_effort ) @@ -223,6 +228,35 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): existing_output_config.setdefault("effort", mapped_effort) optional_params["output_config"] = existing_output_config + @staticmethod + def _normalize_xhigh_effort_for_max_model(model: str, effort: str) -> str: + """Return ``max`` when a model supports max effort but not xhigh.""" + from litellm.llms.anthropic.chat.transformation import AnthropicConfig + + if effort != "xhigh": + return effort + if AnthropicConfig._supports_effort_level(model, "xhigh"): + return effort + if AnthropicConfig._supports_effort_level(model, "max"): + return "max" + return effort + + @staticmethod + def _normalize_explicit_output_config_xhigh_effort( + model: str, optional_params: Dict + ) -> None: + """Normalize explicit ``output_config.effort=xhigh`` when ``max`` is supported.""" + output_config = optional_params.get("output_config") + if not isinstance(output_config, dict): + return + effort = output_config.get("effort") + if not isinstance(effort, str): + return + + output_config["effort"] = ( + AnthropicMessagesConfig._normalize_xhigh_effort_for_max_model(model, effort) + ) + @staticmethod def _translate_legacy_thinking_for_adaptive_model( model: str, optional_params: Dict @@ -274,6 +308,11 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): status_code=400, ) + self._normalize_explicit_output_config_xhigh_effort( + model=model, + optional_params=anthropic_messages_optional_request_params, + ) + self._translate_reasoning_effort_to_anthropic( model=model, optional_params=anthropic_messages_optional_request_params, 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 83716b8c8d3..50e4c4bead6 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 @@ -99,28 +99,27 @@ def test_invalid_reasoning_effort_raises_400(bad_effort): @pytest.mark.parametrize( - "model,bad_effort", + "model", [ - ("claude-opus-4-6", "xhigh"), - ("bedrock/invoke/us.anthropic.claude-opus-4-6-v1", "xhigh"), - ("claude-sonnet-4-6", "xhigh"), + "claude-opus-4-6", + "bedrock/invoke/us.anthropic.claude-opus-4-6-v1", + "claude-sonnet-4-6", ], ) -def test_reasoning_effort_unsupported_tier_raises_400_messages(model, bad_effort): +def test_reasoning_effort_xhigh_normalizes_to_max_messages(model): config = AnthropicMessagesConfig() - optional_params = {"max_tokens": 1024, "reasoning_effort": bad_effort} + optional_params = {"max_tokens": 1024, "reasoning_effort": "xhigh"} - with pytest.raises(AnthropicError) as exc_info: - config.transform_anthropic_messages_request( - model=model, - messages=[{"role": "user", "content": "Hello"}], - anthropic_messages_optional_request_params=optional_params, - litellm_params={}, - headers={}, - ) + result = config.transform_anthropic_messages_request( + model=model, + messages=[{"role": "user", "content": "Hello"}], + anthropic_messages_optional_request_params=optional_params, + litellm_params={}, + headers={}, + ) - assert exc_info.value.status_code == 400 - assert "not supported by this model" in str(exc_info.value) + output_config = result.get("output_config") + assert isinstance(output_config, dict) and output_config.get("effort") == "max" @pytest.mark.parametrize( @@ -146,6 +145,43 @@ def test_reasoning_effort_max_accepted_on_sonnet_46_messages(model): assert isinstance(output_config, dict) and output_config.get("effort") == "max" +@pytest.mark.parametrize( + "model", + [ + "claude-sonnet-4-6", + "bedrock/invoke/us.anthropic.claude-sonnet-4-6", + ], +) +def test_explicit_output_config_xhigh_normalizes_to_max_messages(model): + config = AnthropicMessagesConfig() + optional_params = {"max_tokens": 1024, "output_config": {"effort": "xhigh"}} + + 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("output_config") == {"effort": "max"} + + +def test_explicit_output_config_xhigh_preserved_when_supported_messages(): + config = AnthropicMessagesConfig() + optional_params = {"max_tokens": 1024, "output_config": {"effort": "xhigh"}} + + 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("output_config") == {"effort": "xhigh"} + + def test_explicit_output_config_wins_over_reasoning_effort(): config = AnthropicMessagesConfig() optional_params = {