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 f3de4aa30a5..41ca6d5ae2e 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 @@ -282,6 +282,16 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): return int(major_version.group(1)) >= 3 return model.split("/")[-1] in GEMINI_ROLLING_LATEST_ALIASES + @staticmethod + def _is_gemini_3_flash_or_newer(model: str | None) -> bool: + """ + Check if the model is a Gemini 3 or newer Flash, the tier that accepts + the MINIMAL thinking level. Flash-Lite counts, Pro does not. + """ + if not model: + return False + return VertexGeminiConfig._is_gemini_3_or_newer(model) and "flash" in model.lower() + @staticmethod def _forward_gemini_function_call_id(model: str) -> bool: """ @@ -866,10 +876,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): Returns: GeminiThinkingConfig with thinkingLevel and includeThoughts """ - # Check if this is gemini-3-flash which supports MINIMAL thinking level - # Covers gemini-3-flash, gemini-3-flash-preview, gemini-3.1-flash, gemini-3.1-flash-lite-preview, - # gemini-3.5-flash, and any future 3.x-flash variants. - is_gemini3flash: Final = model and ("flash" in model.lower() and "gemini-3" in model.lower()) + is_gemini3flash: Final = VertexGeminiConfig._is_gemini_3_flash_or_newer(model) is_gemini31pro: Final = model and ("gemini-3.1-pro-preview" in model.lower()) if reasoning_effort == "minimal": if is_gemini3flash: @@ -963,7 +970,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): params["includeThoughts"] = True # Follow provider defaults unless explicitly opted into legacy behavior. if litellm.enable_gemini_default_thinking_level_low is True: - is_gemini3flash: Final = "gemini-3" in model.lower() and "flash" in model.lower() + is_gemini3flash: Final = VertexGeminiConfig._is_gemini_3_flash_or_newer(model) params["thinkingLevel"] = "minimal" if is_gemini3flash else "low" else: # Thinking disabled 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 919d87cf1d2..0d63b21a56c 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 @@ -2349,6 +2349,152 @@ def test_thought_signature_fallback_for_rolling_latest_alias(): assert not any("thoughtSignature" in part for part in parts) +def test_is_gemini_3_flash_or_newer(): + from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import ( + VertexGeminiConfig, + ) + + assert VertexGeminiConfig._is_gemini_3_flash_or_newer("gemini-3-flash") == True + assert ( + VertexGeminiConfig._is_gemini_3_flash_or_newer("gemini-3-flash-preview") == True + ) + assert VertexGeminiConfig._is_gemini_3_flash_or_newer("gemini-3.1-flash") == True + assert VertexGeminiConfig._is_gemini_3_flash_or_newer("gemini-3.5-flash") == True + assert ( + VertexGeminiConfig._is_gemini_3_flash_or_newer( + "gemini-3.1-flash-lite-preview" + ) + == True + ) + + # Rolling aliases resolving to a Flash tier + assert VertexGeminiConfig._is_gemini_3_flash_or_newer("gemini-flash-latest") == True + assert ( + VertexGeminiConfig._is_gemini_3_flash_or_newer("gemini-flash-lite-latest") + == True + ) + assert ( + VertexGeminiConfig._is_gemini_3_flash_or_newer("gemini/gemini-flash-latest") + == True + ) + assert ( + VertexGeminiConfig._is_gemini_3_flash_or_newer("vertex_ai/gemini-flash-latest") + == True + ) + + # Later major versions, without a code change + assert VertexGeminiConfig._is_gemini_3_flash_or_newer("gemini-4-flash") == True + assert VertexGeminiConfig._is_gemini_3_flash_or_newer("gemini/gemini-10-flash") == True + + # MINIMAL is a Flash-tier level, so Pro is excluded + assert VertexGeminiConfig._is_gemini_3_flash_or_newer("gemini-pro-latest") == False + assert ( + VertexGeminiConfig._is_gemini_3_flash_or_newer("gemini-3-pro-preview") == False + ) + + # Older majors are excluded + assert VertexGeminiConfig._is_gemini_3_flash_or_newer("gemini-2.5-flash") == False + assert ( + VertexGeminiConfig._is_gemini_3_flash_or_newer( + "gemini-2.5-flash-native-audio-latest" + ) + == False + ) + + # Edge cases + assert VertexGeminiConfig._is_gemini_3_flash_or_newer(None) == False + assert VertexGeminiConfig._is_gemini_3_flash_or_newer("") == False + + +def test_reasoning_effort_minimal_reaches_minimal_on_rolling_flash_alias(): + """ + `reasoning_effort="minimal"` must reach `thinkingLevel="minimal"` on the + Flash tier. `gemini-flash-latest` serves Gemini 3.x Flash, so it was + demoted to "low" and the caller silently got more thinking than it asked + for. + """ + from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import ( + VertexGeminiConfig, + ) + + v = VertexGeminiConfig() + + for model in [ + "gemini-flash-latest", + "gemini/gemini-flash-latest", + "gemini-flash-lite-latest", + "gemini-3.5-flash", + ]: + result = v.map_openai_params( + non_default_params={"reasoning_effort": "minimal"}, + optional_params={}, + model=model, + drop_params=False, + ) + assert result["thinkingConfig"]["thinkingLevel"] == "minimal", model + assert result["thinkingConfig"]["includeThoughts"] is True + + # Pro keeps "low" — MINIMAL is a Flash-tier level + result = v.map_openai_params( + non_default_params={"reasoning_effort": "minimal"}, + optional_params={}, + model="gemini-pro-latest", + drop_params=False, + ) + assert result["thinkingConfig"]["thinkingLevel"] == "low" + + +def test_reasoning_effort_medium_reaches_medium_on_rolling_flash_alias(): + from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import ( + VertexGeminiConfig, + ) + + v = VertexGeminiConfig() + + result = v.map_openai_params( + non_default_params={"reasoning_effort": "medium"}, + optional_params={}, + model="gemini-flash-latest", + drop_params=False, + ) + assert result["thinkingConfig"]["thinkingLevel"] == "medium" + + # Pro (other than 3.1) still escalates to "high" + result = v.map_openai_params( + non_default_params={"reasoning_effort": "medium"}, + optional_params={}, + model="gemini-pro-latest", + drop_params=False, + ) + assert result["thinkingConfig"]["thinkingLevel"] == "high" + + +def test_default_thinking_level_reaches_minimal_on_rolling_flash_alias(monkeypatch): + """ + The `enable_gemini_default_thinking_level_low` opt-in reads the Flash tier + off the same helper, so a rolling alias is not demoted to "low" there + either. + """ + import litellm + from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import ( + VertexGeminiConfig, + ) + + monkeypatch.setattr( + litellm, "enable_gemini_default_thinking_level_low", True, raising=False + ) + + params = VertexGeminiConfig._map_thinking_param( + {"type": "enabled", "budget_tokens": 2048}, model="gemini-flash-latest" + ) + assert params["thinkingLevel"] == "minimal" + + params = VertexGeminiConfig._map_thinking_param( + {"type": "enabled", "budget_tokens": 2048}, model="gemini-pro-latest" + ) + assert params["thinkingLevel"] == "low" + + def _tool_call_messages(tool_call_id: str): return [ {"role": "user", "content": "hi"},