From 7056151b91c1bdf21f494f778afd8f52648a9ddd Mon Sep 17 00:00:00 2001 From: Philipp Schmid <32632186+philschmid@users.noreply.github.com> Date: Tue, 22 Sep 2026 18:53:22 +0200 Subject: [PATCH] fix(gemini): simplify model version check (#42465) --- litellm/llms/gemini/chat/transformation.py | 2 +- .../vertex_and_google_ai_studio_gemini.py | 36 +++++------- .../test_fallback_generalizations.py | 2 +- ...test_vertex_and_google_ai_studio_gemini.py | 56 +++++++++++++++++-- 4 files changed, 68 insertions(+), 28 deletions(-) diff --git a/litellm/llms/gemini/chat/transformation.py b/litellm/llms/gemini/chat/transformation.py index 42c9ef13730..285350aecba 100644 --- a/litellm/llms/gemini/chat/transformation.py +++ b/litellm/llms/gemini/chat/transformation.py @@ -102,7 +102,7 @@ class GoogleAIStudioGeminiConfig(VertexGeminiConfig): "include_server_side_tool_invocations", "service_tier", ] - if supports_reasoning(model, custom_llm_provider="gemini"): + if supports_reasoning(model, custom_llm_provider="gemini") or self._is_gemini_3_or_newer(model): supported_params.append("reasoning_effort") supported_params.append("thinking") if self.is_model_gemini_audio_model(model): 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 46f1b948026..204b1ddd5ea 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 @@ -283,20 +284,15 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): @staticmethod def _is_gemini_3_or_newer(model: str) -> bool: """ - Check if the model is Gemini 3 Pro or newer. - - 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 + Check if the model is Gemini 3 or newer. """ - # Check for Gemini 3 models - if "gemini-3" in model: - return True - return False + model_name = model.split("/")[-1].lower() + if not model_name: + return False + # Pre-Gemini 3 models: gemini-1.x, gemini-2.x, gemini-pro, gemini-flash, gemini-exp + if re.match(r"^gemini-(?:[12](?:\.\d+)?|exp|(?:pro|flash)(?!-(?:lite-)?latest$))(?:-|$)", model_name): + return False + return True @staticmethod def _forward_gemini_function_call_id(model: str) -> bool: @@ -347,9 +343,10 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): if self._supports_penalty_parameters(model): supported_params.extend(["frequency_penalty", "presence_penalty"]) - if supports_reasoning(model): + if supports_reasoning(model) or self._is_gemini_3_or_newer(model): supported_params.append("reasoning_effort") supported_params.append("thinking") + return supported_params def map_tool_choice_values(self, model: str, tool_choice: str | dict) -> ToolConfig | None: @@ -871,8 +868,8 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): @staticmethod def _supports_minimal_thinking_level(model: str) -> bool: lowered: Final = model.lower() - is_gemini3flash: Final = "gemini-3" in lowered and "flash" in lowered - return is_gemini3flash and not is_explicitly_disabled_factory( + is_gemini3_or_newer_flash: Final = VertexGeminiConfig._is_gemini_3_or_newer(model) and "flash" in lowered + return is_gemini3_or_newer_flash and not is_explicitly_disabled_factory( model=model, custom_llm_provider=None, key="supports_minimal_reasoning_effort" ) @@ -890,9 +887,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): Returns: GeminiThinkingConfig with thinkingLevel and includeThoughts """ - is_gemini3flash: Final = model and ("flash" in model.lower() and "gemini-3" in model.lower()) supports_minimal: Final = bool(model) and VertexGeminiConfig._supports_minimal_thinking_level(model) - is_gemini31pro: Final = model and ("gemini-3.1-pro-preview" in model.lower()) if reasoning_effort == "minimal": if supports_minimal: return {"thinkingLevel": "minimal", "includeThoughts": True} @@ -901,10 +896,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): elif reasoning_effort == "low": return {"thinkingLevel": "low", "includeThoughts": True} elif reasoning_effort == "medium": - if is_gemini31pro or is_gemini3flash: - return {"thinkingLevel": "medium", "includeThoughts": True} - else: - return {"thinkingLevel": "high", "includeThoughts": True} + return {"thinkingLevel": "medium", "includeThoughts": True} elif reasoning_effort == "high": return {"thinkingLevel": "high", "includeThoughts": True} elif reasoning_effort in ("disable", "none"): diff --git a/tests/test_litellm/litellm_core_utils/test_fallback_generalizations.py b/tests/test_litellm/litellm_core_utils/test_fallback_generalizations.py index 25a12bebf9a..7928768b3bd 100644 --- a/tests/test_litellm/litellm_core_utils/test_fallback_generalizations.py +++ b/tests/test_litellm/litellm_core_utils/test_fallback_generalizations.py @@ -517,7 +517,7 @@ def test_shipped_gemini_chat_baseline_keeps_reasoning_effort_on_unmapped_model(s drop_params=False, ) assert isinstance(optional_params, dict) - assert optional_params["thinkingConfig"]["thinkingBudget"] > 0 + assert optional_params["thinkingConfig"]["thinkingLevel"] == "medium" assert optional_params["thinkingConfig"]["includeThoughts"] is True 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 6c818016c87..a796e2ac607 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 @@ -2435,10 +2435,16 @@ def test_is_gemini_3_or_newer(): VertexGeminiConfig, ) - # Gemini 3 models + # Gemini 3+ models assert VertexGeminiConfig._is_gemini_3_or_newer("gemini-3-pro-preview") == True assert VertexGeminiConfig._is_gemini_3_or_newer("gemini-3-flash") == True assert VertexGeminiConfig._is_gemini_3_or_newer("gemini-3-pro") == True + assert VertexGeminiConfig._is_gemini_3_or_newer("gemini-3.1-pro-preview") == True + assert VertexGeminiConfig._is_gemini_3_or_newer("gemini-test-id-bla") == True + assert VertexGeminiConfig._is_gemini_3_or_newer("test-id-bla") == True + 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("vertex_ai/gemini-3-pro-preview") == True @@ -2453,11 +2459,53 @@ def test_is_gemini_3_or_newer(): assert VertexGeminiConfig._is_gemini_3_or_newer("gemini-2.0-flash") == False assert VertexGeminiConfig._is_gemini_3_or_newer("gemini-1.5-pro") == False assert VertexGeminiConfig._is_gemini_3_or_newer("gemini-pro") == False + assert VertexGeminiConfig._is_gemini_3_or_newer("gemini-flash") == False # Edge cases assert VertexGeminiConfig._is_gemini_3_or_newer("") == False +@pytest.mark.parametrize( + "model", + [ + "gemini-3.1-pro-preview", + "gemini-3-flash", + "gemini-test-id-bla", + "test-id-bla", + ], +) +def test_gemini_3_reasoning_effort_maps_to_thinking_level(model: str): + """Test that reasoning_effort maps to thinkingLevel and default temperature=1.0""" + from litellm.llms.gemini.chat.transformation import GoogleAIStudioGeminiConfig + from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import ( + VertexGeminiConfig, + ) + + vertex_cfg = VertexGeminiConfig() + studio_cfg = GoogleAIStudioGeminiConfig() + + for cfg in (vertex_cfg, studio_cfg): + supported = cfg.get_supported_openai_params(model) + assert "reasoning_effort" in supported + assert "thinking" in supported + + for effort in ("low", "medium", "high"): + mapped = vertex_cfg.map_openai_params( + non_default_params={"reasoning_effort": effort}, + optional_params={}, + model=model, + drop_params=False, + ) + assert mapped["thinkingConfig"] == { + "thinkingLevel": effort, + "includeThoughts": True, + } + assert mapped["temperature"] == 1.0 + assert "thinkingBudget" not in mapped["thinkingConfig"] + + + + def _tool_call_messages(tool_call_id: str): return [ {"role": "user", "content": "hi"}, @@ -2643,7 +2691,7 @@ def test_reasoning_effort_maps_to_thinking_level_gemini_3(): assert result["thinkingConfig"]["thinkingLevel"] == "low" assert result["thinkingConfig"]["includeThoughts"] is True - # Test medium -> high + includeThoughts=True (medium not available yet) + # Test medium -> medium + includeThoughts=True optional_params = {} non_default_params = {"reasoning_effort": "medium"} result = v.map_openai_params( @@ -2652,7 +2700,7 @@ def test_reasoning_effort_maps_to_thinking_level_gemini_3(): model=model, drop_params=False, ) - assert result["thinkingConfig"]["thinkingLevel"] == "high" + assert result["thinkingConfig"]["thinkingLevel"] == "medium" assert result["thinkingConfig"]["includeThoughts"] is True # Test high -> high + includeThoughts=True @@ -2853,7 +2901,7 @@ def test_reasoning_effort_dict_format_gemini_3(): model=model, drop_params=False, ) - assert result["thinkingConfig"]["thinkingLevel"] == "high" + assert result["thinkingConfig"]["thinkingLevel"] == "medium" assert result["thinkingConfig"]["includeThoughts"] is True # Test dict format without effort key - no thinkingConfig should be set