Merge pull request #40748 from BerriAI/litellm_fix_gemini_reasoning_effort_400

fix(vertex_ai): return 400 for invalid reasoning_effort instead of 500
This commit is contained in:
ryan-crabbe-berri 2026-09-11 16:31:16 -07:00 committed by GitHub
commit 4e9d414603
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 85 additions and 2 deletions

View file

@ -23,6 +23,7 @@ from litellm.constants import (
DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET_GEMINI_2_5_FLASH_LITE,
DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET_GEMINI_2_5_PRO,
)
from litellm.exceptions import UnsupportedParamsError
from litellm.litellm_core_utils.json_fragment_accumulator import JSONFragmentAccumulator
from litellm.litellm_core_utils.prompt_templates.factory import (
_encode_tool_call_id_with_signature,
@ -108,6 +109,21 @@ else:
StreamingChoices = Any
SUPPORTED_REASONING_EFFORTS: Final = ("minimal", "low", "medium", "high", "none", "disable")
def _unsupported_reasoning_effort(reasoning_effort: str) -> UnsupportedParamsError:
return UnsupportedParamsError(
message=(
f"Invalid `reasoning_effort`: {reasoning_effort!r}. "
f"Must be one of: {', '.join(repr(effort) for effort in SUPPORTED_REASONING_EFFORTS)}. "
"To drop this param, set `litellm.drop_params = True` or pass in `(.., drop_params=True)` "
"in the request - https://docs.litellm.ai/docs/completion/drop_params"
),
status_code=400,
)
class VertexAIBaseConfig:
def get_mapped_special_auth_params(self) -> dict:
"""
@ -842,7 +858,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
"includeThoughts": False,
}
else:
raise ValueError(f"Invalid reasoning effort: {reasoning_effort}")
raise _unsupported_reasoning_effort(reasoning_effort)
@staticmethod
def _map_reasoning_effort_to_thinking_level(
@ -890,7 +906,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
else:
return {"thinkingLevel": "low", "includeThoughts": False}
else:
raise ValueError(f"Invalid reasoning effort: {reasoning_effort}")
raise _unsupported_reasoning_effort(reasoning_effort)
@staticmethod
def _is_thinking_budget_zero(thinking_budget: int | None) -> bool:

View file

@ -5769,3 +5769,70 @@ def test_calculate_web_search_requests_counts_unique_queries():
assert VertexGeminiConfig._calculate_web_search_requests([]) is None
assert VertexGeminiConfig._calculate_web_search_requests([{"webSearchQueries": ["", ""]}]) is None
@pytest.mark.parametrize("custom_llm_provider", ["gemini", "vertex_ai"])
@pytest.mark.parametrize(
"model",
["gemini-2.5-flash", "gemini-3-pro-preview"],
ids=["thinking_budget_mapper", "thinking_level_mapper"],
)
@pytest.mark.parametrize("reasoning_effort", ["banana", "xhigh"])
def test_invalid_reasoning_effort_is_a_400_not_a_500(custom_llm_provider, model, reasoning_effort):
"""Regression for #40474.
Both reasoning_effort mappers used to end their if/elif chain in a bare `ValueError`, which
`exception_type()` has no branch for, so it fell through to `APIConnectionError` and the proxy
answered a malformed client request with a retryable HTTP 500. `xhigh` is covered alongside the
nonsense value because it is a member of litellm's own `REASONING_EFFORT` literal, so callers
bridging from OpenAI-shaped code reach it without typing anything wrong.
"""
from litellm.utils import get_optional_params
with pytest.raises(litellm.BadRequestError) as exc_info:
get_optional_params(
model=model,
custom_llm_provider=custom_llm_provider,
reasoning_effort=reasoning_effort,
drop_params=True,
)
assert exc_info.value.status_code == 400
message: Final = str(exc_info.value)
assert reasoning_effort in message
for supported in ("minimal", "low", "medium", "high", "none", "disable"):
assert supported in message
@pytest.mark.parametrize("custom_llm_provider", ["gemini", "vertex_ai"])
def test_invalid_reasoning_effort_surfaces_as_400_through_completion(custom_llm_provider):
"""The same request through `completion()` must not come back as a retryable 500.
Needs no provider credentials: param mapping runs before any network call.
"""
with pytest.raises(litellm.BadRequestError) as exc_info:
completion(
model=f"{custom_llm_provider}/gemini-3-pro-preview",
messages=[{"role": "user", "content": "hi"}],
reasoning_effort="banana",
)
assert exc_info.value.status_code == 400
assert not isinstance(exc_info.value, litellm.APIConnectionError)
@pytest.mark.parametrize("model", ["gemini-2.5-flash", "gemini-3-pro-preview"])
def test_supported_reasoning_efforts_still_map(model):
"""Guards the fix against over-rejecting: every advertised value must still produce a config."""
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import (
SUPPORTED_REASONING_EFFORTS,
)
for effort in SUPPORTED_REASONING_EFFORTS:
result: Final = VertexGeminiConfig().map_openai_params(
non_default_params={"reasoning_effort": effort},
optional_params={},
model=model,
drop_params=False,
)
assert "thinkingConfig" in result