From 212b249e38e407bf9103e6c7de6690f7dcfa1207 Mon Sep 17 00:00:00 2001 From: LeVDuan Date: Mon, 16 Mar 2026 14:24:40 +0900 Subject: [PATCH] fix(vertex_ai): drop search tools when mixed with function declarations (#23337) Vertex AI rejects requests containing both search tools (googleSearch, enterpriseWebSearch, urlContext) and function declarations with error: 'Multiple tools are supported only when they are all search tools.' When _merge_tools_from_deployment() combines deployment-level search tools with user-request function tools (e.g. via MCP), the mixed tool list causes a 400 error. This fix detects the conflict in _map_function() and drops search tools, keeping function declarations. Non-search tools like code_execution and computerUse are preserved. Fixes #23337 --- .../vertex_and_google_ai_studio_gemini.py | 30 ++++ ...test_vertex_and_google_ai_studio_gemini.py | 167 ++++++++++++++---- 2 files changed, 159 insertions(+), 38 deletions(-) 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 e6e548ab98a..4e8e6c7994c 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 @@ -633,6 +633,36 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): # per Vertex AI API spec: "A Tool object should contain exactly one type of Tool" _tools_list: List[Tools] = [] + # Vertex AI constraint: multiple Tool objects in a request must ALL be + # search tools. Mixing function declarations with search tools in the + # same request causes a 400 error: + # "Multiple tools are supported only when they are all search tools." + # When both are present (e.g. deployment config has search tools and + # user request adds function calling tools via MCP), drop search tools + # and keep function declarations. + # Ref: https://github.com/BerriAI/litellm/issues/23337 + has_search_tools = any( + v is not None + for v in [ + googleSearch, + googleSearchRetrieval, + enterpriseWebSearch, + urlContext, + ] + ) + if gtool_func_declarations and has_search_tools: + verbose_logger.warning( + "Vertex AI does not support mixing function declarations with " + "search tools (googleSearch, enterpriseWebSearch, urlContext, " + "googleSearchRetrieval) in the same request. Dropping search " + "tools and keeping function declarations. To use search tools, " + "send a request without function calling tools." + ) + googleSearch = None + googleSearchRetrieval = None + enterpriseWebSearch = None + urlContext = None + # Function declarations can be grouped together in one Tool if gtool_func_declarations: func_tool = 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 ddc404cb8c7..873f99d031a 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 @@ -2678,10 +2678,14 @@ def test_vertex_ai_multiple_tool_types_separate_objects(): def test_vertex_ai_function_declarations_with_other_tools_separate(): """ - Test that function declarations and other tool types are in separate Tool objects. + Test that when function declarations are mixed with search tools AND + non-search tools like code_execution, search tools are dropped but + non-search tools are preserved. - This ensures that when using both function calling AND special tools like - google_search or code_execution, they are properly separated per API spec. + Vertex AI constraint: "Multiple tools are supported only when they are + all search tools." So mixing function declarations with googleSearch + would cause a 400 error. code_execution is NOT a search tool, so it + is preserved. Input: value=[ @@ -2693,7 +2697,6 @@ def test_vertex_ai_function_declarations_with_other_tools_separate(): Expected Output: tools=[ {"function_declarations": [{"name": "get_weather", "description": "Get weather"}]}, - {"googleSearch": {}}, {"code_execution": {}}, ] """ @@ -2709,32 +2712,24 @@ def test_vertex_ai_function_declarations_with_other_tools_separate(): optional_params=optional_params ) - # Should have 3 separate Tool objects - assert len(tools) == 3, f"Expected 3 separate Tool objects, got {len(tools)}" + # Should have 2 Tool objects: function declarations + code_execution + # googleSearch is dropped to avoid Vertex AI 400 error + assert len(tools) == 2, f"Expected 2 Tool objects, got {len(tools)}" # Find each tool type func_tool = None - search_tool = None code_tool = None for tool in tools: if "function_declarations" in tool: func_tool = tool - elif "googleSearch" in tool: - search_tool = tool elif "code_execution" in tool: code_tool = tool - # Verify all tools are present and separate + # Verify function declarations and code_execution are present assert func_tool is not None, "function_declarations Tool should be present" - assert search_tool is not None, "googleSearch Tool should be present" assert code_tool is not None, "code_execution Tool should be present" - # Verify each Tool has exactly one type - assert len(func_tool.keys()) == 1, "function_declarations Tool should have only one key" - assert len(search_tool.keys()) == 1, "googleSearch Tool should have only one key" - assert len(code_tool.keys()) == 1, "code_execution Tool should have only one key" - # Verify function declaration content assert func_tool["function_declarations"][0]["name"] == "get_weather" @@ -2762,6 +2757,116 @@ def test_vertex_ai_single_tool_type_still_works(): assert tools[0]["code_execution"] == {} +def test_vertex_ai_mixed_search_and_function_tools_drops_search(): + """ + Test that when both search tools and function declarations are present, + search tools are dropped to avoid Vertex AI 400 error: + "Multiple tools are supported only when they are all search tools." + + This happens when deployment config has search tools (enterpriseWebSearch, + urlContext) and user request adds function calling tools (e.g. via MCP). + + Ref: https://github.com/BerriAI/litellm/issues/23337 + """ + v = VertexGeminiConfig() + optional_params = {} + + tools = v._map_function( + value=[ + {"enterpriseWebSearch": {}}, + {"urlContext": {}}, + { + "type": "function", + "function": { + "name": "get_weather", + "description": "Get weather", + "parameters": { + "type": "object", + "properties": {"city": {"type": "string"}}, + }, + }, + }, + ], + optional_params=optional_params, + ) + + # Should only have function declarations (search tools dropped) + assert len(tools) == 1, f"Expected 1 Tool object, got {len(tools)}: {tools}" + assert "function_declarations" in tools[0] + assert tools[0]["function_declarations"][0]["name"] == "get_weather" + + +def test_vertex_ai_mixed_google_search_and_function_tools_drops_search(): + """ + Test that googleSearch is also dropped when mixed with function declarations. + """ + v = VertexGeminiConfig() + optional_params = {} + + tools = v._map_function( + value=[ + {"googleSearch": {}}, + { + "type": "function", + "function": {"name": "my_func", "description": "A function"}, + }, + ], + optional_params=optional_params, + ) + + assert len(tools) == 1 + assert "function_declarations" in tools[0] + assert tools[0]["function_declarations"][0]["name"] == "my_func" + + +def test_vertex_ai_search_tools_only_no_drop(): + """ + Test that search tools are preserved when no function declarations are present. + """ + v = VertexGeminiConfig() + optional_params = {} + + tools = v._map_function( + value=[ + {"enterpriseWebSearch": {}}, + {"urlContext": {}}, + ], + optional_params=optional_params, + ) + + assert len(tools) == 2 + tool_keys = [list(t.keys())[0] for t in tools] + assert "enterpriseWebSearch" in tool_keys + assert "url_context" in tool_keys + + +def test_vertex_ai_function_tools_with_code_execution_preserved(): + """ + Test that code_execution is NOT dropped when mixed with function declarations. + Only search tools should be dropped. + """ + v = VertexGeminiConfig() + optional_params = {} + + tools = v._map_function( + value=[ + {"code_execution": {}}, + { + "type": "function", + "function": {"name": "my_func", "description": "A function"}, + }, + ], + optional_params=optional_params, + ) + + assert len(tools) == 2 + tool_keys = set() + for t in tools: + tool_keys.update(t.keys()) + assert "function_declarations" in tool_keys + assert "code_execution" in tool_keys + + def test_vertex_ai_openai_web_search_tool_transformation(): """ Test that OpenAI-style web_search and web_search_preview tools are transformed to googleSearch. @@ -2818,7 +2923,9 @@ def test_vertex_ai_openai_web_search_preview_tool_transformation(): def test_vertex_ai_openai_web_search_with_function_tools(): """ - Test that OpenAI-style web_search tool works alongside function tools. + Test that when OpenAI-style web_search tool (transformed to googleSearch) + is mixed with function tools, search tools are dropped to avoid Vertex AI + 400 error: "Multiple tools are supported only when they are all search tools." Input: value=[ @@ -2828,7 +2935,6 @@ def test_vertex_ai_openai_web_search_with_function_tools(): Expected Output: tools=[ - {"googleSearch": {}}, {"function_declarations": [{"name": "get_weather", "description": "Get weather"}]}, ] """ @@ -2843,27 +2949,12 @@ def test_vertex_ai_openai_web_search_with_function_tools(): optional_params=optional_params ) - # Should have 2 separate Tool objects - assert len(tools) == 2, f"Expected 2 Tool objects, got {len(tools)}" + # Should have 1 Tool object: function declarations only + # googleSearch (from web_search) is dropped to avoid Vertex AI 400 error + assert len(tools) == 1, f"Expected 1 Tool object, got {len(tools)}" - # Find each tool type - search_tool = None - func_tool = None - - for tool in tools: - if "googleSearch" in tool: - search_tool = tool - elif "function_declarations" in tool: - func_tool = tool - - # Verify both tools are present - assert search_tool is not None, "googleSearch Tool should be present" - assert func_tool is not None, "function_declarations Tool should be present" - - # Verify googleSearch is empty config - assert search_tool["googleSearch"] == {} - - # Verify function declaration content + func_tool = tools[0] + assert "function_declarations" in func_tool assert func_tool["function_declarations"][0]["name"] == "get_weather"