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"}