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 e4a68dd82e0..37a4ab84dda 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 @@ -305,9 +305,9 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): return None for tool in value: - openai_function_object: Optional[ChatCompletionToolParamFunctionChunk] = ( - None - ) + openai_function_object: Optional[ + ChatCompletionToolParamFunctionChunk + ] = None if "function" in tool: # tools list _openai_function_object = ChatCompletionToolParamFunctionChunk( # type: ignore **tool["function"] @@ -597,14 +597,14 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): elif param == "seed": optional_params["seed"] = value elif param == "reasoning_effort" and isinstance(value, str): - optional_params["thinkingConfig"] = ( - VertexGeminiConfig._map_reasoning_effort_to_thinking_budget(value) - ) + optional_params[ + "thinkingConfig" + ] = VertexGeminiConfig._map_reasoning_effort_to_thinking_budget(value) elif param == "thinking": - optional_params["thinkingConfig"] = ( - VertexGeminiConfig._map_thinking_param( - cast(AnthropicThinkingParam, value) - ) + optional_params[ + "thinkingConfig" + ] = VertexGeminiConfig._map_thinking_param( + cast(AnthropicThinkingParam, value) ) elif param == "modalities" and isinstance(value, list): response_modalities = self.map_response_modalities(value) @@ -854,7 +854,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): function = _function_chunk else: _tool_response_chunk = ChatCompletionToolCallChunk( - id=f"call_{str(uuid.uuid4())}", + id=f"call_{uuid.uuid4().hex[:28]}", type="function", function=_function_chunk, index=cumulative_tool_call_idx, @@ -1077,10 +1077,8 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): elif ( finish_reason and finish_reason in mapped_finish_reason.keys() ): # vertex ai - return mapped_finish_reason[finish_reason] else: - return "stop" @staticmethod @@ -1175,12 +1173,14 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): if reasoning_content is not None: chat_completion_message["reasoning_content"] = reasoning_content - functions, tools, cumulative_tool_call_index = ( - VertexGeminiConfig._transform_parts( - parts=candidate["content"]["parts"], - cumulative_tool_call_idx=cumulative_tool_call_index, - is_function_call=is_function_call(standard_optional_params), - ) + ( + functions, + tools, + cumulative_tool_call_index, + ) = VertexGeminiConfig._transform_parts( + parts=candidate["content"]["parts"], + cumulative_tool_call_idx=cumulative_tool_call_index, + is_function_call=is_function_call(standard_optional_params), ) if "logprobsResult" in candidate: @@ -1344,28 +1344,28 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): ## ADD METADATA TO RESPONSE ## setattr(model_response, "vertex_ai_grounding_metadata", grounding_metadata) - model_response._hidden_params["vertex_ai_grounding_metadata"] = ( - grounding_metadata - ) + model_response._hidden_params[ + "vertex_ai_grounding_metadata" + ] = grounding_metadata setattr( model_response, "vertex_ai_url_context_metadata", url_context_metadata ) - model_response._hidden_params["vertex_ai_url_context_metadata"] = ( - url_context_metadata - ) + model_response._hidden_params[ + "vertex_ai_url_context_metadata" + ] = url_context_metadata setattr(model_response, "vertex_ai_safety_results", safety_ratings) - model_response._hidden_params["vertex_ai_safety_results"] = ( - safety_ratings # older approach - maintaining to prevent regressions - ) + model_response._hidden_params[ + "vertex_ai_safety_results" + ] = safety_ratings # older approach - maintaining to prevent regressions ## ADD CITATION METADATA ## setattr(model_response, "vertex_ai_citation_metadata", citation_metadata) - model_response._hidden_params["vertex_ai_citation_metadata"] = ( - citation_metadata # older approach - maintaining to prevent regressions - ) + model_response._hidden_params[ + "vertex_ai_citation_metadata" + ] = citation_metadata # older approach - maintaining to prevent regressions except Exception as e: raise VertexAIError( 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 21395f59e5c..1c2ebe163d9 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 @@ -1,5 +1,6 @@ import asyncio import json +import re from copy import deepcopy from typing import List, cast from unittest.mock import MagicMock, patch @@ -921,3 +922,106 @@ def test_vertex_ai_process_candidates_with_grounding_metadata(): print(result) assert isinstance(result[0], list) assert len(result[0]) == 1 + + +def test_vertex_ai_tool_call_id_format(): + """ + Test that tool call IDs have the correct format and length. + + The ID should be in format 'call_' + 28 hex characters (total 33 characters). + This test verifies the fix for keeping the code line under 40 characters. + """ + from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import ( + VertexGeminiConfig, + ) + from litellm.types.llms.vertex_ai import HttpxPartType + + # Create parts with function calls + parts_with_functions = [ + HttpxPartType( + functionCall={ + "name": "get_weather", + "args": {"location": "San Francisco", "unit": "celsius"}, + } + ), + HttpxPartType( + functionCall={ + "name": "get_time", + "args": {"timezone": "PST"} + } + ), + ] + + function, tools, updated_idx = VertexGeminiConfig._transform_parts( + parts=parts_with_functions, cumulative_tool_call_idx=0, is_function_call=False + ) + + # Verify tools were created + assert function is None + assert tools is not None + assert len(tools) == 2 + + # Test ID format for both tool calls + for tool in tools: + tool_id = tool["id"] + + # Should start with 'call_' + assert tool_id.startswith("call_"), f"ID should start with 'call_', got: {tool_id}" + + # Should have exactly 33 total characters (call_ + 28 hex chars) + assert len(tool_id) == 33, f"ID should be 33 characters long, got {len(tool_id)}: {tool_id}" + + # The part after 'call_' should be 28 hex characters + hex_part = tool_id[5:] # Remove 'call_' prefix + assert len(hex_part) == 28, f"Hex part should be 28 characters, got {len(hex_part)}: {hex_part}" + + # Should only contain valid hex characters + assert re.match(r'^[0-9a-f]{28}$', hex_part), f"Should contain only lowercase hex chars, got: {hex_part}" + + # Verify IDs are unique + assert tools[0]["id"] != tools[1]["id"], "Tool call IDs should be unique" + + # Test with multiple generations to ensure uniqueness + ids_generated = set() + for _ in range(10): + _, test_tools, _ = VertexGeminiConfig._transform_parts( + parts=[HttpxPartType(functionCall={"name": "test", "args": {}})], + cumulative_tool_call_idx=0, + is_function_call=False, + ) + if test_tools: + ids_generated.add(test_tools[0]["id"]) + + # All generated IDs should be unique + assert len(ids_generated) == 10, f"All 10 IDs should be unique, got {len(ids_generated)} unique IDs" + + +def test_vertex_ai_code_line_length(): + """ + Test that the specific code line generating tool call IDs is within character limit. + + This is a meta-test to ensure the code change meets the 40-character requirement. + """ + import inspect + from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import ( + VertexGeminiConfig, + ) + + # Get the source code of the _transform_parts method + source_lines = inspect.getsource(VertexGeminiConfig._transform_parts).split('\n') + + # Find the line that generates the ID + id_line = None + for line in source_lines: + if 'id=f"call_{uuid.uuid4().hex' in line: + id_line = line.strip() # Remove indentation for length check + break + + assert id_line is not None, "Could not find the ID generation line in source code" + + # Check that the line is 40 characters or less (excluding indentation) + line_length = len(id_line) + assert line_length <= 40, f"ID generation line is {line_length} characters, should be ≤40: {id_line}" + + # Verify it contains the expected UUID format + assert 'uuid.uuid4().hex[:28]' in id_line, f"Line should contain shortened UUID format: {id_line}"