diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index 9f094521bfe..29347f6554a 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -104,6 +104,7 @@ from ..common_utils import ( BedrockModelInfo, bedrock_converse_supports_parallel_tool_use_config, bedrock_model_accepts_cache_points, + bedrock_reasoning_effort_disabled, get_anthropic_beta_from_headers, get_bedrock_tool_name, is_bedrock_application_inference_profile_arn, @@ -1135,6 +1136,25 @@ class AmazonConverseConfig(BaseConfig): "Dropping unsupported `reasoning_effort` param for Bedrock model=%s; it always reasons and rejects it.", model, ) + elif ( + param == "reasoning_effort" + and isinstance(value, str) + and self._is_openai_gpt_reasoning_model(model) + and bedrock_reasoning_effort_disabled(model=model, effort=value) + ): + if not (litellm.drop_params or drop_params): + raise litellm.utils.UnsupportedParamsError( + message=( + f"{model} does not support reasoning_effort={value}. " + "To drop unsupported params, set `litellm.drop_params = True`." + ), + status_code=400, + ) + verbose_logger.debug( + "Dropping unsupported `reasoning_effort=%s` for Bedrock model=%s.", + value, + model, + ) elif param == "reasoning_effort" and isinstance(value, str): self._handle_reasoning_effort_parameter( model=model, reasoning_effort=value, optional_params=optional_params diff --git a/litellm/llms/bedrock/common_utils.py b/litellm/llms/bedrock/common_utils.py index 2ab506771ec..12d04a08392 100644 --- a/litellm/llms/bedrock/common_utils.py +++ b/litellm/llms/bedrock/common_utils.py @@ -1017,6 +1017,14 @@ def _mantle_api_base_from_env() -> str | None: return next((base[: -len(suffix)] for suffix in _MANTLE_OPENAI_BASE_SUFFIXES if base.endswith(suffix)), base) +def bedrock_reasoning_effort_disabled(model: str, effort: str) -> bool: + from litellm.utils import is_explicitly_disabled_factory + + return is_explicitly_disabled_factory( + model=model, custom_llm_provider="bedrock_converse", key=f"supports_{effort}_reasoning_effort" + ) + + def bedrock_supports_openai_responses(model: str | None, model_cost: Mapping[str, object]) -> bool: """Whether a Bedrock model is served by bedrock-runtime's OpenAI Responses surface. diff --git a/litellm/llms/bedrock/responses/transformation.py b/litellm/llms/bedrock/responses/transformation.py index b56069c79a6..086d1211835 100644 --- a/litellm/llms/bedrock/responses/transformation.py +++ b/litellm/llms/bedrock/responses/transformation.py @@ -52,6 +52,7 @@ from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM from litellm.llms.bedrock.common_utils import ( BEDROCK_CHAT_COMPLETIONS_ROUTE_PREFIX, BedrockError, + bedrock_reasoning_effort_disabled, bedrock_supports_openai_responses, ) from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig @@ -154,6 +155,29 @@ def inline_remote_image_urls( return items # pyright: ignore[reportReturnType] # items keep the caller's input union +def _without_disabled_reasoning_effort( + params: Mapping[str, object], model: str, drop_params: bool +) -> dict[str, object]: # mutable-ok: becomes the map_openai_params return value + reasoning: Final = params.get("reasoning") + effort: Final = reasoning.get("effort") if isinstance(reasoning, Mapping) else None + if not isinstance(reasoning, Mapping) or not isinstance(effort, str): + return dict(params) + if not bedrock_reasoning_effort_disabled(model=model, effort=effort): + return dict(params) + if not (drop_params or litellm.drop_params): + raise litellm.UnsupportedParamsError( + message=( + f"{model} does not support reasoning.effort={effort}. " + "To drop unsupported params, set `litellm.drop_params = True`." + ), + status_code=400, + ) + verbose_logger.debug("Dropping unsupported `reasoning.effort=%s` for Bedrock model=%s.", effort, model) + rest: Final = {key: value for key, value in reasoning.items() if key != "effort"} + without_reasoning: Final = {key: value for key, value in params.items() if key != "reasoning"} + return {**without_reasoning, "reasoning": rest} if rest else without_reasoning + + class BedrockOpenAIResponsesConfig(BaseAWSLLM, OpenAIResponsesAPIConfig): """Responses API config for the OpenAI models on the bedrock-runtime endpoint.""" @@ -270,7 +294,8 @@ class BedrockOpenAIResponsesConfig(BaseAWSLLM, OpenAIResponsesAPIConfig): "Bedrock Runtime Responses API: dropping unsupported parameter(s) %s that the endpoint rejects.", unsupported, ) - params: Final = {key: value for key, value in mapped.items() if key not in unsupported} + supported: Final[dict[str, object]] = {key: value for key, value in mapped.items() if key not in unsupported} + params: Final = _without_disabled_reasoning_effort(supported, model, drop_params) tools: Final = params.get("tools") if not isinstance(tools, list): return params diff --git a/tests/unit/llms/bedrock/chat/test_converse_transformation.py b/tests/unit/llms/bedrock/chat/test_converse_transformation.py index 0c0be5d868f..07c54eee395 100644 --- a/tests/unit/llms/bedrock/chat/test_converse_transformation.py +++ b/tests/unit/llms/bedrock/chat/test_converse_transformation.py @@ -520,6 +520,39 @@ def test_reasoning_effort_maps_to_reasoning_effort_for_openai_gpt5_converse(mode assert "thinking" not in additional_request_params +@pytest.mark.parametrize( + "model", + [ + "us.openai.gpt-5.6-luna", + "bedrock/converse/global.openai.gpt-5.6-terra", + "us.openai.gpt-6-astra", + ], +) +def test_openai_gpt5_converse_rejects_effort_level_disabled_in_model_map(model, local_model_cost_map): + config = AmazonConverseConfig() + assert litellm.utils.is_explicitly_disabled_factory( + model=model, custom_llm_provider="bedrock_converse", key="supports_minimal_reasoning_effort" + ) + + with pytest.raises(litellm.utils.UnsupportedParamsError, match="minimal"): + config.map_openai_params( + non_default_params={"reasoning_effort": "minimal"}, + optional_params={}, + model=model, + drop_params=False, + ) + + optional_params = config.map_openai_params( + non_default_params={"reasoning_effort": "minimal"}, + optional_params={}, + model=model, + drop_params=True, + ) + _, additional_request_params, _, _ = config._prepare_request_params(optional_params, model) + assert "reasoning" not in additional_request_params + assert "thinking" not in additional_request_params + + @pytest.mark.parametrize( "model", [ diff --git a/tests/unit/llms/bedrock/responses/test_bedrock_openai_responses.py b/tests/unit/llms/bedrock/responses/test_bedrock_openai_responses.py index c0803f6636b..6da131f38cc 100644 --- a/tests/unit/llms/bedrock/responses/test_bedrock_openai_responses.py +++ b/tests/unit/llms/bedrock/responses/test_bedrock_openai_responses.py @@ -328,6 +328,36 @@ class TestBackgroundDrop: assert not [r for r in caplog.records if "dropping unsupported parameter" in r.getMessage()] +class TestDisabledReasoningEffort: + @pytest.mark.parametrize("model", ["us.openai.gpt-5.6-luna", MODEL]) + def test_effort_level_disabled_in_model_map_is_rejected(self, model, local_model_cost_map): + with pytest.raises(litellm.UnsupportedParamsError, match="minimal"): + _cfg().map_openai_params( + response_api_optional_params={"reasoning": {"effort": "minimal"}}, model=model, drop_params=False + ) + + @pytest.mark.parametrize("model", ["us.openai.gpt-5.6-luna", MODEL]) + def test_effort_level_disabled_in_model_map_is_dropped_with_drop_params(self, model, local_model_cost_map): + params = _cfg().map_openai_params( + response_api_optional_params={"reasoning": {"effort": "minimal", "summary": "auto"}, "max_output_tokens": 64}, + model=model, + drop_params=True, + ) + assert params == {"reasoning": {"summary": "auto"}, "max_output_tokens": 64} + + def test_effort_only_reasoning_is_removed_when_dropped(self, local_model_cost_map): + params = _cfg().map_openai_params( + response_api_optional_params={"reasoning": {"effort": "minimal"}}, model=MODEL, drop_params=True + ) + assert params == {} + + def test_supported_effort_level_is_forwarded(self, local_model_cost_map): + params = _cfg().map_openai_params( + response_api_optional_params={"reasoning": {"effort": "low"}}, model=MODEL, drop_params=False + ) + assert params == {"reasoning": {"effort": "low"}} + + def _never_fetch(url: str) -> str: raise AssertionError(f"unexpected sync fetch of {url}")