fix(gemini): read the Flash tier off the version too, not "gemini-3"

`_map_reasoning_effort_to_thinking_level` and `_map_thinking_param` each
re-derived the Flash tier with the same substring the outer gate just
stopped using, so a rolling alias reached the Gemini 3 branch and was then
demoted inside it: `reasoning_effort="minimal"` on `gemini-flash-latest`
returned `thinkingLevel="low"`, and `"none"` / `"disable"` likewise, so the
caller silently got more thinking than it asked for. Closes #18245.

Both sites now share `_is_gemini_3_flash_or_newer`, which reuses the major
version read and so covers Gemini 4 Flash without a code change. Flash-Lite
counts; Pro does not, since MINIMAL is a Flash-tier level. Pro keeps "low"
for minimal and "high" for medium, unchanged.
This commit is contained in:
maya-lukas 2026-08-31 12:30:12 +02:00
parent a686f36f0c
commit a190e6bc11
2 changed files with 158 additions and 5 deletions

View file

@ -282,6 +282,16 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
return int(major_version.group(1)) >= 3
return model.split("/")[-1] in GEMINI_ROLLING_LATEST_ALIASES
@staticmethod
def _is_gemini_3_flash_or_newer(model: str | None) -> bool:
"""
Check if the model is a Gemini 3 or newer Flash, the tier that accepts
the MINIMAL thinking level. Flash-Lite counts, Pro does not.
"""
if not model:
return False
return VertexGeminiConfig._is_gemini_3_or_newer(model) and "flash" in model.lower()
@staticmethod
def _forward_gemini_function_call_id(model: str) -> bool:
"""
@ -866,10 +876,7 @@ 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 = VertexGeminiConfig._is_gemini_3_flash_or_newer(model)
is_gemini31pro: Final = model and ("gemini-3.1-pro-preview" in model.lower())
if reasoning_effort == "minimal":
if is_gemini3flash:
@ -963,7 +970,7 @@ 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()
is_gemini3flash: Final = VertexGeminiConfig._is_gemini_3_flash_or_newer(model)
params["thinkingLevel"] = "minimal" if is_gemini3flash else "low"
else:
# Thinking disabled

View file

@ -2349,6 +2349,152 @@ def test_thought_signature_fallback_for_rolling_latest_alias():
assert not any("thoughtSignature" in part for part in parts)
def test_is_gemini_3_flash_or_newer():
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import (
VertexGeminiConfig,
)
assert VertexGeminiConfig._is_gemini_3_flash_or_newer("gemini-3-flash") == True
assert (
VertexGeminiConfig._is_gemini_3_flash_or_newer("gemini-3-flash-preview") == True
)
assert VertexGeminiConfig._is_gemini_3_flash_or_newer("gemini-3.1-flash") == True
assert VertexGeminiConfig._is_gemini_3_flash_or_newer("gemini-3.5-flash") == True
assert (
VertexGeminiConfig._is_gemini_3_flash_or_newer(
"gemini-3.1-flash-lite-preview"
)
== True
)
# Rolling aliases resolving to a Flash tier
assert VertexGeminiConfig._is_gemini_3_flash_or_newer("gemini-flash-latest") == True
assert (
VertexGeminiConfig._is_gemini_3_flash_or_newer("gemini-flash-lite-latest")
== True
)
assert (
VertexGeminiConfig._is_gemini_3_flash_or_newer("gemini/gemini-flash-latest")
== True
)
assert (
VertexGeminiConfig._is_gemini_3_flash_or_newer("vertex_ai/gemini-flash-latest")
== True
)
# Later major versions, without a code change
assert VertexGeminiConfig._is_gemini_3_flash_or_newer("gemini-4-flash") == True
assert VertexGeminiConfig._is_gemini_3_flash_or_newer("gemini/gemini-10-flash") == True
# MINIMAL is a Flash-tier level, so Pro is excluded
assert VertexGeminiConfig._is_gemini_3_flash_or_newer("gemini-pro-latest") == False
assert (
VertexGeminiConfig._is_gemini_3_flash_or_newer("gemini-3-pro-preview") == False
)
# Older majors are excluded
assert VertexGeminiConfig._is_gemini_3_flash_or_newer("gemini-2.5-flash") == False
assert (
VertexGeminiConfig._is_gemini_3_flash_or_newer(
"gemini-2.5-flash-native-audio-latest"
)
== False
)
# Edge cases
assert VertexGeminiConfig._is_gemini_3_flash_or_newer(None) == False
assert VertexGeminiConfig._is_gemini_3_flash_or_newer("") == False
def test_reasoning_effort_minimal_reaches_minimal_on_rolling_flash_alias():
"""
`reasoning_effort="minimal"` must reach `thinkingLevel="minimal"` on the
Flash tier. `gemini-flash-latest` serves Gemini 3.x Flash, so it was
demoted to "low" and the caller silently got more thinking than it asked
for.
"""
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import (
VertexGeminiConfig,
)
v = VertexGeminiConfig()
for model in [
"gemini-flash-latest",
"gemini/gemini-flash-latest",
"gemini-flash-lite-latest",
"gemini-3.5-flash",
]:
result = v.map_openai_params(
non_default_params={"reasoning_effort": "minimal"},
optional_params={},
model=model,
drop_params=False,
)
assert result["thinkingConfig"]["thinkingLevel"] == "minimal", model
assert result["thinkingConfig"]["includeThoughts"] is True
# Pro keeps "low" — MINIMAL is a Flash-tier level
result = v.map_openai_params(
non_default_params={"reasoning_effort": "minimal"},
optional_params={},
model="gemini-pro-latest",
drop_params=False,
)
assert result["thinkingConfig"]["thinkingLevel"] == "low"
def test_reasoning_effort_medium_reaches_medium_on_rolling_flash_alias():
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import (
VertexGeminiConfig,
)
v = VertexGeminiConfig()
result = v.map_openai_params(
non_default_params={"reasoning_effort": "medium"},
optional_params={},
model="gemini-flash-latest",
drop_params=False,
)
assert result["thinkingConfig"]["thinkingLevel"] == "medium"
# Pro (other than 3.1) still escalates to "high"
result = v.map_openai_params(
non_default_params={"reasoning_effort": "medium"},
optional_params={},
model="gemini-pro-latest",
drop_params=False,
)
assert result["thinkingConfig"]["thinkingLevel"] == "high"
def test_default_thinking_level_reaches_minimal_on_rolling_flash_alias(monkeypatch):
"""
The `enable_gemini_default_thinking_level_low` opt-in reads the Flash tier
off the same helper, so a rolling alias is not demoted to "low" there
either.
"""
import litellm
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import (
VertexGeminiConfig,
)
monkeypatch.setattr(
litellm, "enable_gemini_default_thinking_level_low", True, raising=False
)
params = VertexGeminiConfig._map_thinking_param(
{"type": "enabled", "budget_tokens": 2048}, model="gemini-flash-latest"
)
assert params["thinkingLevel"] == "minimal"
params = VertexGeminiConfig._map_thinking_param(
{"type": "enabled", "budget_tokens": 2048}, model="gemini-pro-latest"
)
assert params["thinkingLevel"] == "low"
def _tool_call_messages(tool_call_id: str):
return [
{"role": "user", "content": "hi"},