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 [