fix(vertex_ai): separate Tool objects for each tool type per API spec

Fix Vertex AI API error: "tools[0].tool_type: one_of 'tool_type' has more
than one initialized field"

The Vertex AI API requires each Tool object to contain exactly one type
of tool (e.g., FunctionDeclaration, GoogleSearch, CodeExecution).
Previously, all tool types were combined into a single Tool object,
causing INVALID_ARGUMENT errors when using multiple tools simultaneously.

This change creates separate Tool objects for each tool type:
- Function declarations in one Tool
- Google Search in its own Tool
- Code Execution in its own Tool
- etc.

Ref: https://cloud.google.com/vertex-ai/generative-ai/docs/reference/rest/v1beta1/Tool

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
xuan07t2
2025-12-31 08:48:35 +07:00
co-authored by Claude Opus 4.5
parent c7734cb8bd
commit f3f1acd338
2 changed files with 224 additions and 11 deletions
@@ -552,24 +552,46 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
"Invalid tool={}. Use `litellm.set_verbose` or `litellm --detailed_debug` to see raw request."
)
# Only include function_declarations if there are actual functions
_tools = Tools()
# Build list of Tool objects - each Tool should contain exactly one type
# per Vertex AI API spec: "A Tool object should contain exactly one type of Tool"
_tools_list: List[Tools] = []
# Function declarations can be grouped together in one Tool
if gtool_func_declarations:
_tools["function_declarations"] = gtool_func_declarations
func_tool = Tools()
func_tool["function_declarations"] = gtool_func_declarations
_tools_list.append(func_tool)
# Each special tool type must be in its own Tool object
if googleSearch is not None:
_tools[VertexToolName.GOOGLE_SEARCH.value] = googleSearch
search_tool = Tools()
search_tool[VertexToolName.GOOGLE_SEARCH.value] = googleSearch
_tools_list.append(search_tool)
if googleSearchRetrieval is not None:
_tools[VertexToolName.GOOGLE_SEARCH_RETRIEVAL.value] = googleSearchRetrieval
retrieval_tool = Tools()
retrieval_tool[VertexToolName.GOOGLE_SEARCH_RETRIEVAL.value] = googleSearchRetrieval
_tools_list.append(retrieval_tool)
if enterpriseWebSearch is not None:
_tools[VertexToolName.ENTERPRISE_WEB_SEARCH.value] = enterpriseWebSearch
enterprise_tool = Tools()
enterprise_tool[VertexToolName.ENTERPRISE_WEB_SEARCH.value] = enterpriseWebSearch
_tools_list.append(enterprise_tool)
if code_execution is not None:
_tools[VertexToolName.CODE_EXECUTION.value] = code_execution
code_tool = Tools()
code_tool[VertexToolName.CODE_EXECUTION.value] = code_execution
_tools_list.append(code_tool)
if urlContext is not None:
_tools[VertexToolName.URL_CONTEXT.value] = urlContext
url_tool = Tools()
url_tool[VertexToolName.URL_CONTEXT.value] = urlContext
_tools_list.append(url_tool)
if googleMaps is not None:
_tools[VertexToolName.GOOGLE_MAPS.value] = googleMaps
maps_tool = Tools()
maps_tool[VertexToolName.GOOGLE_MAPS.value] = googleMaps
_tools_list.append(maps_tool)
if computerUse is not None:
_tools[VertexToolName.COMPUTER_USE.value] = computerUse
computer_tool = Tools()
computer_tool[VertexToolName.COMPUTER_USE.value] = computerUse
_tools_list.append(computer_tool)
# Add retrieval config to toolConfig if googleMaps has location data
if google_maps_retrieval_config is not None:
@@ -579,7 +601,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
"retrievalConfig"
] = google_maps_retrieval_config
return [_tools]
return _tools_list
def _map_response_schema(self, value: dict) -> dict:
old_schema = deepcopy(value)
@@ -2279,3 +2279,194 @@ def test_partial_json_chunk_on_first_chunk():
assert result is None, "Partial first chunk should return None"
assert iterator.chunk_type == "accumulated_json", "Should switch to accumulated_json mode"
# ==================== Tool Type Separation Tests ====================
# These tests verify that each Tool object contains exactly one type per Vertex AI API spec
# Ref: https://cloud.google.com/vertex-ai/generative-ai/docs/reference/rest/v1beta1/Tool
def test_vertex_ai_multiple_tool_types_separate_objects():
"""
Test that multiple tool types are placed in separate Tool objects.
This is required by Vertex AI API spec:
"A Tool object should contain exactly one type of Tool"
Related error without this fix:
"tools[0].tool_type: one_of 'tool_type' has more than one initialized field:
enterprise_web_search, url_context"
Input:
value=[
{"enterpriseWebSearch": {}},
{"url_context": {}},
]
Expected Output:
tools=[
{"enterpriseWebSearch": {}}, # First Tool object
{"url_context": {}}, # Second Tool object (separate!)
]
NOT (incorrect - causes API error):
tools=[
{"enterpriseWebSearch": {}, "url_context": {}} # Multiple types in one object
]
"""
v = VertexGeminiConfig()
optional_params = {}
tools = v._map_function(
value=[
{"enterpriseWebSearch": {}},
{"url_context": {}},
],
optional_params=optional_params
)
# Should have 2 separate Tool objects
assert len(tools) == 2, f"Expected 2 separate Tool objects, got {len(tools)}"
# Each Tool object should contain exactly ONE type
tool_types_in_first = [k for k in tools[0].keys()]
tool_types_in_second = [k for k in tools[1].keys()]
assert len(tool_types_in_first) == 1, f"First Tool should have exactly 1 type, got {tool_types_in_first}"
assert len(tool_types_in_second) == 1, f"Second Tool should have exactly 1 type, got {tool_types_in_second}"
# Verify the correct tool types are present
assert "enterpriseWebSearch" in tools[0], "First Tool should contain enterpriseWebSearch"
assert "url_context" in tools[1], "Second Tool should contain url_context"
def test_vertex_ai_function_declarations_with_other_tools_separate():
"""
Test that function declarations and other tool types are in separate Tool objects.
This ensures that when using both function calling AND special tools like
google_search or code_execution, they are properly separated per API spec.
Input:
value=[
{"type": "function", "function": {"name": "get_weather", "description": "Get weather"}},
{"googleSearch": {}},
{"code_execution": {}},
]
Expected Output:
tools=[
{"function_declarations": [{"name": "get_weather", "description": "Get weather"}]},
{"googleSearch": {}},
{"code_execution": {}},
]
"""
v = VertexGeminiConfig()
optional_params = {}
tools = v._map_function(
value=[
{"type": "function", "function": {"name": "get_weather", "description": "Get weather"}},
{"googleSearch": {}},
{"code_execution": {}},
],
optional_params=optional_params
)
# Should have 3 separate Tool objects
assert len(tools) == 3, f"Expected 3 separate 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
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"
def test_vertex_ai_single_tool_type_still_works():
"""
Test that single tool type usage still works correctly (backward compatibility).
Input:
value=[{"code_execution": {}}]
Expected Output:
tools=[{"code_execution": {}}]
"""
v = VertexGeminiConfig()
optional_params = {}
tools = v._map_function(
value=[{"code_execution": {}}],
optional_params=optional_params
)
assert len(tools) == 1
assert "code_execution" in tools[0]
assert tools[0]["code_execution"] == {}
def test_vertex_ai_multiple_function_declarations_grouped():
"""
Test that multiple function declarations are grouped in ONE Tool object.
Function declarations are the exception - they CAN be grouped together
in a single Tool object (up to 512 declarations).
Input:
value=[
{"type": "function", "function": {"name": "func1", "description": "First function"}},
{"type": "function", "function": {"name": "func2", "description": "Second function"}},
]
Expected Output:
tools=[
{
"function_declarations": [
{"name": "func1", "description": "First function"},
{"name": "func2", "description": "Second function"},
]
}
]
"""
v = VertexGeminiConfig()
optional_params = {}
tools = v._map_function(
value=[
{"type": "function", "function": {"name": "func1", "description": "First function"}},
{"type": "function", "function": {"name": "func2", "description": "Second function"}},
],
optional_params=optional_params
)
# Should have only 1 Tool object (function declarations grouped)
assert len(tools) == 1, f"Expected 1 Tool object for grouped functions, got {len(tools)}"
# Should contain function_declarations with 2 functions
assert "function_declarations" in tools[0]
assert len(tools[0]["function_declarations"]) == 2
# Verify function names
func_names = [f["name"] for f in tools[0]["function_declarations"]]
assert "func1" in func_names
assert "func2" in func_names