From 6e90f12e645017f220ef0762b2b30c372bba0184 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 1 Sep 2025 17:05:01 -0700 Subject: [PATCH] [Bug Fix] Gemini Tool Calling - fix gemini empty enum property (#14155) * fix: _convert_schema_types * fix recursive detector * test_convert_schema_types_type_array_conversion * fix: DEFAULT_NUM_WORKERS_LITELLM_PROXY * add _fix_enum_empty_strings * test_tool_call_with_empty_enum_property * test_fix_enum_empty_strings * fix _fix_enum_empty_strings --- litellm/llms/vertex_ai/common_utils.py | 22 +++++++ .../code_coverage_tests/recursive_detector.py | 1 + tests/llm_translation/base_llm_unit_tests.py | 56 +++++++++++++++++ .../vertex_ai/test_vertex_ai_common_utils.py | 61 ++++++++++++++++++- 4 files changed, 139 insertions(+), 1 deletion(-) diff --git a/litellm/llms/vertex_ai/common_utils.py b/litellm/llms/vertex_ai/common_utils.py index 6c8eb887eb0..8588c3efa27 100644 --- a/litellm/llms/vertex_ai/common_utils.py +++ b/litellm/llms/vertex_ai/common_utils.py @@ -187,6 +187,25 @@ def _check_text_in_content(parts: List[PartType]) -> bool: return has_text_param +def _fix_enum_empty_strings(schema, depth=0): + """Fix empty strings in enum values by replacing them with None. Gemini doesn't accept empty strings in enums.""" + if depth > DEFAULT_MAX_RECURSE_DEPTH: + raise ValueError(f"Max depth of {DEFAULT_MAX_RECURSE_DEPTH} exceeded while processing schema.") + + if "enum" in schema and isinstance(schema["enum"], list): + schema["enum"] = [None if value == "" else value for value in schema["enum"]] + + # Reuse existing recursion pattern from convert_anyof_null_to_nullable + properties = schema.get("properties", None) + if properties is not None: + for _, value in properties.items(): + _fix_enum_empty_strings(value, depth=depth + 1) + + items = schema.get("items", None) + if items is not None: + _fix_enum_empty_strings(items, depth=depth + 1) + + def _build_vertex_schema(parameters: dict, add_property_ordering: bool = False): """ This is a modified version of https://github.com/google-gemini/generative-ai-python/blob/8f77cc6ac99937cd3a81299ecf79608b91b06bbb/google/generativeai/types/content_types.py#L419 @@ -217,6 +236,9 @@ def _build_vertex_schema(parameters: dict, add_property_ordering: bool = False): _convert_schema_types(parameters) + # Handle empty strings in enum values - Gemini doesn't accept empty strings in enums + _fix_enum_empty_strings(parameters) + # Handle empty items objects process_items(parameters) add_object_type(parameters) diff --git a/tests/code_coverage_tests/recursive_detector.py b/tests/code_coverage_tests/recursive_detector.py index c75f95b267f..ee948f13170 100644 --- a/tests/code_coverage_tests/recursive_detector.py +++ b/tests/code_coverage_tests/recursive_detector.py @@ -27,6 +27,7 @@ IGNORE_FUNCTIONS = [ "_extract_fields_recursive", # max depth set. "_remove_json_schema_refs", # max depth set., "_convert_schema_types", # max depth set., + "_fix_enum_empty_strings", # max depth set., ] diff --git a/tests/llm_translation/base_llm_unit_tests.py b/tests/llm_translation/base_llm_unit_tests.py index f2f785629c5..3c7d20ac950 100644 --- a/tests/llm_translation/base_llm_unit_tests.py +++ b/tests/llm_translation/base_llm_unit_tests.py @@ -186,6 +186,62 @@ class BaseLLMChatTest(ABC): print(response) print(json.dumps(response, indent=4, default=str)) + def test_tool_call_with_empty_enum_property(self): + litellm._turn_on_debug() + from litellm.utils import supports_function_calling + os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" + litellm.model_cost = litellm.get_model_cost_map(url="") + + base_completion_call_args = self.get_base_completion_call_args() + if not supports_function_calling(base_completion_call_args["model"], None): + print("Model does not support function calling") + pytest.skip("Model does not support function calling") + base_completion_call_args = self.get_base_completion_call_args() + response = self.completion_function( + **base_completion_call_args, + messages = [ + { + "role": "user", + "content": "Search for the latest iPhone models and tell me which storage options are available." + } + ], + tools = [ + { + "type": "function", + "function": { + "name": "litellm_product_search", + "description": "Search for product information and specifications.\n\nSupports filtering by category, brand, price range, and availability.\nCan retrieve detailed product specifications, pricing, and stock information.\nSupports different search modes and result formatting options.\n", + "parameters": { + "properties": { + "search_mode": { + "default": "", + "description": "The search strategy to use for finding products.", + "enum": [ + "", + "product_search", + "product_search_with_filters", + "product_search_with_sorting", + "product_search_with_pagination", + "product_search_with_aggregation", + ], + "title": "Search Mode", + "type": "string" + }, + }, + "required": [ + "search_mode" + ], + "title": "product_search_arguments", + "type": "object" + } + } + } + ] + ) + print(response) + print(json.dumps(response, indent=4, default=str)) + + def test_streaming(self): """Check if litellm handles streaming correctly""" diff --git a/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py b/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py index 18fffbed59b..02cd51920da 100644 --- a/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py +++ b/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py @@ -741,4 +741,63 @@ def test_convert_schema_types_type_array_conversion(): # 4. Other properties preserved assert input_schema["properties"]["studio"]["description"] == "The studio ID or name" - assert input_schema["required"] == ["studio"] \ No newline at end of file + assert input_schema["required"] == ["studio"] + + +def test_fix_enum_empty_strings(): + """ + Test _fix_enum_empty_strings function replaces empty strings with None in enum arrays. + + This test verifies the fix for the issue where Gemini rejects tool definitions + with empty strings in enum values, causing API failures. + + Relevant issue: Gemini does not accept empty strings in enum values + """ + from litellm.llms.vertex_ai.common_utils import _fix_enum_empty_strings + + # Input: Schema with empty string in enum (the problematic case) + input_schema = { + "type": "object", + "properties": { + "user_agent_type": { + "enum": ["", "desktop", "mobile", "tablet"], + "type": "string", + "description": "Device type for user agent" + } + }, + "required": ["user_agent_type"] + } + + # Expected output: Empty strings replaced with None + expected_output = { + "type": "object", + "properties": { + "user_agent_type": { + "enum": [None, "desktop", "mobile", "tablet"], + "type": "string", + "description": "Device type for user agent" + } + }, + "required": ["user_agent_type"] + } + + # Apply the transformation + _fix_enum_empty_strings(input_schema) + + # Verify the transformation + assert input_schema == expected_output + + # Verify specific transformations: + # 1. Empty string replaced with None + enum_values = input_schema["properties"]["user_agent_type"]["enum"] + assert "" not in enum_values + assert None in enum_values + + # 2. Other enum values preserved + assert "desktop" in enum_values + assert "mobile" in enum_values + assert "tablet" in enum_values + + # 3. Other properties preserved + assert input_schema["properties"]["user_agent_type"]["type"] == "string" + assert input_schema["properties"]["user_agent_type"]["description"] == "Device type for user agent"