Skip to content

Commit 6dcb542

Browse files
authored
fix(vertex): fix CreateCachedContentRequest enum error (#16965)
* feat: add _fix_enum_types function to remove enums from non-string fields in schema * test: add test for _fix_enum_types function to validate enum removal from non-string fields
1 parent babee43 commit 6dcb542

File tree

2 files changed

+172
-0
lines changed

2 files changed

+172
-0
lines changed

litellm/llms/vertex_ai/common_utils.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,57 @@ def _fix_enum_empty_strings(schema, depth=0):
274274
_fix_enum_empty_strings(items, depth=depth + 1)
275275

276276

277+
def _fix_enum_types(schema, depth=0):
278+
"""Remove `enum` fields when the schema type is not string.
279+
280+
Gemini / Vertex APIs only allow enums for string-typed fields. When an enum
281+
is present on a non-string typed property (or when `anyOf` types do not
282+
include a string type), remove the enum to avoid provider validation errors.
283+
"""
284+
if depth > DEFAULT_MAX_RECURSE_DEPTH:
285+
raise ValueError(
286+
f"Max depth of {DEFAULT_MAX_RECURSE_DEPTH} exceeded while processing schema."
287+
)
288+
289+
if not isinstance(schema, dict):
290+
return
291+
292+
# If enum exists but type is not string (and anyOf doesn't include string), drop enum
293+
if "enum" in schema and isinstance(schema["enum"], list):
294+
schema_type = schema.get("type")
295+
keep_enum = False
296+
if isinstance(schema_type, str) and schema_type.lower() == "string":
297+
keep_enum = True
298+
else:
299+
anyof = schema.get("anyOf")
300+
if isinstance(anyof, list):
301+
for item in anyof:
302+
if isinstance(item, dict):
303+
item_type = item.get("type")
304+
if isinstance(item_type, str) and item_type.lower() == "string":
305+
keep_enum = True
306+
break
307+
308+
if not keep_enum:
309+
schema.pop("enum", None)
310+
311+
# Recurse into nested structures
312+
properties = schema.get("properties", None)
313+
if properties is not None:
314+
for _, value in properties.items():
315+
_fix_enum_types(value, depth=depth + 1)
316+
317+
items = schema.get("items", None)
318+
if items is not None:
319+
_fix_enum_types(items, depth=depth + 1)
320+
321+
anyof = schema.get("anyOf", None)
322+
if anyof is not None and isinstance(anyof, list):
323+
for item in anyof:
324+
if isinstance(item, dict):
325+
_fix_enum_types(item, depth=depth + 1)
326+
327+
277328
def _build_vertex_schema(parameters: dict, add_property_ordering: bool = False):
278329
"""
279330
This is a modified version of https://github.com/google-gemini/generative-ai-python/blob/8f77cc6ac99937cd3a81299ecf79608b91b06bbb/google/generativeai/types/content_types.py#L419
@@ -307,6 +358,9 @@ def _build_vertex_schema(parameters: dict, add_property_ordering: bool = False):
307358
# Handle empty strings in enum values - Gemini doesn't accept empty strings in enums
308359
_fix_enum_empty_strings(parameters)
309360

361+
# Remove enums for non-string typed fields (Gemini requires enum only on strings)
362+
_fix_enum_types(parameters)
363+
310364
# Handle empty items objects
311365
process_items(parameters)
312366
add_object_type(parameters)

tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,124 @@ def test_fix_enum_empty_strings():
803803
assert input_schema["properties"]["user_agent_type"]["description"] == "Device type for user agent"
804804

805805

806+
def test_fix_enum_types():
807+
"""
808+
Test _fix_enum_types function removes enum fields when type is not string.
809+
810+
This test verifies the fix for the issue where Gemini rejects cached content
811+
with function parameter enums on non-string types, causing API failures.
812+
813+
Relevant issue: Gemini only allows enums for string-typed fields
814+
"""
815+
from litellm.llms.vertex_ai.common_utils import _fix_enum_types
816+
817+
# Input: Schema with enum on non-string type (the problematic case)
818+
input_schema = {
819+
"type": "object",
820+
"properties": {
821+
"truncateMode": {
822+
"enum": ["auto", "none", "start", "end"],
823+
"type": "string", # This should keep the enum
824+
"description": "How to truncate content"
825+
},
826+
"maxLength": {
827+
"enum": [100, 200, 500], # This should be removed
828+
"type": "integer",
829+
"description": "Maximum length"
830+
},
831+
"enabled": {
832+
"enum": [True, False], # This should be removed
833+
"type": "boolean",
834+
"description": "Whether feature is enabled"
835+
},
836+
"nested": {
837+
"type": "object",
838+
"properties": {
839+
"innerEnum": {
840+
"enum": ["a", "b", "c"], # This should be kept
841+
"type": "string"
842+
},
843+
"innerNonStringEnum": {
844+
"enum": [1, 2, 3], # This should be removed
845+
"type": "integer"
846+
}
847+
}
848+
},
849+
"anyOfField": {
850+
"anyOf": [
851+
{"type": "string", "enum": ["option1", "option2"]}, # This should be kept
852+
{"type": "integer", "enum": [1, 2, 3]} # This should be removed
853+
]
854+
}
855+
}
856+
}
857+
858+
# Expected output: Non-string enums removed, string enums kept
859+
expected_output = {
860+
"type": "object",
861+
"properties": {
862+
"truncateMode": {
863+
"enum": ["auto", "none", "start", "end"], # Kept - string type
864+
"type": "string",
865+
"description": "How to truncate content"
866+
},
867+
"maxLength": { # enum removed
868+
"type": "integer",
869+
"description": "Maximum length"
870+
},
871+
"enabled": { # enum removed
872+
"type": "boolean",
873+
"description": "Whether feature is enabled"
874+
},
875+
"nested": {
876+
"type": "object",
877+
"properties": {
878+
"innerEnum": {
879+
"enum": ["a", "b", "c"], # Kept - string type
880+
"type": "string"
881+
},
882+
"innerNonStringEnum": { # enum removed
883+
"type": "integer"
884+
}
885+
}
886+
},
887+
"anyOfField": {
888+
"anyOf": [
889+
{"type": "string", "enum": ["option1", "option2"]}, # Kept - has string type
890+
{"type": "integer"} # enum removed
891+
]
892+
}
893+
}
894+
}
895+
896+
# Apply the transformation
897+
_fix_enum_types(input_schema)
898+
899+
# Verify the transformation
900+
assert input_schema == expected_output
901+
902+
# Verify specific transformations:
903+
# 1. String enums are preserved
904+
assert "enum" in input_schema["properties"]["truncateMode"]
905+
assert input_schema["properties"]["truncateMode"]["enum"] == ["auto", "none", "start", "end"]
906+
907+
assert "enum" in input_schema["properties"]["nested"]["properties"]["innerEnum"]
908+
assert input_schema["properties"]["nested"]["properties"]["innerEnum"]["enum"] == ["a", "b", "c"]
909+
910+
# 2. Non-string enums are removed
911+
assert "enum" not in input_schema["properties"]["maxLength"]
912+
assert "enum" not in input_schema["properties"]["enabled"]
913+
assert "enum" not in input_schema["properties"]["nested"]["properties"]["innerNonStringEnum"]
914+
915+
# 3. anyOf with string type keeps enum, non-string removes it
916+
assert "enum" in input_schema["properties"]["anyOfField"]["anyOf"][0]
917+
assert "enum" not in input_schema["properties"]["anyOfField"]["anyOf"][1]
918+
919+
# 4. Other properties preserved
920+
assert input_schema["properties"]["maxLength"]["type"] == "integer"
921+
assert input_schema["properties"]["enabled"]["type"] == "boolean"
922+
923+
806924
def test_get_token_url():
807925
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import (
808926
VertexLLM,

0 commit comments

Comments
 (0)