diff --git a/litellm/llms/anthropic/messages/adapters/transformation.py b/litellm/llms/anthropic/messages/adapters/transformation.py index 0636532e092..15843d19f49 100644 --- a/litellm/llms/anthropic/messages/adapters/transformation.py +++ b/litellm/llms/anthropic/messages/adapters/transformation.py @@ -16,6 +16,7 @@ from litellm.llms.anthropic.messages.utils import ( json_copy_or_original, prompt_cache_key_from_user_id, ) +from litellm.llms.bedrock.common_utils import is_bedrock_application_inference_profile_arn from litellm.responses.utils import auto_reasoning_summary # OpenAI has a 64-character limit for function/tool names @@ -366,6 +367,11 @@ _BlockT: Final = TypeVar("_BlockT", bound=Mapping[str, object]) _CLAUDE_OVER_REASONING_EFFORT_PROVIDERS: Final = frozenset({LlmProviders.KYMA.value}) +UNRESOLVED_ARN_EFFORT_WITHOUT_THINKING_WARNING: Final = ( + "Dropping `output_config` for model=%s under drop_params: an application inference profile ARN does not name " + "its model, so LiteLLM cannot tell whether it takes an effort without a `thinking` block. Turn off drop_params " + "on the deployment to send it." +) class LiteLLMAnthropicMessagesAdapter: @@ -1203,6 +1209,7 @@ class LiteLLMAnthropicMessagesAdapter: self, anthropic_message_request: AnthropicMessagesRequest, new_kwargs: ChatCompletionRequest, + drop_params: bool, ) -> None: """Forward the effort-bearing part of ``output_config`` to a Bedrock Claude target. @@ -1218,8 +1225,17 @@ class LiteLLMAnthropicMessagesAdapter: return effort_config: Final = {k: v for k, v in output_config.items() if k != "format"} - if effort_config: - new_kwargs["output_config"] = effort_config # rebind-ok: out-param store like its sibling translators + if not effort_config: + return + model: Final = new_kwargs["model"] + if ( + drop_params + and not anthropic_message_request.get("thinking") + and is_bedrock_application_inference_profile_arn(model) + ): + litellm.verbose_logger.warning(UNRESOLVED_ARN_EFFORT_WITHOUT_THINKING_WARNING, model) + return + new_kwargs["output_config"] = effort_config # rebind-ok: out-param store like its sibling translators def _translate_output_format_to_openai( self, @@ -1324,6 +1340,7 @@ class LiteLLMAnthropicMessagesAdapter: self._translate_output_config_to_openai( anthropic_message_request=anthropic_message_request, new_kwargs=new_kwargs, + drop_params=drop_params, ) ## CONVERT STOP_SEQUENCES self._translate_stop_sequences_to_openai( diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index 7729b3fc7d2..e0474df4c3b 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -142,11 +142,6 @@ ADAPTIVE_THINKING_ON_UNRESOLVED_ARN_WARNING: Final = ( "does not name its model, so LiteLLM cannot tell whether it supports adaptive thinking. Set `base_model` on " "the deployment (for example `us.anthropic.claude-opus-4-8`) to send adaptive thinking as requested." ) -UNRESOLVED_ARN_OUTPUT_CONFIG_WARNING: Final = ( - "Dropping `output_config` for model=%s under drop_params: an application inference profile ARN does not name " - "its model, so LiteLLM cannot tell whether it takes an effort. Set `base_model` on the deployment (for example " - "`us.anthropic.claude-opus-4-8`) to send it." -) DROP_UNSUPPORTED_EFFORT_WARNING: Final = "Dropping `output_config.effort` for model=%s under drop_params: %s" # Computer use tool prefixes supported by Bedrock @@ -583,7 +578,7 @@ class AmazonConverseConfig(BaseConfig): optional_params["_output_config_normalized"] = True @staticmethod - def _anthropic_adaptive_effort_error(model: str, effort: object) -> str | None: + def _anthropic_adaptive_effort_error(model: str, effort: object, checks_tier: bool) -> str | None: """Why ``output_config.effort`` is not valid on ``model``, or None when it is.""" valid_efforts: Final = {"high", "medium", "low", "xhigh", "max"} if not isinstance(effort, str) or effort not in valid_efforts: @@ -592,12 +587,14 @@ class AmazonConverseConfig(BaseConfig): f"{effort!r}. Must be one of: 'low', 'medium', 'high', " f"'xhigh', or 'max'." ) + if not checks_tier: + return None return AnthropicConfig.validate_effort_for_model(model=model, effort=effort, custom_llm_provider="bedrock") @staticmethod def _validate_anthropic_adaptive_effort(model: str, effort: str) -> None: """Validate ``output_config.effort`` for adaptive-thinking Claude 4.6/4.7.""" - error: Final = AmazonConverseConfig._anthropic_adaptive_effort_error(model, effort) + error: Final = AmazonConverseConfig._anthropic_adaptive_effort_error(model, effort, checks_tier=True) if error is not None: raise litellm.exceptions.BadRequestError( message=error, @@ -614,15 +611,11 @@ class AmazonConverseConfig(BaseConfig): drops_unsupported_effort: bool, ) -> Mapping[str, object] | None: if is_bedrock_application_inference_profile_arn(model): - if not drops_unsupported_effort: - return output_config - litellm.verbose_logger.warning(UNRESOLVED_ARN_OUTPUT_CONFIG_WARNING, model) - return None + return output_config if not base_model.startswith("anthropic"): return None - model_takes_effort: Final = AnthropicConfig.model_supports_effort_param( - model, "bedrock" - ) or AnthropicConfig.is_adaptive_thinking_model(model, "bedrock") + model_lists_effort: Final = AnthropicConfig.model_supports_effort_param(model, "bedrock") + model_takes_effort: Final = model_lists_effort or AnthropicConfig.is_adaptive_thinking_model(model, "bedrock") if drops_unsupported_effort and not model_takes_effort: litellm.verbose_logger.warning(DROP_UNSUPPORTED_OUTPUT_CONFIG_WARNING, model) return None @@ -631,7 +624,9 @@ class AmazonConverseConfig(BaseConfig): normalize_bedrock_opus_output_config_effort(model=model, output_config=normalized_output_config) effort: Final = normalized_output_config.get("effort") effort_error: Final = ( - None if effort is None else AmazonConverseConfig._anthropic_adaptive_effort_error(model, effort) + None + if effort is None + else AmazonConverseConfig._anthropic_adaptive_effort_error(model, effort, checks_tier=model_lists_effort) ) if effort_error is None: return normalized_output_config diff --git a/litellm/llms/bedrock/common_utils.py b/litellm/llms/bedrock/common_utils.py index 4175479f068..e322f926bfd 100644 --- a/litellm/llms/bedrock/common_utils.py +++ b/litellm/llms/bedrock/common_utils.py @@ -1634,7 +1634,7 @@ def normalize_bedrock_opus_output_config_effort(model: str, output_config: objec return effort: Final = output_config.get("effort") - if effort not in _BEDROCK_OUTPUT_CONFIG_EFFORT_ORDER: + if not isinstance(effort, str) or effort not in _BEDROCK_OUTPUT_CONFIG_EFFORT_ORDER: return ceiling: Final = _get_bedrock_output_config_effort_ceiling(model) diff --git a/tests/unit/llms/anthropic/messages/adapters/test_anthropic_messages_adapters_transformation.py b/tests/unit/llms/anthropic/messages/adapters/test_anthropic_messages_adapters_transformation.py index 74ba574b2e5..8256fa034b6 100644 --- a/tests/unit/llms/anthropic/messages/adapters/test_anthropic_messages_adapters_transformation.py +++ b/tests/unit/llms/anthropic/messages/adapters/test_anthropic_messages_adapters_transformation.py @@ -5553,6 +5553,39 @@ async def test_bedrock_converse_claude_without_effort_support_drops_the_effort_o assert ("output_config" in _sent_converse_fields(_converse_route)) is forwards_effort +_APPLICATION_INFERENCE_PROFILE: Final = ( + "arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/abcdef123456" +) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("thinking", "drop_params", "forwards_effort"), + [ + pytest.param(None, False, True, id="no_thinking"), + pytest.param(None, True, False, id="no_thinking_drop_params"), + pytest.param({"type": "enabled", "budget_tokens": 1024}, True, True, id="thinking_drop_params"), + ], +) +async def test_bedrock_application_inference_profile_loses_the_effort_only_without_thinking_under_drop_params( + _converse_route: respx.Route, + thinking: Mapping[str, object] | None, + drop_params: bool, + forwards_effort: bool, +) -> None: + await litellm.anthropic.messages.acreate( + model=f"bedrock/converse/{_APPLICATION_INFERENCE_PROFILE}", + max_tokens=2048, + messages=[{"role": "user", "content": "Name the capital of France."}], + output_config={"effort": "high", "format": _CITY_FORMAT}, + drop_params=drop_params, + **({"thinking": thinking} if thinking else {}), + ) + + sent: Final = _sent_converse_fields(_converse_route) + assert sent.get("output_config") == ({"effort": "high"} if forwards_effort else None) + + @pytest.mark.parametrize( "output_config", [pytest.param(None, id="null"), pytest.param({"format": _CITY_FORMAT}, id="format_only")], diff --git a/tests/unit/llms/bedrock/chat/test_converse_transformation.py b/tests/unit/llms/bedrock/chat/test_converse_transformation.py index 8d809b33020..6377da481b9 100644 --- a/tests/unit/llms/bedrock/chat/test_converse_transformation.py +++ b/tests/unit/llms/bedrock/chat/test_converse_transformation.py @@ -5581,13 +5581,13 @@ def _converse_output_config(model: str, output_config: dict, litellm_params: dic return result.get("additionalModelRequestFields", {}).get("output_config") -@pytest.mark.parametrize("effort", ["xhigh", "turbo"]) +@pytest.mark.parametrize("effort", ["xhigh", "turbo", ["high"]]) def test_converse_refuses_an_effort_the_model_cannot_take_without_drop_params(local_model_cost_map, effort): with pytest.raises(litellm.BadRequestError, match="effort"): _converse_output_config("us.anthropic.claude-sonnet-4-6", {"effort": effort}, {}) -@pytest.mark.parametrize("effort", ["xhigh", "turbo"]) +@pytest.mark.parametrize("effort", ["xhigh", "turbo", ["high"]]) def test_converse_drops_an_effort_the_model_cannot_take_under_deployment_drop_params( local_model_cost_map, caplog, effort ): @@ -5600,9 +5600,11 @@ def test_converse_drops_an_effort_the_model_cannot_take_under_deployment_drop_pa assert any("output_config.effort" in record.getMessage() for record in caplog.records) -def test_converse_forwards_an_unresolved_arn_effort_only_without_drop_params(local_model_cost_map): - assert _converse_output_config(_APPLICATION_INFERENCE_PROFILE_ARN, {"effort": "high"}, {}) == {"effort": "high"} - assert _converse_output_config(_APPLICATION_INFERENCE_PROFILE_ARN, {"effort": "high"}, {"drop_params": True}) is None +@pytest.mark.parametrize("litellm_params", [{}, {"drop_params": True}]) +def test_converse_forwards_an_unresolved_arn_effort_verbatim(local_model_cost_map, litellm_params): + sent: Final = _converse_output_config(_APPLICATION_INFERENCE_PROFILE_ARN, {"effort": "high"}, litellm_params) + + assert sent == {"effort": "high"} @pytest.mark.parametrize( @@ -5625,11 +5627,15 @@ def test_converse_refuses_a_reasoning_effort_tier_the_model_cannot_take(local_mo ) -def test_converse_keeps_the_effort_for_a_claude_id_the_cost_map_does_not_list_yet(local_model_cost_map): +@pytest.mark.parametrize("effort", ["high", "xhigh", "max"]) +@pytest.mark.parametrize("litellm_params", [{}, {"drop_params": True}]) +def test_converse_keeps_the_effort_for_a_claude_id_the_cost_map_does_not_list_yet( + local_model_cost_map, effort, litellm_params +): model: Final = "us.anthropic.claude-opus-9-9-v1:0" assert model not in litellm.model_cost - assert _converse_output_config(model, {"effort": "high"}, {"drop_params": True}) == {"effort": "high"} + assert _converse_output_config(model, {"effort": effort}, litellm_params) == {"effort": effort} def test_transform_response_native_structured_output():