diff --git a/litellm/llms/anthropic/messages/responses_adapters/handler.py b/litellm/llms/anthropic/messages/responses_adapters/handler.py index bf6f114f198..cb78b47a2b7 100644 --- a/litellm/llms/anthropic/messages/responses_adapters/handler.py +++ b/litellm/llms/anthropic/messages/responses_adapters/handler.py @@ -52,6 +52,7 @@ 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, @@ -276,7 +277,9 @@ 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) - return None if off_level is None else {**reasoning_params, "effort": off_level} + if off_level is None or cost_map_row_lacks_reasoning(model, custom_llm_provider): + return None + return {**reasoning_params, "effort": off_level} if drop_params and effort not in _KNOWN_REASONING_EFFORTS: verbose_logger.warning( "Dropping reasoning.effort=%r for %s, no reasoning API defines it, because drop_params is set", diff --git a/litellm/llms/anthropic/messages/utils.py b/litellm/llms/anthropic/messages/utils.py index 02a0c9c70c7..87f7089cfb4 100644 --- a/litellm/llms/anthropic/messages/utils.py +++ b/litellm/llms/anthropic/messages/utils.py @@ -92,6 +92,7 @@ _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]: @@ -242,12 +243,17 @@ 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 resolve_supported_reasoning_efforts + from litellm.litellm_core_utils.reasoning_effort_capability import ( + deployment_is_catalog_mapped, + resolve_supported_reasoning_efforts, + ) 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=True) + return resolve_supported_reasoning_efforts( + model_info, deployment_is_mapped=deployment_is_catalog_mapped(model_info, _NO_OPERATOR_MODEL_INFO) + ) def _lowest_tier_instead_of_off(levels: tuple[str, ...] | None) -> str | None: @@ -256,6 +262,11 @@ 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/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 c313a79e902..fe091e67a5a 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 @@ -737,3 +737,47 @@ class TestAnOpenAIChatRouteRequestGetsTheResponsesBridgeThinkingControls: ) assert "reasoning_effort" not in sent + + +_OFF_MAP_OPENAI_DEPLOYMENT: Final = { + "model": "openai/off-map-hybrid-reasoner", + "api_base": "https://compatible.example.test/v1", + "api_key": "fake-key", +} + + +class TestDisabledOnAnOffMapOpenAIDeployment: + @pytest.fixture(autouse=True) + def _httpx_transport(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(litellm, "disable_aiohttp_transport", True) + + @pytest.mark.asyncio + async def test_the_chat_route_sends_reasoning_effort_none( + self, respx_mock: respx.MockRouter, local_model_cost_map: None + ) -> None: + sent: Final = await _body_sent_upstream( + respx_mock, + _OFF_MAP_OPENAI_DEPLOYMENT, + {}, + r"https://compatible\.example\.test/v1/chat/completions", + _CHAT_COMPLETION, + thinking={"type": "disabled"}, + stop_sequences=["STOP"], + ) + + assert sent["reasoning_effort"] == "none" + + @pytest.mark.asyncio + async def test_the_responses_bridge_sends_no_reasoning_its_transformation_would_refuse( + self, respx_mock: respx.MockRouter, local_model_cost_map: None + ) -> None: + sent: Final = await _body_sent_upstream( + respx_mock, + _OFF_MAP_OPENAI_DEPLOYMENT, + {}, + r"https://compatible\.example\.test/v1/responses", + _RESPONSE, + thinking={"type": "disabled"}, + ) + + assert "reasoning" not in sent