fix(gemini): treat rolling -latest aliases as Gemini 3+

`_is_gemini_3_or_newer` matched the substring "gemini-3", so Google's
rolling aliases (`gemini-flash-latest`, `gemini-flash-lite-latest` and
`gemini-pro-latest`) were not recognised even though they resolve to
Gemini 3.x. Every Gemini 3 behaviour keyed off that check was therefore
silently disabled for them, including the documented
`skip_thought_signature_validator` fallback.

The visible symptom is a multi-turn tool call failing with
`400 Function call is missing a thought_signature in functionCall parts`
whenever the assistant turn's tool call does not carry LiteLLM's
`__thought__`-encoded id, for example history replayed by a client that
uses its own tool-call ids.

Matching is by exact alias name rather than an `endswith("-latest")`
check, because versioned names such as
`gemini-2.5-flash-native-audio-latest` also end in `-latest` and are not
Gemini 3.
This commit is contained in:
maya-lukas 2026-08-17 10:06:09 +02:00
parent 973329e986
commit 16b157f939
2 changed files with 92 additions and 1 deletions

View file

@ -106,6 +106,19 @@ else:
StreamingChoices = Any
# Google's rolling aliases, which always resolve to the newest release of each
# tier and today serve Gemini 3.x. They carry no version number, so they must be
# listed explicitly for Gemini 3 feature detection to see them.
# https://ai.google.dev/gemini-api/docs/models
GEMINI_ROLLING_LATEST_ALIASES = frozenset(
{
"gemini-flash-latest",
"gemini-flash-lite-latest",
"gemini-pro-latest",
}
)
class VertexAIBaseConfig:
def get_mapped_special_auth_params(self) -> dict:
"""
@ -266,11 +279,21 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
- gemini-3.1-pro-preview, gemini-3.1-flash, gemini-3.1-flash-lite-preview
- gemini-3.5-flash
- Any future Gemini 3.x models
- The rolling `-latest` aliases, which Google keeps pointed at the
newest release of each tier and which currently serve Gemini 3.x.
The `-latest` aliases are matched by exact name rather than by an
`endswith("-latest")` suffix check, because versioned names such as
`gemini-2.5-flash-native-audio-latest` also end in `-latest` and are
not Gemini 3.
"""
# Check for Gemini 3 models
if "gemini-3" in model:
return True
return False
# Rolling aliases carry no version in the name, so the substring check
# above cannot see them. Strip any provider prefix (`gemini/`,
# `vertex_ai/`) before comparing.
return model.split("/")[-1] in GEMINI_ROLLING_LATEST_ALIASES
@staticmethod
def _forward_gemini_function_call_id(model: str) -> bool:

View file

@ -2275,6 +2275,74 @@ def test_is_gemini_3_or_newer():
# Edge cases
assert VertexGeminiConfig._is_gemini_3_or_newer("") == False
# Rolling `-latest` aliases resolve to Gemini 3.x and carry no version in
# the name, so they must be detected explicitly.
assert VertexGeminiConfig._is_gemini_3_or_newer("gemini-flash-latest") == True
assert VertexGeminiConfig._is_gemini_3_or_newer("gemini-flash-lite-latest") == True
assert VertexGeminiConfig._is_gemini_3_or_newer("gemini-pro-latest") == True
assert (
VertexGeminiConfig._is_gemini_3_or_newer("gemini/gemini-flash-latest") == True
)
assert (
VertexGeminiConfig._is_gemini_3_or_newer("vertex_ai/gemini-pro-latest") == True
)
# A versioned name that merely ends in `-latest` is not Gemini 3.
assert (
VertexGeminiConfig._is_gemini_3_or_newer(
"gemini-2.5-flash-native-audio-latest"
)
== False
)
def test_thought_signature_fallback_for_rolling_latest_alias():
"""
A tool call whose thought signature did not survive the round-trip must
still get the dummy signature Google documents, otherwise Gemini rejects
the follow-up turn with:
400 Function call is missing a thought_signature in functionCall parts.
Regression test: `gemini-flash-latest` serves Gemini 3.x but was not
detected as such, so the fallback was skipped and every multi-turn tool
call that lost its signature failed.
"""
from litellm.litellm_core_utils.prompt_templates.factory import (
convert_to_gemini_tool_call_invoke,
)
message = {
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_abc123", # no `__thought__` suffix -> signature lost
"type": "function",
"function": {
"name": "search_website",
"arguments": '{"query": "Iceland"}',
},
}
],
}
for model in [
"gemini-3-flash",
"gemini-flash-latest",
"gemini-flash-lite-latest",
"gemini-pro-latest",
"gemini/gemini-flash-latest",
]:
parts = convert_to_gemini_tool_call_invoke(message, model=model)
assert any(
"thoughtSignature" in part for part in parts
), f"expected a thought signature for {model}"
# Pre-Gemini-3 models must not gain a signature they never needed.
parts = convert_to_gemini_tool_call_invoke(message, model="gemini-2.5-flash")
assert not any("thoughtSignature" in part for part in parts)
def _tool_call_messages(tool_call_id: str):
return [