mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
fix(vertex_ai): return 400 for invalid reasoning_effort instead of 500
Both reasoning_effort mappers ended their if/elif chain in a bare ValueError. exception_type() has no branch for ValueError, so it fell through to the shared APIConnectionError fallback and the proxy answered a malformed client request with a retryable HTTP 500 carrying no hint of the accepted values. Raise UnsupportedParamsError (400) instead, listing the supported set, matching what the Anthropic and Bedrock transforms already do and what this same file already does at its five other param-validation sites. This also covers 'xhigh' and 'max', which are members of litellm's own REASONING_EFFORT literal but have no Gemini mapping, so callers bridging from OpenAI-shaped code were hitting the 500 without typing anything wrong. Fixes #40474 Claude-Session: https://claude.ai/code/session_01XT1qsbjLwnhiN5sQ2hNUxr
This commit is contained in:
parent
db3338b206
commit
f72b117b21
2 changed files with 85 additions and 2 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue