From 812f0054a404fbf1b1fa1ced7311a3ef4612a1f2 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:44:47 -0700 Subject: [PATCH] fix(reasoning): suggest reasoning_effort_levels and base_model only where the route reads them The chat gate suggested model_info.reasoning_effort_levels on routes that never pass declared levels (github_copilot, chatgpt and other OpenAIConfig delegates) and model_info.base_model on OpenAI and Azure AI chat, whose gate looks up the deployment name rather than base_model. /v1/responses/compact suggested levels it never reads either. Declared levels are now None when a caller passes none, so the advice appears only when a caller fed them, and the base_model advice is limited to the Azure chat gate, the one that looks up base_model. The openai row of the chat warning test now expects no base_model advice, the Responses warning test goes through get_optional_params_responses_api the way /v1/responses and compact call it, and the Azure direct-config test passes declared levels the way get_optional_params does. Long test lines are wrapped. --- .../chat/gpt_reasoning_transformation.py | 3 ++- litellm/llms/azure_ai/chat/transformation.py | 2 +- .../chat/gpt_reasoning_transformation.py | 10 ++++--- .../llms/openai/responses/transformation.py | 8 +++--- litellm/responses/utils.py | 6 ++--- ...test_azure_gpt_reasoning_transformation.py | 1 + .../test_bedrock_openai_responses.py | 5 +++- ...bedrock_mantle_responses_transformation.py | 4 ++- .../test_openai_responses_transformation.py | 26 +++++++++++++------ .../test_gpt_reasoning_transformation.py | 25 +++++++++++++----- 10 files changed, 60 insertions(+), 30 deletions(-) diff --git a/litellm/llms/azure/chat/gpt_reasoning_transformation.py b/litellm/llms/azure/chat/gpt_reasoning_transformation.py index 5bd03f511c5..72a0500e0af 100644 --- a/litellm/llms/azure/chat/gpt_reasoning_transformation.py +++ b/litellm/llms/azure/chat/gpt_reasoning_transformation.py @@ -24,6 +24,7 @@ class AzureOpenAIGPT5Config(AzureOpenAIConfig, OpenAIGPT5Config): GPT5_SERIES_ROUTE = "gpt5_series/" OPT_IN_REASONING_EFFORTS: ClassVar[frozenset[str]] = frozenset(("xhigh", "none")) + LOOKS_UP_BASE_MODEL: ClassVar[bool] = True @classmethod def _model_map_lookup_name(cls, model: str) -> str: @@ -88,7 +89,7 @@ class AzureOpenAIGPT5Config(AzureOpenAIConfig, OpenAIGPT5Config): drop_params: bool, api_version: str = "", *, - declared_reasoning_efforts: Collection[str] = (), + declared_reasoning_efforts: Collection[str] | None = None, ) -> dict: return OpenAIGPT5Config.map_openai_params( self, diff --git a/litellm/llms/azure_ai/chat/transformation.py b/litellm/llms/azure_ai/chat/transformation.py index 6aed7eb3c54..8bb6072d0a9 100644 --- a/litellm/llms/azure_ai/chat/transformation.py +++ b/litellm/llms/azure_ai/chat/transformation.py @@ -106,7 +106,7 @@ class AzureAIStudioConfig(OpenAIConfig): model: str, drop_params: bool, *, - declared_reasoning_efforts: Collection[str] = (), + declared_reasoning_efforts: Collection[str] | None = None, ) -> dict[str, object]: # mutable-ok: OpenAIConfig.map_openai_params signature if not azureAIGPT5Config.is_model_gpt_5_model(model): return super().map_openai_params( diff --git a/litellm/llms/openai/chat/gpt_reasoning_transformation.py b/litellm/llms/openai/chat/gpt_reasoning_transformation.py index 151db5d6887..8c9551d01c0 100644 --- a/litellm/llms/openai/chat/gpt_reasoning_transformation.py +++ b/litellm/llms/openai/chat/gpt_reasoning_transformation.py @@ -114,6 +114,7 @@ class OpenAIGPT5Config(OpenAIGPTConfig): OPT_IN_REASONING_EFFORTS: ClassVar[frozenset[str]] = frozenset(("xhigh",)) ROUTE_SPECIFIC_REASONING_EFFORTS: ClassVar[frozenset[str]] = frozenset(("max",)) + LOOKS_UP_BASE_MODEL: ClassVar[bool] = False @classmethod def is_model_gpt_5_model(cls, model: str) -> bool: @@ -328,7 +329,7 @@ class OpenAIGPT5Config(OpenAIGPTConfig): model: str, drop_params: bool, *, - declared_reasoning_efforts: Collection[str] = (), + declared_reasoning_efforts: Collection[str] | None = None, ) -> dict: if self.is_model_gpt_5_search_model(model): if "max_tokens" in non_default_params: @@ -359,14 +360,15 @@ class OpenAIGPT5Config(OpenAIGPTConfig): if ( requested_effort is not None - and requested_effort not in declared_reasoning_efforts + and requested_effort not in (declared_reasoning_efforts or ()) and self.refuses_reasoning_effort_level(model, requested_effort) ): if litellm.drop_params or drop_params: verbose_logger.warning( refused_reasoning_effort_warning( - suggest_declared_levels=True, - suggest_base_model=not has_catalog_entry(self._model_map_lookup_name(model), None), + suggest_declared_levels=declared_reasoning_efforts is not None, + suggest_base_model=self.LOOKS_UP_BASE_MODEL + and not has_catalog_entry(self._model_map_lookup_name(model), None), ), "reasoning_effort", requested_effort, diff --git a/litellm/llms/openai/responses/transformation.py b/litellm/llms/openai/responses/transformation.py index 21f19e8175c..07ba095c50c 100644 --- a/litellm/llms/openai/responses/transformation.py +++ b/litellm/llms/openai/responses/transformation.py @@ -382,7 +382,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): ) -> dict: return dict( self.map_openai_params_for_deployment( - response_api_optional_params, model, drop_params, declared_reasoning_efforts=() + response_api_optional_params, model, drop_params, declared_reasoning_efforts=None ) ) @@ -395,7 +395,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): model: str, drop_params: bool, *, - declared_reasoning_efforts: Collection[str], + declared_reasoning_efforts: Collection[str] | None, ) -> Mapping[str, object]: """No mapping applied since inputs are in OpenAI spec already. @@ -435,7 +435,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): if ( isinstance(reasoning, dict) and isinstance(requested_effort, str) - and requested_effort not in declared_reasoning_efforts + and requested_effort not in (declared_reasoning_efforts or ()) and self._effort_level_is_disabled(lookup_name, requested_effort) ): if not (drop_params or litellm.drop_params): @@ -445,7 +445,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): ) verbose_logger.warning( refused_reasoning_effort_warning( - suggest_declared_levels=self.honors_declared_reasoning_efforts(), suggest_base_model=False + suggest_declared_levels=declared_reasoning_efforts is not None, suggest_base_model=False ), "reasoning.effort", requested_effort, diff --git a/litellm/responses/utils.py b/litellm/responses/utils.py index 25f18a760d0..159d3ab8a65 100644 --- a/litellm/responses/utils.py +++ b/litellm/responses/utils.py @@ -127,12 +127,12 @@ def _provider_mapped_params( params: ResponsesAPIOptionalRequestParams, model: str, drop_params: bool, - declared_reasoning_efforts: Collection[str], + declared_reasoning_efforts: Collection[str] | None, ) -> Mapping[str, object]: from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig if ( - declared_reasoning_efforts + declared_reasoning_efforts is not None and isinstance(config, OpenAIResponsesAPIConfig) and config.honors_declared_reasoning_efforts() ): @@ -283,7 +283,7 @@ class ResponsesAPIRequestUtils: response_api_optional_params: ResponsesAPIOptionalRequestParams, allowed_openai_params: list[str] | None = None, drop_params: bool | None = None, - declared_reasoning_efforts: Collection[str] = (), + declared_reasoning_efforts: Collection[str] | None = None, ) -> dict: """ Get optional parameters for the responses API. diff --git a/tests/unit/llms/azure/chat/test_azure_gpt_reasoning_transformation.py b/tests/unit/llms/azure/chat/test_azure_gpt_reasoning_transformation.py index 1d5d0c808de..2d9381ec16a 100644 --- a/tests/unit/llms/azure/chat/test_azure_gpt_reasoning_transformation.py +++ b/tests/unit/llms/azure/chat/test_azure_gpt_reasoning_transformation.py @@ -291,6 +291,7 @@ def test_azure_dropped_effort_warning_suggests_base_model_only_for_a_name_the_ma model=model, drop_params=True, api_version="2025-04-01-preview", + declared_reasoning_efforts=(), ) assert "reasoning_effort" not in params 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 c598abc86e5..a1466cfdae4 100644 --- a/tests/unit/llms/bedrock/responses/test_bedrock_openai_responses.py +++ b/tests/unit/llms/bedrock/responses/test_bedrock_openai_responses.py @@ -14,7 +14,10 @@ import pytest import litellm from litellm.llms.bedrock.common_utils import bedrock_supports_openai_responses -from litellm.llms.bedrock.responses.transformation import BedrockOpenAIResponsesConfig, _without_disabled_reasoning_effort +from litellm.llms.bedrock.responses.transformation import ( + BedrockOpenAIResponsesConfig, + _without_disabled_reasoning_effort, +) from litellm.responses.file_search.emulated_handler import should_use_emulated_file_search from litellm.types.router import GenericLiteLLMParams from litellm.types.utils import LlmProviders diff --git a/tests/unit/llms/bedrock_mantle/test_bedrock_mantle_responses_transformation.py b/tests/unit/llms/bedrock_mantle/test_bedrock_mantle_responses_transformation.py index 03b57d0c318..0ae6a73968d 100644 --- a/tests/unit/llms/bedrock_mantle/test_bedrock_mantle_responses_transformation.py +++ b/tests/unit/llms/bedrock_mantle/test_bedrock_mantle_responses_transformation.py @@ -591,7 +591,9 @@ class TestBedrockMantleRefusedEffort: assert "reasoning" not in params drop_warnings: Final = [ - record.getMessage() for record in caplog.records if f"Dropping reasoning.effort={refused!r}" in record.getMessage() + record.getMessage() + for record in caplog.records + if f"Dropping reasoning.effort={refused!r}" in record.getMessage() ] assert len(drop_warnings) == 1 assert "model_info" not in drop_warnings[0] diff --git a/tests/unit/llms/openai/responses/test_openai_responses_transformation.py b/tests/unit/llms/openai/responses/test_openai_responses_transformation.py index d48617536b4..a528b1a7b75 100644 --- a/tests/unit/llms/openai/responses/test_openai_responses_transformation.py +++ b/tests/unit/llms/openai/responses/test_openai_responses_transformation.py @@ -16,6 +16,7 @@ from litellm.llms.azure.responses.transformation import AzureOpenAIResponsesAPIC from litellm.llms.base_llm.responses.transformation import BaseResponsesAPIConfig from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig from litellm.responses.litellm_completion_transformation.transformation import LiteLLMCompletionResponsesConfig +from litellm.responses.utils import ResponsesAPIRequestUtils from litellm.types.llms.openai import ( ImageGenerationPartialImageEvent, IncompleteDetails, @@ -2388,8 +2389,17 @@ class TestResponsesSurfaceSharesTheEffortRule: ) assert mapped["reasoning"] == {"summary": "auto"} - def test_dropping_a_refused_effort_logs_one_warning_naming_the_level_and_the_fix( - self, local_model_cost_map: None, caplog: pytest.LogCaptureFixture + @pytest.mark.parametrize( + ("declared_reasoning_efforts", "suggests_declared_levels"), + [((), True), (None, False)], + ids=["responses route reads model_info levels", "compact route reads none"], + ) + def test_dropping_a_refused_effort_logs_one_warning_naming_only_the_fix_its_route_honors( + self, + local_model_cost_map: None, + caplog: pytest.LogCaptureFixture, + declared_reasoning_efforts: tuple[str, ...] | None, + suggests_declared_levels: bool, ) -> None: refused: Final = next( level @@ -2397,19 +2407,19 @@ class TestResponsesSurfaceSharesTheEffortRule: if litellm.model_cost["gpt-6.1-sol"].get(f"supports_{level}_reasoning_effort") is False ) with caplog.at_level(logging.WARNING, logger="LiteLLM"): - mapped: Final = OpenAIResponsesAPIConfig().map_openai_params( - response_api_optional_params={"reasoning": {"effort": refused}}, + mapped: Final = ResponsesAPIRequestUtils.get_optional_params_responses_api( model="gpt-6.1-sol", + responses_api_provider_config=OpenAIResponsesAPIConfig(), + response_api_optional_params={"reasoning": {"effort": refused}}, drop_params=True, + declared_reasoning_efforts=declared_reasoning_efforts, ) assert "reasoning" not in mapped drop_warnings: Final = [record.getMessage() for record in caplog.records if "Dropping" in record.getMessage()] assert len(drop_warnings) == 1 - assert all( - fragment in drop_warnings[0] - for fragment in (repr(refused), "gpt-6.1-sol", "model_info.reasoning_effort_levels") - ) + assert repr(refused) in drop_warnings[0] and "gpt-6.1-sol" in drop_warnings[0] + assert ("model_info.reasoning_effort_levels" in drop_warnings[0]) is suggests_declared_levels assert "model_info.base_model" not in drop_warnings[0] def test_dropping_reasoning_for_a_model_without_supports_reasoning_logs_one_warning( diff --git a/tests/unit/llms/openai/test_gpt_reasoning_transformation.py b/tests/unit/llms/openai/test_gpt_reasoning_transformation.py index 98f37cb2c4e..807a43c5d9d 100644 --- a/tests/unit/llms/openai/test_gpt_reasoning_transformation.py +++ b/tests/unit/llms/openai/test_gpt_reasoning_transformation.py @@ -1593,11 +1593,17 @@ def test_gpt_6_astra_takes_the_reasoning_series_request_shape(): @pytest.mark.parametrize( - ("custom_llm_provider", "level"), - [("azure", "none"), ("azure", "xhigh"), ("openai", "xhigh")], + ("custom_llm_provider", "level", "advice"), + [ + ("azure", "none", frozenset(("model_info.reasoning_effort_levels", "model_info.base_model"))), + ("azure", "xhigh", frozenset(("model_info.reasoning_effort_levels", "model_info.base_model"))), + ("openai", "xhigh", frozenset(("model_info.reasoning_effort_levels",))), + ("azure_ai", "xhigh", frozenset(("model_info.reasoning_effort_levels",))), + ("github_copilot", "xhigh", frozenset()), + ], ) -def test_a_reasoning_effort_the_level_gate_drops_logs_a_warning_naming_the_fix( - caplog: pytest.LogCaptureFixture, custom_llm_provider: str, level: str +def test_a_reasoning_effort_the_level_gate_drops_logs_a_warning_naming_only_the_fixes_its_route_honors( + caplog: pytest.LogCaptureFixture, custom_llm_provider: str, level: str, advice: frozenset[str] ) -> None: with caplog.at_level(logging.WARNING, logger="LiteLLM"): params: Final = litellm.get_optional_params( @@ -1614,9 +1620,14 @@ def test_a_reasoning_effort_the_level_gate_drops_logs_a_warning_naming_the_fix( if record.levelno == logging.WARNING and "reasoning_effort" in record.getMessage() ] assert len(drop_warnings) == 1 - assert all( - fragment in drop_warnings[0] - for fragment in (level, "gpt-5.6-terra-ptu", "model_info.base_model", "model_info.reasoning_effort_levels") + assert repr(level) in drop_warnings[0] and "gpt-5.6-terra-ptu" in drop_warnings[0] + assert ( + frozenset( + fragment + for fragment in ("model_info.reasoning_effort_levels", "model_info.base_model") + if fragment in drop_warnings[0] + ) + == advice )