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 474ddb402a1..b0e23926c47 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 @@ -536,6 +536,68 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): return googleSearch, googleSearchRetrieval, enterpriseWebSearch, urlContext + @staticmethod + def _has_function_declarations_tool(tools: Optional[List[dict]]) -> bool: + if not tools: + return False + + return any( + bool(tool.get("function_declarations") or tool.get("functionDeclarations")) + for tool in tools + ) + + @staticmethod + def _is_search_tool(tool: dict) -> bool: + search_tool_keys = { + VertexToolName.GOOGLE_SEARCH.value, + VertexToolName.GOOGLE_SEARCH_RETRIEVAL.value, + VertexToolName.ENTERPRISE_WEB_SEARCH.value, + VertexToolName.URL_CONTEXT.value, + "google_search", + "google_search_retrieval", + "enterprise_web_search", + "urlContext", + } + return any(key in tool for key in search_tool_keys) + + def _drop_search_tools_if_mixed_with_functions( + self, optional_params: dict + ) -> None: + tools = optional_params.get("tools") + if not isinstance(tools, list) or not self._has_function_declarations_tool( + tools + ): + return + + server_side_tool_invocations = optional_params.get( + "include_server_side_tool_invocations", False + ) + if server_side_tool_invocations: + return + + filtered_tools = [ + tool for tool in tools if not self._is_search_tool(tool=tool) + ] + if len(filtered_tools) == len(tools): + return + + verbose_logger.warning( + "Vertex AI does not support mixing function declarations with " + "search tools in the same request. Dropping search tools and " + "keeping function declarations. To use search tools, send a " + "request without function calling tools." + ) + optional_params["tools"] = filtered_tools + + def _add_tools_to_optional_params(self, optional_params: dict, tools: List) -> dict: + optional_params = super()._add_tools_to_optional_params( + optional_params=optional_params, tools=tools + ) + self._drop_search_tools_if_mixed_with_functions( + optional_params=optional_params + ) + return optional_params + def _map_function( # noqa: PLR0915 self, value: List[dict], optional_params: dict ) -> List[Tools]: 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 353d19b0198..0e0839d702c 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 @@ -3905,6 +3905,124 @@ def test_vertex_ai_web_search_options_in_map_openai_params(): ), "web_search_options should be removed after transformation" +def test_vertex_ai_web_search_options_after_function_tools_drops_search(): + """ + web_search_options is mapped outside _map_function, so the conflict resolver + must also run when tools are appended to optional_params. + """ + from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import ( + VertexGeminiConfig, + ) + + v = VertexGeminiConfig() + optional_params = {} + + function_tools = v._map_function( + value=[ + { + "type": "function", + "function": { + "name": "lookup_weather", + "description": "Lookup weather", + }, + } + ], + optional_params=optional_params, + ) + optional_params = v._add_tools_to_optional_params( + optional_params=optional_params, tools=function_tools + ) + + search_tool = v._map_web_search_options({}) + optional_params = v._add_tools_to_optional_params( + optional_params=optional_params, tools=[search_tool] + ) + + assert len(optional_params["tools"]) == 1 + assert "function_declarations" in optional_params["tools"][0] + assert optional_params["tools"][0]["function_declarations"][0]["name"] == ( + "lookup_weather" + ) + + +def test_vertex_ai_web_search_options_before_function_tools_drops_search(): + """ + The same conflict can occur when web_search_options is processed before + function tools, depending on request parameter order. + """ + from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import ( + VertexGeminiConfig, + ) + + v = VertexGeminiConfig() + optional_params = {} + + search_tool = v._map_web_search_options({}) + optional_params = v._add_tools_to_optional_params( + optional_params=optional_params, tools=[search_tool] + ) + + function_tools = v._map_function( + value=[ + { + "type": "function", + "function": { + "name": "lookup_weather", + "description": "Lookup weather", + }, + } + ], + optional_params=optional_params, + ) + optional_params = v._add_tools_to_optional_params( + optional_params=optional_params, tools=function_tools + ) + + assert len(optional_params["tools"]) == 1 + assert "function_declarations" in optional_params["tools"][0] + assert optional_params["tools"][0]["function_declarations"][0]["name"] == ( + "lookup_weather" + ) + + +def test_vertex_ai_web_search_options_with_function_tools_preserved_for_server_side_invocations(): + """ + Gemini 3+ server-side tool invocation mode intentionally allows combining + function declarations with search tools. + """ + from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import ( + VertexGeminiConfig, + ) + + v = VertexGeminiConfig() + optional_params = {"include_server_side_tool_invocations": True} + + function_tools = v._map_function( + value=[ + { + "type": "function", + "function": { + "name": "lookup_weather", + "description": "Lookup weather", + }, + } + ], + optional_params=optional_params, + ) + optional_params = v._add_tools_to_optional_params( + optional_params=optional_params, tools=function_tools + ) + + search_tool = v._map_web_search_options({}) + optional_params = v._add_tools_to_optional_params( + optional_params=optional_params, tools=[search_tool] + ) + + tool_keys = {key for tool in optional_params["tools"] for key in tool.keys()} + assert "function_declarations" in tool_keys + assert "googleSearch" in tool_keys + + def test_vertex_ai_service_tier_in_map_openai_params(): """Test that service_tier is correctly mapped to optional_params.""" from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import (