mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
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:
commit
4e9d414603
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