mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-21 00:21:38 +00:00
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:
co-authored by
Claude Opus 4.5
parent
c7734cb8bd
commit
f3f1acd338
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user