fix: drop vertex search tools when functions are present

This commit is contained in:
Genmin 2026-04-30 08:11:32 -07:00
parent d3891e6eae
commit 92f84d35ab
2 changed files with 180 additions and 0 deletions

View file

@ -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]:

View file

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