From 476755238fdf6c709f98a237541cd97308c8769a Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 3 Nov 2026 10:00:15 -0800 Subject: [PATCH] fix(anthropic): send reasoning effort none wherever the Responses transformation accepts it An openai/ deployment the cost map does not list now gets effort none for disabled thinking on the chat route even when its model_info sets a mode, since the off level now asks whether the loaded cost map lists the model instead of reading the row the router registers. On the Responses bridge the off switch is dropped only where the Responses transformation itself would refuse reasoning.effort, so a slash-named model it accepts gets none again for adaptive effort none and disabled thinking. --- .../messages/responses_adapters/handler.py | 12 +++++-- litellm/llms/anthropic/messages/utils.py | 14 ++------ .../llms/openai/responses/transformation.py | 4 +-- ..._handler_reasoning_effort_normalization.py | 36 +++++++++++++++++++ 4 files changed, 50 insertions(+), 16 deletions(-) diff --git a/litellm/llms/anthropic/messages/responses_adapters/handler.py b/litellm/llms/anthropic/messages/responses_adapters/handler.py index cb78b47a2b7..77d50bc4a16 100644 --- a/litellm/llms/anthropic/messages/responses_adapters/handler.py +++ b/litellm/llms/anthropic/messages/responses_adapters/handler.py @@ -27,6 +27,7 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import ( ) from litellm.litellm_core_utils.reasoning_effort_capability import deployment_rounds_reasoning_effort from litellm.litellm_core_utils.tool_fields import drops_mcp_servers, tools_without_mcp_servers +from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig from litellm.responses.utils import auto_reasoning_summary from litellm.types.integrations.custom_logger import CONVERTED_STREAM_KEYS from litellm.types.llms.anthropic import ( @@ -41,7 +42,7 @@ from litellm.types.llms.anthropic_messages.anthropic_response import ( AnthropicMessagesResponse, ) from litellm.types.llms.openai import REASONING_EFFORT, ResponsesAPIResponse -from litellm.types.utils import CountContextManagement +from litellm.types.utils import CountContextManagement, LlmProviders from litellm.utils import ProviderConfigManager from ..adapters.handler import context_management_explicitly_dropped, request_safeguards_evaluator @@ -52,7 +53,6 @@ from ..convertible_blocks import reject_malformed_tool_result_text from ..safeguards import with_safeguard_results from ..utils import ( REASONING_EFFORT_OFF, - cost_map_row_lacks_reasoning, litellm_logging_obj_from_kwargs, local_model_name, maps_bare_output_effort, @@ -277,7 +277,7 @@ def _reasoning_for_target( return reasoning if effort == REASONING_EFFORT_OFF: off_level: Final = reasoning_off_level(model, custom_llm_provider, drop_params=drop_params) - if off_level is None or cost_map_row_lacks_reasoning(model, custom_llm_provider): + if off_level is None or _responses_api_refuses_reasoning_effort(model, custom_llm_provider): return None return {**reasoning_params, "effort": off_level} if drop_params and effort not in _KNOWN_REASONING_EFFORTS: @@ -305,6 +305,12 @@ def _reasoning_for_target( return reasoning if normalized == effort else {**reasoning_params, "effort": normalized} +def _responses_api_refuses_reasoning_effort(model: str, custom_llm_provider: str | None) -> bool: + return custom_llm_provider == LlmProviders.OPENAI.value and not OpenAIResponsesAPIConfig.supports_reasoning_param( + model + ) + + def _model_excludes_effort( effort: str, *, model: str, custom_llm_provider: str | None, deployment_model_info: object ) -> bool: diff --git a/litellm/llms/anthropic/messages/utils.py b/litellm/llms/anthropic/messages/utils.py index 87f7089cfb4..05e9cc19d11 100644 --- a/litellm/llms/anthropic/messages/utils.py +++ b/litellm/llms/anthropic/messages/utils.py @@ -92,7 +92,6 @@ _OBJECT_LIST: Final = TypeAdapter(list[object]) _NO_SETTINGS: Final[Mapping[str, object]] = MappingProxyType({}) -_NO_OPERATOR_MODEL_INFO: Final[Mapping[str, object]] = MappingProxyType({}) def proxy_general_settings() -> Mapping[str, object]: @@ -243,16 +242,14 @@ def _mapped_model_info(model: str, custom_llm_provider: str | None) -> ModelInfo def model_reasoning_efforts(model: str, custom_llm_provider: str | None) -> tuple[str, ...] | None: - from litellm.litellm_core_utils.reasoning_effort_capability import ( - deployment_is_catalog_mapped, - resolve_supported_reasoning_efforts, - ) + from litellm.litellm_core_utils.reasoning_effort_capability import resolve_supported_reasoning_efforts + from litellm.utils import has_catalog_entry model_info: Final = _mapped_model_info(model, custom_llm_provider) if model_info is None: return None return resolve_supported_reasoning_efforts( - model_info, deployment_is_mapped=deployment_is_catalog_mapped(model_info, _NO_OPERATOR_MODEL_INFO) + model_info, deployment_is_mapped=has_catalog_entry(model, custom_llm_provider) ) @@ -262,11 +259,6 @@ def _lowest_tier_instead_of_off(levels: tuple[str, ...] | None) -> str | None: return next((tier for tier in _LOWEST_REASONING_TIERS if tier in levels), None) -def cost_map_row_lacks_reasoning(model: str, custom_llm_provider: str | None) -> bool: - model_info: Final = _mapped_model_info(model, custom_llm_provider) - return model_info is not None and not model_info.get("supports_reasoning") - - def reasoning_off_level(model: str, custom_llm_provider: str | None, *, drop_params: bool) -> str | None: from litellm.utils import supports_none_reasoning_effort, supports_reasoning diff --git a/litellm/llms/openai/responses/transformation.py b/litellm/llms/openai/responses/transformation.py index 6bbf3598bd8..8bdb96c62c3 100644 --- a/litellm/llms/openai/responses/transformation.py +++ b/litellm/llms/openai/responses/transformation.py @@ -405,7 +405,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): return OpenAIGPT5Config.effort_resolves_to_none(model, effort) @staticmethod - def _supports_reasoning_param(model: str) -> bool: + def supports_reasoning_param(model: str) -> bool: from litellm.utils import get_model_info_helper try: @@ -503,7 +503,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): if ( self.custom_llm_provider == LlmProviders.OPENAI and self._requests_reasoning_effort(params.get("reasoning")) - and not self._supports_reasoning_param(model=model) + and not self.supports_reasoning_param(model=model) ): if drop_params or litellm.drop_params: dropped_reasoning: Final = params.pop("reasoning", None) diff --git a/tests/unit/llms/anthropic/messages/adapters/test_handler_reasoning_effort_normalization.py b/tests/unit/llms/anthropic/messages/adapters/test_handler_reasoning_effort_normalization.py index fe091e67a5a..bedf95e8a95 100644 --- a/tests/unit/llms/anthropic/messages/adapters/test_handler_reasoning_effort_normalization.py +++ b/tests/unit/llms/anthropic/messages/adapters/test_handler_reasoning_effort_normalization.py @@ -781,3 +781,39 @@ class TestDisabledOnAnOffMapOpenAIDeployment: ) assert "reasoning" not in sent + + @pytest.mark.asyncio + async def test_the_chat_route_sends_none_when_the_operator_sets_the_deployment_mode( + self, respx_mock: respx.MockRouter, local_model_cost_map: None + ) -> None: + sent: Final = await _body_sent_upstream( + respx_mock, + {**_OFF_MAP_OPENAI_DEPLOYMENT, "model": "openai/vendor/off-map-hybrid-reasoner"}, + {"mode": "responses"}, + r"https://compatible\.example\.test/v1/responses", + _RESPONSE, + thinking={"type": "disabled"}, + stop_sequences=["STOP"], + ) + + assert sent["reasoning"]["effort"] == "none" + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "messages_params", + [{"thinking": {"type": "disabled"}}, {"thinking": {"type": "adaptive"}, "output_config": {"effort": "none"}}], + ids=["disabled", "adaptive-effort-none"], + ) + async def test_the_responses_bridge_sends_none_where_its_transformation_accepts_it( + self, respx_mock: respx.MockRouter, local_model_cost_map: None, messages_params: Mapping[str, object] + ) -> None: + sent: Final = await _body_sent_upstream( + respx_mock, + {**_OFF_MAP_OPENAI_DEPLOYMENT, "model": "openai/vendor/off-map-hybrid-reasoner"}, + {}, + r"https://compatible\.example\.test/v1/responses", + _RESPONSE, + **messages_params, + ) + + assert sent["reasoning"] == {"effort": "none"}