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 a5cc3dca8c..ae4fabc6b6 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 @@ -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) 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 783d85f471..91a28ee6ec 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 @@ -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