From 16b157f9392dc068891636d1cc9182378c8dc474 Mon Sep 17 00:00:00 2001 From: maya-lukas Date: Mon, 17 Aug 2026 10:06:09 +0200 Subject: [PATCH] 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. --- .../vertex_and_google_ai_studio_gemini.py | 25 ++++++- ...test_vertex_and_google_ai_studio_gemini.py | 68 +++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py index d298670aa7a..5eac5d62fdc 100644 --- a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py +++ b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py @@ -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: diff --git a/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py b/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py index 51cc2857252..1350d186e42 100644 --- a/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py +++ b/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py @@ -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 [