diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index ba0d1833952..77bded73b54 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -467,7 +467,6 @@ class AmazonConverseConfig(BaseConfig): max_tokens, ) optional_params.pop("thinking", None) - optional_params.pop("output_config", None) else: optional_params["thinking"] = fitted_thinking if fitted_thinking is not None and AnthropicConfig._is_adaptive_thinking_model(model, "bedrock"): @@ -894,10 +893,14 @@ class AmazonConverseConfig(BaseConfig): drop_params: bool, ) -> dict: is_thinking_enabled: Final = self.is_thinking_enabled(non_default_params) + max_tokens: Final = non_default_params.get("max_tokens") + max_completion_tokens: Final = non_default_params.get("max_completion_tokens") resolved_max_tokens: Final = ( - non_default_params.get("max_completion_tokens") - if non_default_params.get("max_completion_tokens") is not None - else non_default_params.get("max_tokens") + min(max_tokens, max_completion_tokens) + if isinstance(max_tokens, int) and isinstance(max_completion_tokens, int) + else max_completion_tokens + if max_completion_tokens is not None + else max_tokens ) for param, value in non_default_params.items(): diff --git a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py index 2d472ddf3f8..5ffc3ad4979 100644 --- a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py +++ b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py @@ -507,12 +507,12 @@ def test_reasoning_effort_budget_fits_max_tokens_for_anthropic_converse( "params,expected_max_tokens,expects_thinking", [ ( - {"reasoning_effort": "medium", "max_tokens": 1024, "max_completion_tokens": 4096}, + {"reasoning_effort": "medium", "max_tokens": 4096, "max_completion_tokens": 8192}, 4096, True, ), ( - {"reasoning_effort": "medium", "max_completion_tokens": 4096, "max_tokens": 1024}, + {"reasoning_effort": "medium", "max_completion_tokens": 8192, "max_tokens": 4096}, 4096, True, ), @@ -528,10 +528,10 @@ def test_reasoning_effort_budget_fits_max_tokens_for_anthropic_converse( ), ], ) -def test_max_completion_tokens_wins_for_reasoning_effort_anthropic_converse( +def test_restrictive_token_alias_wins_for_reasoning_effort_anthropic_converse( params, expected_max_tokens, expects_thinking ): - """The completion-token alias controls both output and thinking limits regardless of input order.""" + """The smaller output-token alias controls both limits regardless of input order.""" config = AmazonConverseConfig() model = "bedrock/converse/us.anthropic.claude-haiku-4-5-20251001-v1:0" @@ -549,6 +549,35 @@ def test_max_completion_tokens_wins_for_reasoning_effort_anthropic_converse( assert "thinking" not in optional_params +@pytest.mark.parametrize("output_config_first", [True, False]) +def test_unfitting_reasoning_effort_preserves_explicit_output_config(output_config_first): + """Dropping generated thinking must not delete an independent structured-output configuration.""" + config = AmazonConverseConfig() + model = "bedrock/converse/us.anthropic.claude-haiku-4-5-20251001-v1:0" + output_config = {"format": {"type": "json_schema", "schema": {"type": "object"}}} + params = { + "output_config": output_config, + "reasoning_effort": "medium", + "max_tokens": 1024, + } + if not output_config_first: + params = { + "reasoning_effort": "medium", + "max_tokens": 1024, + "output_config": output_config, + } + + optional_params = config.map_openai_params( + non_default_params=params, + optional_params={}, + model=model, + drop_params=False, + ) + + assert "thinking" not in optional_params + assert optional_params["output_config"] == output_config + + def test_reasoning_effort_without_max_tokens_synthesizes_valid_limit(): """Legacy thinking without an output limit receives enough completion-token headroom.""" from litellm.constants import DEFAULT_MAX_TOKENS