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
This commit is contained in:
LeVDuan 2026-03-16 14:24:40 +09:00 committed by Sameer Kankute
parent 924418aeea
commit 212b249e38
No known key found for this signature in database
2 changed files with 159 additions and 38 deletions

View file

@ -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()

View file

@ -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"