diff --git a/litellm/llms/base_llm/chat/transformation.py b/litellm/llms/base_llm/chat/transformation.py index b71ae0fddee..1a2a65579ec 100644 --- a/litellm/llms/base_llm/chat/transformation.py +++ b/litellm/llms/base_llm/chat/transformation.py @@ -79,6 +79,19 @@ class BaseLLMException(Exception): class BaseConfig(ABC): + # Per-provider capability flag: ``True`` means this provider's + # ``map_openai_params`` / ``_map_reasoning_effort`` (or equivalent) + # accepts the literal value ``reasoning_effort="disable"`` and translates + # it into a real "no reasoning" signal upstream (e.g. Gemini's + # ``thinkingConfig.thinkingBudget = 0``, Ollama's native ``think=False``). + # Providers that only accept ``low|medium|high|minimal|none|None`` and + # would raise on ``"disable"`` (Anthropic, Bedrock, OpenAI o-series, ...) + # must leave this as ``False``. Used by + # ``litellm.utils._think_to_reasoning_effort`` to decide whether the + # Ollama-style ``"think": false`` request flag should be translated to + # ``reasoning_effort="disable"`` for this provider, or silently dropped. + supports_reasoning_disable: bool = False + def __init__(self): pass diff --git a/litellm/llms/ollama/chat/transformation.py b/litellm/llms/ollama/chat/transformation.py index 48534799c97..5d42518efc9 100644 --- a/litellm/llms/ollama/chat/transformation.py +++ b/litellm/llms/ollama/chat/transformation.py @@ -47,6 +47,9 @@ else: class OllamaChatConfig(BaseConfig): + # See OllamaConfig: `reasoning_effort="disable"` is mapped to the native + # `think=False` parameter upstream, so it is safe to inject for `think:false`. + supports_reasoning_disable: bool = True """ Reference: https://github.com/ollama/ollama/blob/main/docs/api.md#parameters diff --git a/litellm/llms/ollama/completion/transformation.py b/litellm/llms/ollama/completion/transformation.py index 32981776753..33589c92fee 100644 --- a/litellm/llms/ollama/completion/transformation.py +++ b/litellm/llms/ollama/completion/transformation.py @@ -40,6 +40,10 @@ else: class OllamaConfig(BaseConfig): + # Ollama natively understands `think=False`, and litellm's existing + # `reasoning_effort="disable"` -> `think=False` mapping turns the OpenAI- + # style flag into the native one upstream. Safe to inject "disable". + supports_reasoning_disable: bool = True """ Reference: https://github.com/ollama/ollama/blob/main/docs/api.md#parameters diff --git a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py index a90e05919fb..333a8682e82 100644 --- a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py +++ b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py @@ -168,6 +168,10 @@ class VertexAIBaseConfig: class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): + # Gemini's `thinkingConfig.thinkingBudget = 0` translates the OpenAI-style + # `reasoning_effort="disable"` into a real "no reasoning" upstream signal, + # so it is safe to inject "disable" for `think:false` requests. + supports_reasoning_disable: bool = True """ Reference: https://cloud.google.com/vertex-ai/docs/generative-ai/chat/test-chat-prompts Reference: https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference diff --git a/litellm/utils.py b/litellm/utils.py index bbb00608713..db44fb4d382 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -3683,6 +3683,7 @@ def _think_to_reasoning_effort( think_value: Any, non_default_params: dict, supported_params: List[str], + provider_config: Optional[Any] = None, custom_llm_provider: Optional[str] = None, ) -> None: """ @@ -3701,12 +3702,10 @@ def _think_to_reasoning_effort( set ``reasoning_effort = "disable"`` on ``non_default_params`` -- but only when (a) the caller has not already supplied ``reasoning_effort``, (b) the provider lists ``reasoning_effort`` as a supported param, and (c) the - provider's transformation layer is known to recognise the literal value - ``"disable"`` (currently Gemini / Vertex AI / Ollama). Other providers - (Anthropic, Bedrock, OpenAI o-series, ...) advertise ``reasoning_effort`` - but only accept ``low|medium|high|minimal|none|None``; passing - ``"disable"`` to them raises ``ValueError`` / ``BadRequestError``. For - those providers, ``think`` is silently dropped (with a debug log). + provider's config opts in via the ``supports_reasoning_disable`` class + flag (currently Gemini / Vertex AI / Ollama). For other providers (whose + ``_map_reasoning_effort`` raises on the literal ``"disable"``), + ``think:false`` is silently dropped (with a debug log). - ``think`` truthy: ``reasoning_effort`` is left untouched (callers that want to enable reasoning should pass ``reasoning_effort`` explicitly). - ``think`` is an unrecognized string: ignored. @@ -3725,30 +3724,23 @@ def _think_to_reasoning_effort( else: think_bool = bool(think_value) - # Providers whose `_map_reasoning_effort` (or equivalent) accepts the - # literal "disable" value. Adding to this list is safe; omitting a provider - # that *does* accept it just means `think:false` is silently ignored there. - _PROVIDERS_SUPPORTING_REASONING_DISABLE = { - "gemini", - "vertex_ai", - "vertex_ai_beta", - "ollama", - "ollama_chat", - } + provider_supports_disable = bool( + getattr(provider_config, "supports_reasoning_disable", False) + ) if ( think_bool is False and "reasoning_effort" not in non_default_params and "reasoning_effort" in supported_params - and (custom_llm_provider or "") in _PROVIDERS_SUPPORTING_REASONING_DISABLE + and provider_supports_disable ): non_default_params["reasoning_effort"] = "disable" elif think_bool is False: verbose_logger.debug( - "litellm: 'think=False' was passed but provider %r does not accept " - "reasoning_effort='disable'; dropping silently. Pass " - "reasoning_effort explicitly if your provider supports a " - "'disable'-equivalent value.", + "litellm: 'think=False' was passed but provider %r does not opt " + "in to reasoning_effort='disable' (supports_reasoning_disable=False); " + "dropping silently. Pass reasoning_effort explicitly if your " + "provider supports a 'disable'-equivalent value.", custom_llm_provider, ) elif think_bool is True: @@ -4158,6 +4150,7 @@ def get_optional_params( # noqa: PLR0915 think_value=_think_value, non_default_params=non_default_params, supported_params=supported_params, + provider_config=provider_config, custom_llm_provider=custom_llm_provider, ) diff --git a/tests/test_litellm/test_think_param_normalization.py b/tests/test_litellm/test_think_param_normalization.py index b336f245561..447d51b6369 100644 --- a/tests/test_litellm/test_think_param_normalization.py +++ b/tests/test_litellm/test_think_param_normalization.py @@ -15,6 +15,23 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", from litellm.utils import _think_to_reasoning_effort, get_optional_params +class _FakeConfigSupportsDisable: + """Stand-in for a provider config (e.g. ``GeminiConfig`` / + ``OllamaConfig``) that opts in to ``reasoning_effort='disable'`` via the + ``supports_reasoning_disable`` class flag.""" + + supports_reasoning_disable = True + + +class _FakeConfigRejectsDisable: + """Stand-in for a provider config (e.g. ``AnthropicConfig`` / + ``BedrockConverseConfig``) whose ``_map_reasoning_effort`` would raise on + the literal ``'disable'``; default value of the ``BaseConfig`` class flag + is ``False``.""" + + supports_reasoning_disable = False + + class TestThinkToReasoningEffortHelper: def test_think_false_bool_sets_reasoning_effort_disable(self): non_default_params: dict = {} @@ -24,6 +41,7 @@ class TestThinkToReasoningEffortHelper: think_value=False, non_default_params=non_default_params, supported_params=supported_params, + provider_config=_FakeConfigSupportsDisable(), custom_llm_provider="gemini", ) @@ -37,6 +55,7 @@ class TestThinkToReasoningEffortHelper: think_value=falsy, non_default_params=non_default_params, supported_params=["reasoning_effort"], + provider_config=_FakeConfigSupportsDisable(), custom_llm_provider="gemini", ) @@ -49,6 +68,7 @@ class TestThinkToReasoningEffortHelper: think_value=True, non_default_params=non_default_params, supported_params=["reasoning_effort"], + provider_config=_FakeConfigSupportsDisable(), custom_llm_provider="gemini", ) @@ -63,6 +83,7 @@ class TestThinkToReasoningEffortHelper: think_value=False, non_default_params=non_default_params, supported_params=["temperature", "top_p"], + provider_config=_FakeConfigRejectsDisable(), custom_llm_provider="cohere_chat", ) @@ -75,6 +96,7 @@ class TestThinkToReasoningEffortHelper: think_value=False, non_default_params=non_default_params, supported_params=["reasoning_effort"], + provider_config=_FakeConfigSupportsDisable(), custom_llm_provider="gemini", ) @@ -90,29 +112,40 @@ class TestThinkToReasoningEffortHelper: think_value=False, non_default_params=non_default_params, supported_params=["reasoning_effort"], + provider_config=_FakeConfigSupportsDisable(), custom_llm_provider="gemini", ) assert non_default_params["reasoning_effort"] == "" - @pytest.mark.parametrize( - "provider", - ["anthropic", "bedrock", "openai", "azure", "vertex_ai_anthropic"], - ) - def test_think_false_dropped_for_providers_that_reject_disable(self, provider): - """Providers whose `_map_reasoning_effort` does not accept the literal - ``"disable"`` (Anthropic, Bedrock, OpenAI o-series, ...) would raise a - runtime ``ValueError`` / ``BadRequestError`` if we injected - ``reasoning_effort="disable"``. For these providers, ``think:false`` - must be silently dropped instead. Regression test for the P1 review - comment on PR #26642.""" + def test_think_false_dropped_when_provider_config_rejects_disable(self): + """Providers whose config has ``supports_reasoning_disable=False`` (the + ``BaseConfig`` default — Anthropic, Bedrock, OpenAI o-series, ...) + would raise on ``reasoning_effort='disable'``. ``think:false`` must be + silently dropped instead. Regression for the P1 review on PR #26642.""" non_default_params: dict = {} _think_to_reasoning_effort( think_value=False, non_default_params=non_default_params, supported_params=["reasoning_effort"], - custom_llm_provider=provider, + provider_config=_FakeConfigRejectsDisable(), + custom_llm_provider="anthropic", + ) + + assert "reasoning_effort" not in non_default_params + + def test_think_false_dropped_when_no_provider_config_passed(self): + """If we cannot determine the provider's capability (no config), be + conservative and drop rather than risk a runtime ValueError.""" + non_default_params: dict = {} + + _think_to_reasoning_effort( + think_value=False, + non_default_params=non_default_params, + supported_params=["reasoning_effort"], + provider_config=None, + custom_llm_provider="some_unknown_provider", ) assert "reasoning_effort" not in non_default_params @@ -124,12 +157,50 @@ class TestThinkToReasoningEffortHelper: think_value="maybe", non_default_params=non_default_params, supported_params=["reasoning_effort"], + provider_config=_FakeConfigSupportsDisable(), custom_llm_provider="gemini", ) assert "reasoning_effort" not in non_default_params +class TestProviderConfigsOptInToReasoningDisable: + """Verifies the per-provider opt-in flags wired up on the actual config + classes — these are what the real ``get_optional_params`` flow consults.""" + + def test_gemini_configs_opt_in(self): + from litellm.llms.gemini.chat.transformation import GoogleAIStudioGeminiConfig + from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import ( + VertexGeminiConfig, + ) + + assert VertexGeminiConfig.supports_reasoning_disable is True + assert GoogleAIStudioGeminiConfig.supports_reasoning_disable is True + + def test_ollama_configs_opt_in(self): + from litellm.llms.ollama.chat.transformation import OllamaChatConfig + from litellm.llms.ollama.completion.transformation import OllamaConfig + + assert OllamaConfig.supports_reasoning_disable is True + assert OllamaChatConfig.supports_reasoning_disable is True + + def test_anthropic_config_does_not_opt_in(self): + """Anthropic's ``_map_reasoning_effort`` raises on ``'disable'``, so it + must keep the ``BaseConfig`` default of ``False``.""" + from litellm.llms.anthropic.chat.transformation import AnthropicConfig + + assert AnthropicConfig.supports_reasoning_disable is False + + def test_bedrock_config_does_not_opt_in(self): + """Bedrock Converse rejects any ``reasoning_effort`` value outside + ``low|medium|high``, so it must not opt in.""" + from litellm.llms.bedrock.chat.converse_transformation import ( + AmazonConverseConfig, + ) + + assert AmazonConverseConfig.supports_reasoning_disable is False + + class TestGetOptionalParamsThinkFalse: """End-to-end tests through ``get_optional_params`` against real provider configs -- no network calls."""