diff --git a/litellm/llms/openai/responses/transformation.py b/litellm/llms/openai/responses/transformation.py index 189cc7fe956..926de3e8854 100644 --- a/litellm/llms/openai/responses/transformation.py +++ b/litellm/llms/openai/responses/transformation.py @@ -149,7 +149,17 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): ) except Exception: return True - return info["key"] in _bundled_openai_reasoning_models() or info.get("supports_reasoning") is True + declared: Final = info.get("supports_reasoning") + if declared is not None: + return declared + return info["key"] in _bundled_openai_reasoning_models() + + @staticmethod + def _requests_reasoning_effort(reasoning: object) -> bool: + effort: Final = ( + reasoning.get("effort") if isinstance(reasoning, Mapping) else getattr(reasoning, "effort", None) + ) + return effort is not None @staticmethod def _enforce_min_max_output_tokens(max_output_tokens: "int | None") -> "int | None": @@ -202,7 +212,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): if ( self.custom_llm_provider == LlmProviders.OPENAI - and params.get("reasoning") is not None + and self._requests_reasoning_effort(params.get("reasoning")) and not self._supports_reasoning_param(model=model) ): if drop_params or litellm.drop_params: @@ -210,7 +220,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): else: raise litellm.UnsupportedParamsError( message=( - f"{model} doesn't support the `reasoning` parameter " + f"{model} doesn't support `reasoning.effort` " "(its model cost map entry lacks `supports_reasoning`). " "To drop unsupported params set `litellm.drop_params = True`" ), diff --git a/tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py b/tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py index 66271de7d04..c5902b32a06 100644 --- a/tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py +++ b/tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py @@ -2086,12 +2086,42 @@ class TestReasoningFollowsModelSupport: drop_params=False, ) assert excinfo.value.status_code == 400 + assert "reasoning.effort" in str(excinfo.value) assert "cost map" in str(excinfo.value) - def test_azure_deployments_keep_reasoning(self, local_model_cost_map): + @pytest.mark.parametrize("drop_params", [True, False]) + @pytest.mark.parametrize( + "reasoning", + [{"summary": "auto"}, {"effort": None, "summary": "auto"}, {}], + ) + def test_reasoning_without_an_effort_passes_through_on_non_reasoning_models( + self, local_model_cost_map, monkeypatch, drop_params, reasoning + ): + monkeypatch.setattr(litellm, "drop_params", drop_params) + mapped = OpenAIResponsesAPIConfig().map_openai_params( + response_api_optional_params={"reasoning": dict(reasoning)}, + model="gpt-4o", + drop_params=drop_params, + ) + assert mapped["reasoning"] == reasoning + + def test_an_explicit_supports_reasoning_false_beats_the_bundled_floor(self, local_model_cost_map, monkeypatch): + overridden = { + name: ({**entry, "supports_reasoning": False} if name == "o3" else entry) + for name, entry in litellm.model_cost.items() + } + monkeypatch.setattr(litellm, "model_cost", overridden) + mapped = OpenAIResponsesAPIConfig().map_openai_params( + response_api_optional_params={"reasoning": {"effort": "medium"}}, + model="o3", + drop_params=True, + ) + assert "reasoning" not in mapped + + def test_azure_deployments_keep_reasoning_even_on_a_non_reasoning_model_name(self, local_model_cost_map): mapped = AzureOpenAIResponsesAPIConfig().map_openai_params( response_api_optional_params={"reasoning": {"effort": "medium"}}, - model="my-o3-deployment", + model="gpt-4o", drop_params=True, ) assert mapped["reasoning"] == {"effort": "medium"}