Skip to content

Commit 4c7b3e1

Browse files
authored
[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
1 parent 7656cb3 commit 4c7b3e1

File tree

4 files changed

+139
-1
lines changed

4 files changed

+139
-1
lines changed

litellm/llms/vertex_ai/common_utils.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,25 @@ def _check_text_in_content(parts: List[PartType]) -> bool:
187187
return has_text_param
188188

189189

190+
def _fix_enum_empty_strings(schema, depth=0):
191+
"""Fix empty strings in enum values by replacing them with None. Gemini doesn't accept empty strings in enums."""
192+
if depth > DEFAULT_MAX_RECURSE_DEPTH:
193+
raise ValueError(f"Max depth of {DEFAULT_MAX_RECURSE_DEPTH} exceeded while processing schema.")
194+
195+
if "enum" in schema and isinstance(schema["enum"], list):
196+
schema["enum"] = [None if value == "" else value for value in schema["enum"]]
197+
198+
# Reuse existing recursion pattern from convert_anyof_null_to_nullable
199+
properties = schema.get("properties", None)
200+
if properties is not None:
201+
for _, value in properties.items():
202+
_fix_enum_empty_strings(value, depth=depth + 1)
203+
204+
items = schema.get("items", None)
205+
if items is not None:
206+
_fix_enum_empty_strings(items, depth=depth + 1)
207+
208+
190209
def _build_vertex_schema(parameters: dict, add_property_ordering: bool = False):
191210
"""
192211
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):
217236

218237
_convert_schema_types(parameters)
219238

239+
# Handle empty strings in enum values - Gemini doesn't accept empty strings in enums
240+
_fix_enum_empty_strings(parameters)
241+
220242
# Handle empty items objects
221243
process_items(parameters)
222244
add_object_type(parameters)

tests/code_coverage_tests/recursive_detector.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"_extract_fields_recursive", # max depth set.
2828
"_remove_json_schema_refs", # max depth set.,
2929
"_convert_schema_types", # max depth set.,
30+
"_fix_enum_empty_strings", # max depth set.,
3031
]
3132

3233

tests/llm_translation/base_llm_unit_tests.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,62 @@ def test_tool_call_with_property_type_array(self):
186186
print(response)
187187
print(json.dumps(response, indent=4, default=str))
188188

189+
def test_tool_call_with_empty_enum_property(self):
190+
litellm._turn_on_debug()
191+
from litellm.utils import supports_function_calling
192+
os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True"
193+
litellm.model_cost = litellm.get_model_cost_map(url="")
194+
195+
base_completion_call_args = self.get_base_completion_call_args()
196+
if not supports_function_calling(base_completion_call_args["model"], None):
197+
print("Model does not support function calling")
198+
pytest.skip("Model does not support function calling")
199+
base_completion_call_args = self.get_base_completion_call_args()
200+
response = self.completion_function(
201+
**base_completion_call_args,
202+
messages = [
203+
{
204+
"role": "user",
205+
"content": "Search for the latest iPhone models and tell me which storage options are available."
206+
}
207+
],
208+
tools = [
209+
{
210+
"type": "function",
211+
"function": {
212+
"name": "litellm_product_search",
213+
"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",
214+
"parameters": {
215+
"properties": {
216+
"search_mode": {
217+
"default": "",
218+
"description": "The search strategy to use for finding products.",
219+
"enum": [
220+
"",
221+
"product_search",
222+
"product_search_with_filters",
223+
"product_search_with_sorting",
224+
"product_search_with_pagination",
225+
"product_search_with_aggregation",
226+
],
227+
"title": "Search Mode",
228+
"type": "string"
229+
},
230+
},
231+
"required": [
232+
"search_mode"
233+
],
234+
"title": "product_search_arguments",
235+
"type": "object"
236+
}
237+
}
238+
}
239+
]
240+
)
241+
print(response)
242+
print(json.dumps(response, indent=4, default=str))
243+
244+
189245

190246
def test_streaming(self):
191247
"""Check if litellm handles streaming correctly"""

tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -741,4 +741,63 @@ def test_convert_schema_types_type_array_conversion():
741741

742742
# 4. Other properties preserved
743743
assert input_schema["properties"]["studio"]["description"] == "The studio ID or name"
744-
assert input_schema["required"] == ["studio"]
744+
assert input_schema["required"] == ["studio"]
745+
746+
747+
def test_fix_enum_empty_strings():
748+
"""
749+
Test _fix_enum_empty_strings function replaces empty strings with None in enum arrays.
750+
751+
This test verifies the fix for the issue where Gemini rejects tool definitions
752+
with empty strings in enum values, causing API failures.
753+
754+
Relevant issue: Gemini does not accept empty strings in enum values
755+
"""
756+
from litellm.llms.vertex_ai.common_utils import _fix_enum_empty_strings
757+
758+
# Input: Schema with empty string in enum (the problematic case)
759+
input_schema = {
760+
"type": "object",
761+
"properties": {
762+
"user_agent_type": {
763+
"enum": ["", "desktop", "mobile", "tablet"],
764+
"type": "string",
765+
"description": "Device type for user agent"
766+
}
767+
},
768+
"required": ["user_agent_type"]
769+
}
770+
771+
# Expected output: Empty strings replaced with None
772+
expected_output = {
773+
"type": "object",
774+
"properties": {
775+
"user_agent_type": {
776+
"enum": [None, "desktop", "mobile", "tablet"],
777+
"type": "string",
778+
"description": "Device type for user agent"
779+
}
780+
},
781+
"required": ["user_agent_type"]
782+
}
783+
784+
# Apply the transformation
785+
_fix_enum_empty_strings(input_schema)
786+
787+
# Verify the transformation
788+
assert input_schema == expected_output
789+
790+
# Verify specific transformations:
791+
# 1. Empty string replaced with None
792+
enum_values = input_schema["properties"]["user_agent_type"]["enum"]
793+
assert "" not in enum_values
794+
assert None in enum_values
795+
796+
# 2. Other enum values preserved
797+
assert "desktop" in enum_values
798+
assert "mobile" in enum_values
799+
assert "tablet" in enum_values
800+
801+
# 3. Other properties preserved
802+
assert input_schema["properties"]["user_agent_type"]["type"] == "string"
803+
assert input_schema["properties"]["user_agent_type"]["description"] == "Device type for user agent"

0 commit comments

Comments
 (0)