fix(vertex): floor reasoning_effort to low on gemini-3.7-flash

gemini-3.7-flash dropped THINKING_LEVEL_MINIMAL support on Vertex; only
LOW, MEDIUM, and HIGH are accepted. reasoning_effort values minimal,
none, and disable previously mapped to "minimal" via the broad
gemini-3.x-flash substring match and returned HTTP 400. They now floor
to "low" so they resolve to a level Vertex accepts.

Fixes #37314
This commit is contained in:
gegemeimingzi 2026-08-19 22:35:49 +08:00
parent c696fdfb05
commit 5b41fc9b11
2 changed files with 83 additions and 9 deletions

View file

@ -272,6 +272,21 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
return True
return False
@staticmethod
def _is_gemini3_flash_supports_minimal(model: str) -> bool:
"""
Whether a gemini-3.x-flash model accepts THINKING_LEVEL_MINIMAL.
gemini-3.7-flash dropped MINIMAL support: Vertex only accepts LOW, MEDIUM, and
HIGH for it, while earlier 3.x-flash models (3.0, 3.1, 3.5, 3.6) still accept it.
"""
lowered: Final = model.lower()
return (
"flash" in lowered
and "gemini-3" in lowered
and "gemini-3.7" not in lowered
)
@staticmethod
def _forward_gemini_function_call_id(model: str) -> bool:
"""
@ -856,10 +871,8 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
Returns:
GeminiThinkingConfig with thinkingLevel and includeThoughts
"""
# Check if this is gemini-3-flash which supports MINIMAL thinking level
# Covers gemini-3-flash, gemini-3-flash-preview, gemini-3.1-flash, gemini-3.1-flash-lite-preview,
# gemini-3.5-flash, and any future 3.x-flash variants.
is_gemini3flash: Final = model and ("flash" in model.lower() and "gemini-3" in model.lower())
is_gemini3flash: Final = bool(model) and VertexGeminiConfig._is_gemini3_flash_supports_minimal(model)
is_gemini3_flash_family: Final = bool(model) and "flash" in model.lower() and "gemini-3" in model.lower()
is_gemini31pro: Final = model and ("gemini-3.1-pro-preview" in model.lower())
if reasoning_effort == "minimal":
if is_gemini3flash:
@ -869,20 +882,20 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
elif reasoning_effort == "low":
return {"thinkingLevel": "low", "includeThoughts": True}
elif reasoning_effort == "medium":
if is_gemini31pro or is_gemini3flash:
if is_gemini31pro or is_gemini3_flash_family:
return {"thinkingLevel": "medium", "includeThoughts": True}
else:
return {"thinkingLevel": "high", "includeThoughts": True}
elif reasoning_effort == "high":
return {"thinkingLevel": "high", "includeThoughts": True}
elif reasoning_effort == "disable":
# Gemini 3 cannot fully disable thinking, so we use "minimal" for gemini-3-flash-preview, "low" for others
# Gemini 3 cannot fully disable thinking, so we use "minimal" for the models that accept it, "low" for others.
if is_gemini3flash:
return {"thinkingLevel": "minimal", "includeThoughts": False}
else:
return {"thinkingLevel": "low", "includeThoughts": False}
elif reasoning_effort == "none":
# For gemini-3-flash-preview, use "minimal" instead of "low"
# Same as disable: "minimal" where accepted, "low" otherwise.
if is_gemini3flash:
return {"thinkingLevel": "minimal", "includeThoughts": False}
else:
@ -953,8 +966,11 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
params["includeThoughts"] = True
# Follow provider defaults unless explicitly opted into legacy behavior.
if litellm.enable_gemini_default_thinking_level_low is True:
is_gemini3flash: Final = "gemini-3" in model.lower() and "flash" in model.lower()
params["thinkingLevel"] = "minimal" if is_gemini3flash else "low"
params["thinkingLevel"] = (
"minimal"
if VertexGeminiConfig._is_gemini3_flash_supports_minimal(model)
else "low"
)
else:
# Thinking disabled
params["includeThoughts"] = False

View file

@ -1806,6 +1806,64 @@ def test_gemini_31_flash_lite_reasoning_effort_minimal():
), "gemini-3.1-flash-lite-preview should use thinkingLevel, not thinkingBudget"
def test_gemini_37_flash_reasoning_effort_no_minimal():
"""
gemini-3.7-flash dropped THINKING_LEVEL_MINIMAL support on Vertex; only LOW, MEDIUM,
and HIGH are accepted. reasoning_effort values that previously mapped to "minimal"
(minimal, none, disable) must resolve to "low" instead so they return 200.
Regression test for: https://github.com/BerriAI/litellm/issues/37314
"""
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import (
VertexGeminiConfig,
)
# Values that used to map to "minimal" must now floor at "low" on 3.7.
for reasoning_effort in ("minimal", "none", "disable"):
result = VertexGeminiConfig._map_reasoning_effort_to_thinking_level(
reasoning_effort=reasoning_effort,
model="gemini-3.7-flash",
)
assert (
result["thinkingLevel"] == "low"
), f"gemini-3.7-flash {reasoning_effort} should map to 'low', got '{result['thinkingLevel']}'"
assert result["includeThoughts"] is (reasoning_effort != "none" and reasoning_effort != "disable")
# Explicit low/medium/high are unaffected.
for reasoning_effort, expected_level in (
("low", "low"),
("medium", "medium"),
("high", "high"),
):
result = VertexGeminiConfig._map_reasoning_effort_to_thinking_level(
reasoning_effort=reasoning_effort,
model="gemini-3.7-flash",
)
assert (
result["thinkingLevel"] == expected_level
), f"gemini-3.7-flash {reasoning_effort} should map to '{expected_level}', got '{result['thinkingLevel']}'"
def test_gemini_36_flash_reasoning_effort_minimal_still_supported():
"""
gemini-3.6-flash still accepts THINKING_LEVEL_MINIMAL, so the fix for 3.7 must not
regress the earlier 3.x-flash models.
"""
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import (
VertexGeminiConfig,
)
for model in ("gemini-3.6-flash", "gemini-3.5-flash"):
result = VertexGeminiConfig._map_reasoning_effort_to_thinking_level(
reasoning_effort="minimal",
model=model,
)
assert (
result["thinkingLevel"] == "minimal"
), f"{model} should keep thinkingLevel='minimal', got '{result['thinkingLevel']}'"
assert result["includeThoughts"] is True
def test_gemini_image_size_limit_exceeded(monkeypatch):
"""
Test that large images exceeding MAX_IMAGE_URL_DOWNLOAD_SIZE_MB are rejected.