[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
This commit is contained in:
Ishaan Jaff 2025-09-01 17:05:01 -07:00
parent 5802dfb93d
commit 6e90f12e64
4 changed files with 139 additions and 1 deletions

View file

@ -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)

View file

@ -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.,
]

View file

@ -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"""

View file

@ -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"]
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"