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 32d7b39a20e..795247eb4f4 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 @@ -1341,18 +1341,28 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): return None @staticmethod - def _map_reasoning_effort(effort: str, model: str, reasoning_effort: object) -> GeminiThinkingConfig: + def _map_reasoning_effort( + effort: str, + model: str, + reasoning_effort: object, + drop_params: bool = False, + ) -> GeminiThinkingConfig | None: include_thoughts: Final = VertexGeminiConfig._include_thoughts_requested_by_reasoning_effort(reasoning_effort) if VertexGeminiConfig._is_gemini_3_or_newer(model): - return VertexGeminiConfig._map_reasoning_effort_to_thinking_level(effort, model, include_thoughts) - return VertexGeminiConfig._map_reasoning_effort_to_thinking_budget(effort, model, include_thoughts) + return VertexGeminiConfig._map_reasoning_effort_to_thinking_level( + effort, model, include_thoughts, drop_params=drop_params + ) + return VertexGeminiConfig._map_reasoning_effort_to_thinking_budget( + effort, model, include_thoughts, drop_params=drop_params + ) @staticmethod def _map_reasoning_effort_to_thinking_budget( reasoning_effort: str, model: str | None = None, include_thoughts: bool | None = None, - ) -> GeminiThinkingConfig: + drop_params: bool = False, + ) -> GeminiThinkingConfig | None: sends_thoughts: Final = VertexGeminiConfig._include_thoughts(include_thoughts) if reasoning_effort == "minimal": # Use model-specific minimum thinking budget or fallback @@ -1396,6 +1406,9 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): "includeThoughts": False, } else: + if drops_unsupported_params(drop_params): + verbose_logger.debug("Dropping unsupported reasoning_effort: %s", reasoning_effort) + return None raise _unsupported_reasoning_effort(reasoning_effort) @staticmethod @@ -1411,7 +1424,8 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): reasoning_effort: str, model: str | None = None, include_thoughts: bool | None = None, - ) -> GeminiThinkingConfig: + drop_params: bool = False, + ) -> GeminiThinkingConfig | None: """ Map reasoning_effort to thinking_level for Gemini 3+ models. Args: @@ -1440,6 +1454,9 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): "includeThoughts": False, } else: + if drops_unsupported_params(drop_params): + verbose_logger.debug("Dropping unsupported reasoning_effort: %s", reasoning_effort) + return None raise _unsupported_reasoning_effort(reasoning_effort) @staticmethod @@ -1765,15 +1782,23 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): effort_value = value.get("effort") if effort_value is not None: - # Validate no conflict with thinking_level - VertexGeminiConfig._validate_thinking_config_conflicts( - optional_params=optional_params, - param_name="reasoning_effort", - param_description="thinking_budget", + should_drop: bool = drops_unsupported_params(drop_params) or ( + "drop_params" in optional_params and optional_params["drop_params"] in (True, "true", "True") ) - optional_params["thinkingConfig"] = VertexGeminiConfig._map_reasoning_effort( - effort=effort_value, model=model, reasoning_effort=value + thinking_config = VertexGeminiConfig._map_reasoning_effort( + effort=effort_value, + model=model, + reasoning_effort=value, + drop_params=should_drop, ) + if thinking_config is not None: + # Validate no conflict with thinking_level + VertexGeminiConfig._validate_thinking_config_conflicts( + optional_params=optional_params, + param_name="reasoning_effort", + param_description="thinking_budget", + ) + optional_params["thinkingConfig"] = thinking_config elif param == "thinking": # Validate no conflict with thinking_level VertexGeminiConfig._validate_thinking_config_conflicts( diff --git a/tests/unit/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py b/tests/unit/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py index 2eecb83e2df..fa747426cdc 100644 --- a/tests/unit/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py +++ b/tests/unit/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py @@ -7298,7 +7298,7 @@ def test_invalid_reasoning_effort_is_a_400_not_a_500(custom_llm_provider, model, model=model, custom_llm_provider=custom_llm_provider, reasoning_effort=reasoning_effort, - drop_params=True, + drop_params=False, ) assert exc_info.value.status_code == 400 diff --git a/tests/unit/llms/vertex_ai/gemini/test_vertex_gemini_reasoning_effort.py b/tests/unit/llms/vertex_ai/gemini/test_vertex_gemini_reasoning_effort.py new file mode 100644 index 00000000000..1531d3a9b24 --- /dev/null +++ b/tests/unit/llms/vertex_ai/gemini/test_vertex_gemini_reasoning_effort.py @@ -0,0 +1,153 @@ +from typing import Final + +import pytest + +import litellm +from litellm.exceptions import UnsupportedParamsError +from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import VertexGeminiConfig + + +def test_vertex_gemini_unsupported_reasoning_effort_with_drop_params_true() -> None: + """Verifies that unsupported reasoning_effort (e.g., 'xhigh') is safely dropped + + when drop_params=True is passed directly or inside optional_params. + """ + config: Final = VertexGeminiConfig() + + # 1. Directly passing drop_params=True (string format) + result_direct: Final = config.map_openai_params( + non_default_params={"reasoning_effort": "xhigh"}, + optional_params={}, + model="gemini-2.5-flash", + drop_params=True, + ) + assert "thinkingConfig" not in result_direct + assert "reasoning_effort" not in result_direct + + # 2. Directly passing drop_params=True (dict format from Agents SDK) + result_dict: Final = config.map_openai_params( + non_default_params={"reasoning_effort": {"effort": "xhigh", "summary": "auto"}}, + optional_params={}, + model="gemini-2.5-flash", + drop_params=True, + ) + assert "thinkingConfig" not in result_dict + + # 3. Passing drop_params=True in optional_params + result_optional: Final = config.map_openai_params( + non_default_params={"reasoning_effort": "xhigh"}, + optional_params={"drop_params": True}, + model="gemini-3-pro-preview", + drop_params=False, + ) + assert "thinkingConfig" not in result_optional + + +def test_vertex_gemini_unsupported_reasoning_effort_with_global_drop_params( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Verifies that unsupported reasoning_effort (e.g., 'max') is safely dropped + + when litellm.drop_params = True is globally active. + """ + monkeypatch.setattr(litellm, "drop_params", True) + config: Final = VertexGeminiConfig() + + # Test with Gemini 2.5 (budget-based thinking) + result_gemini_25: Final = config.map_openai_params( + non_default_params={"reasoning_effort": "max"}, + optional_params={}, + model="gemini-2.5-flash", + drop_params=False, + ) + assert "thinkingConfig" not in result_gemini_25 + + # Test with Gemini 3+ (level-based thinking) + result_gemini_3: Final = config.map_openai_params( + non_default_params={"reasoning_effort": "max"}, + optional_params={}, + model="gemini-3-pro-preview", + drop_params=False, + ) + assert "thinkingConfig" not in result_gemini_3 + + +def test_vertex_gemini_unsupported_reasoning_effort_raises_without_drop_params() -> None: + """Verifies that unsupported reasoning_effort values raise UnsupportedParamsError + + when drop_params is False. + """ + config: Final = VertexGeminiConfig() + + for effort in ("xhigh", "max", "unsupported_level"): + with pytest.raises(UnsupportedParamsError): + config.map_openai_params( + non_default_params={"reasoning_effort": effort}, + optional_params={}, + model="gemini-2.5-flash", + drop_params=False, + ) + + with pytest.raises(UnsupportedParamsError): + config.map_openai_params( + non_default_params={"reasoning_effort": effort}, + optional_params={}, + model="gemini-3-pro-preview", + drop_params=False, + ) + + +def test_vertex_gemini_direct_reasoning_effort_mappers_drop_params() -> None: + """Verifies direct mapper methods honor drop_params=True and return None.""" + # Test _map_reasoning_effort_to_thinking_budget directly + budget_result: Final = VertexGeminiConfig._map_reasoning_effort_to_thinking_budget( + reasoning_effort="xhigh", + model="gemini-2.5-flash", + drop_params=True, + ) + assert budget_result is None + + # Test _map_reasoning_effort_to_thinking_level directly + level_result: Final = VertexGeminiConfig._map_reasoning_effort_to_thinking_level( + reasoning_effort="xhigh", + model="gemini-3-pro-preview", + drop_params=True, + ) + assert level_result is None + + # Test _map_reasoning_effort directly + effort_result: Final = VertexGeminiConfig._map_reasoning_effort( + effort="max", + model="gemini-2.5-flash", + reasoning_effort="max", + drop_params=True, + ) + assert effort_result is None + + effort_level_result: Final = VertexGeminiConfig._map_reasoning_effort( + effort="max", + model="gemini-3-pro-preview", + reasoning_effort="max", + drop_params=True, + ) + assert effort_level_result is None + + +@pytest.mark.parametrize("effort", ["low", "medium", "high"]) +@pytest.mark.parametrize("model", ["gemini-2.5-flash", "gemini-3-pro-preview"]) +def test_vertex_gemini_valid_reasoning_effort_mapping(effort: str, model: str) -> None: + """Verifies that supported reasoning_effort values continue to map as expected.""" + config: Final = VertexGeminiConfig() + result: Final = config.map_openai_params( + non_default_params={"reasoning_effort": effort}, + optional_params={}, + model=model, + drop_params=False, + ) + assert "thinkingConfig" in result + thinking_config: Final = result["thinkingConfig"] + assert thinking_config is not None + if "gemini-3" in model: + assert thinking_config.get("thinkingLevel") == effort + else: + assert "thinkingBudget" in thinking_config