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 d8b1e7ba17c..98c46686d43 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 @@ -108,6 +109,17 @@ 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", + "gemini-flash-lite-latest", + "gemini-pro-latest", + } +) + + class VertexAIBaseConfig: def get_mapped_special_auth_params(self) -> dict: """ @@ -259,20 +271,18 @@ 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 + 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 - return False + 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 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 bd07bec900f..ebeb7f448ef 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 @@ -2431,6 +2431,79 @@ def test_is_gemini_3_or_newer(): # Edge cases assert VertexGeminiConfig._is_gemini_3_or_newer("") == False + 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 + ) + + assert ( + VertexGeminiConfig._is_gemini_3_or_newer( + "gemini-2.5-flash-native-audio-latest" + ) + == 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(): + """ + 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}" + + 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 [