From 16b157f9392dc068891636d1cc9182378c8dc474 Mon Sep 17 00:00:00 2001 From: maya-lukas Date: Mon, 17 Aug 2026 10:06:09 +0200 Subject: [PATCH 1/3] 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 [ From d7cbfc7759e7b42709ed8a19661a17fb34ac979b Mon Sep 17 00:00:00 2001 From: maya-lukas Date: Mon, 17 Aug 2026 10:22:10 +0200 Subject: [PATCH 2/3] fix(gemini): annotate alias set as Final, drop narrative comments Addresses review feedback: the module constant now carries the `Final` annotation required by LIT010, and the explanatory comments are folded into the existing docstring so the source keeps to the repository's comment convention. --- .../vertex_and_google_ai_studio_gemini.py | 20 +++++-------------- ...test_vertex_and_google_ai_studio_gemini.py | 4 ---- 2 files changed, 5 insertions(+), 19 deletions(-) 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 5eac5d62fdc..bcc75d36482 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,11 +106,7 @@ 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_ROLLING_LATEST_ALIASES: Final[frozenset[str]] = frozenset( { "gemini-flash-latest", "gemini-flash-lite-latest", @@ -279,20 +275,14 @@ 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. + - GEMINI_ROLLING_LATEST_ALIASES, which resolve to Gemini 3.x and carry + no version number of their own. Matched by exact name, since + versioned names such as `gemini-2.5-flash-native-audio-latest` also + end in `-latest` yet are not Gemini 3 """ # Check for Gemini 3 models if "gemini-3" in model: return True - # 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 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 1350d186e42..caebca689b2 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,8 +2275,6 @@ 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 @@ -2287,7 +2285,6 @@ def test_is_gemini_3_or_newer(): 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" @@ -2339,7 +2336,6 @@ def test_thought_signature_fallback_for_rolling_latest_alias(): "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) From a686f36f0c6b4fbedd272d694ea9e5b6c41b2b2d Mon Sep 17 00:00:00 2001 From: maya-lukas Date: Mon, 17 Aug 2026 10:34:49 +0200 Subject: [PATCH 3/3] fix(gemini): read the major version instead of matching "gemini-3" The substring check would have reported Gemini 4 and later as pre-3, reintroducing the same class of bug the moment Google ships an explicitly versioned `gemini-4-*` model. Parsing the major version off the name and comparing against 3 makes every later release work without a code change, and it also excludes `gemini-2.5-flash-native-audio-latest` on version rather than by special-casing the name. The alias set now covers only names that carry no version at all. --- .../vertex_and_google_ai_studio_gemini.py | 27 +++++++++---------- ...test_vertex_and_google_ai_studio_gemini.py | 9 +++++++ 2 files changed, 21 insertions(+), 15 deletions(-) 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 bcc75d36482..f3de4aa30a5 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 @@ -2,6 +2,7 @@ ## httpx client for vertex ai calls ## Initial implementation - covers gemini + image gen calls import json +import re import time from collections.abc import Callable, Mapping, Sequence from copy import deepcopy @@ -106,6 +107,8 @@ else: StreamingChoices = Any +GEMINI_MAJOR_VERSION_PATTERN: Final[re.Pattern[str]] = re.compile(r"gemini-(\d+)") + GEMINI_ROLLING_LATEST_ALIASES: Final[frozenset[str]] = frozenset( { "gemini-flash-latest", @@ -266,23 +269,17 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): @staticmethod def _is_gemini_3_or_newer(model: str) -> bool: """ - Check if the model is Gemini 3 Pro or newer. + Check if the model is Gemini 3 or newer, e.g. gemini-3-pro-preview, + gemini-3.1-flash, gemini-3.5-flash, and every later major version. - Gemini 3 models include: - - gemini-3-pro-preview - - gemini-3-flash - - gemini-3-flash-preview (Gemini 3 Flash) - - 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 - - GEMINI_ROLLING_LATEST_ALIASES, which resolve to Gemini 3.x and carry - no version number of their own. Matched by exact name, since - versioned names such as `gemini-2.5-flash-native-audio-latest` also - end in `-latest` yet are not Gemini 3 + The major version is read off the model name, so Gemini 4 and beyond + satisfy this without a code change. Names carrying no version at all are + matched against GEMINI_ROLLING_LATEST_ALIASES, which always resolve to + the newest release of their tier. """ - # Check for Gemini 3 models - if "gemini-3" in model: - return True + major_version: Final = GEMINI_MAJOR_VERSION_PATTERN.search(model) + if major_version is not None: + return int(major_version.group(1)) >= 3 return model.split("/")[-1] in GEMINI_ROLLING_LATEST_ALIASES @staticmethod 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 caebca689b2..919d87cf1d2 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 @@ -2292,6 +2292,15 @@ def test_is_gemini_3_or_newer(): == False ) + # Later major versions must satisfy this without a code change + assert VertexGeminiConfig._is_gemini_3_or_newer("gemini-4-flash") == True + assert VertexGeminiConfig._is_gemini_3_or_newer("gemini-4.2-pro-preview") == True + assert VertexGeminiConfig._is_gemini_3_or_newer("gemini/gemini-10-flash") == True + + # Unversioned names that are not rolling aliases stay excluded + assert VertexGeminiConfig._is_gemini_3_or_newer("gemini-embedding-001") == False + assert VertexGeminiConfig._is_gemini_3_or_newer("gemini-exp-1206") == False + def test_thought_signature_fallback_for_rolling_latest_alias(): """