address review (P2): delegate reasoning_effort='disable' opt-in to provider configs

Greptile P2 (PR #26642): the previous fix used a hardcoded provider allowlist
inside `_think_to_reasoning_effort` in `litellm/utils.py`, which violates the
project rule that provider-specific knowledge belongs in `llms/<provider>/`.

This commit replaces the allowlist with a per-provider class-level capability
flag, so each provider's config decides for itself whether
`reasoning_effort='disable'` is a valid value upstream:

- `BaseConfig.supports_reasoning_disable: bool = False` (new, default false)
- `VertexGeminiConfig.supports_reasoning_disable = True`
  (also covers `gemini/...` via the `GoogleAIStudioGeminiConfig` subclass --
  Gemini maps "disable" to `thinkingConfig.thinkingBudget = 0`)
- `OllamaConfig.supports_reasoning_disable = True`
- `OllamaChatConfig.supports_reasoning_disable = True`
  (Ollama natively understands `think=False`; existing
  `reasoning_effort="disable"` -> native `think=False` mapping is unchanged)

`_think_to_reasoning_effort` now accepts the already-resolved
`provider_config` (passed through from `get_optional_params`, where it is
already looked up via `ProviderConfigManager.get_provider_chat_config`) and
checks `getattr(provider_config, "supports_reasoning_disable", False)`
instead of comparing `custom_llm_provider` against a string set.

Behavior for providers like Anthropic / Bedrock / OpenAI o-series is
unchanged: their configs inherit the `False` default, so `think:false` is
silently dropped (with a debug log) rather than injecting an unmapped
"disable" value that would raise `ValueError` / `BadRequestError`. Adding a
new "disable"-capable provider is now a one-line change in that provider's
config, with no need to touch `utils.py`.

Tests
-----
- New `TestProviderConfigsOptInToReasoningDisable` asserts the four configs
  above opt in (`True`) while `AnthropicConfig` and `AmazonConverseConfig`
  keep the safe `False` default -- guards against accidental regressions.
- Existing helper-level tests updated to pass small fake config stand-ins
  (`_FakeConfigSupportsDisable`, `_FakeConfigRejectsDisable`) instead of
  relying on a hardcoded provider-name match.
- Added `test_think_false_dropped_when_no_provider_config_passed` for the
  conservative no-config code path.

`python -m pytest tests/test_litellm/test_think_param_normalization.py -q`
=> 21 passed.
This commit is contained in:
skgandikota 2026-04-28 00:51:07 +01:00
parent b953bfe243
commit f451082541
No known key found for this signature in database
GPG key ID: 9C8C0949390E528F
6 changed files with 121 additions and 33 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,
)

View file

@ -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."""