mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-12 23:01:41 +00:00
fix(gemini): convert prefixItems to items and ensure arrays have items field
Gemini does not support JSON Schema prefixItems (tuple validation, draft 2020-12). Pydantic generates prefixItems for Python tuple types like tuple[str, str, str], causing Gemini to reject the tool schema with "items.items: missing field". Changes: - Convert prefixItems to a single items schema using the common type across all prefix entries (falls back to string for mixed types) - Ensure type=array schemas always have an items field Reproducer: any tool with a list[tuple[str, ...]] parameter
This commit is contained in:
parent
b8f7d61400
commit
452061ec8b
2 changed files with 181 additions and 0 deletions
|
|
@ -583,8 +583,32 @@ def process_items(schema, depth=0):
|
|||
f"Max depth of {DEFAULT_MAX_RECURSE_DEPTH} exceeded while processing schema. Please check the schema for excessive nesting."
|
||||
)
|
||||
if isinstance(schema, dict):
|
||||
# Convert prefixItems (JSON Schema tuple validation) to items.
|
||||
# Gemini does not support prefixItems; collapse to a single items
|
||||
# schema using the common type if all prefix items share one, else string.
|
||||
if "prefixItems" in schema and "items" not in schema:
|
||||
prefix = schema.pop("prefixItems")
|
||||
if isinstance(prefix, list) and prefix:
|
||||
types = {
|
||||
item.get("type") for item in prefix if isinstance(item, dict) and "type" in item
|
||||
}
|
||||
if len(types) == 1:
|
||||
schema["items"] = {"type": types.pop()}
|
||||
else:
|
||||
schema["items"] = {"type": "string"}
|
||||
else:
|
||||
schema["items"] = {"type": "string"}
|
||||
elif "prefixItems" in schema:
|
||||
# items already exists; just drop prefixItems
|
||||
schema.pop("prefixItems")
|
||||
|
||||
if "items" in schema and schema["items"] == {}:
|
||||
schema["items"] = {"type": "object"}
|
||||
|
||||
# Ensure type=array always has an items field (Gemini requires it)
|
||||
if schema.get("type") == "array" and "items" not in schema:
|
||||
schema["items"] = {"type": "string"}
|
||||
|
||||
for key, value in schema.items():
|
||||
if isinstance(value, dict):
|
||||
process_items(value, depth + 1)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,157 @@
|
|||
def test_process_items_converts_prefixitems_to_items():
|
||||
"""
|
||||
Test that prefixItems (JSON Schema tuple validation) is converted to a
|
||||
single items schema for Gemini compatibility.
|
||||
|
||||
Pydantic generates prefixItems for tuple types (e.g. tuple[str, str, str]).
|
||||
Gemini does not support prefixItems and requires items on array types.
|
||||
"""
|
||||
from litellm.llms.vertex_ai.common_utils import process_items
|
||||
|
||||
# Tuple of 3 strings: tuple[str, str, str]
|
||||
schema = {
|
||||
"type": "array",
|
||||
"prefixItems": [
|
||||
{"type": "string"},
|
||||
{"type": "string"},
|
||||
{"type": "string"},
|
||||
],
|
||||
"minItems": 3,
|
||||
"maxItems": 3,
|
||||
}
|
||||
process_items(schema)
|
||||
assert "prefixItems" not in schema
|
||||
assert schema["items"] == {"type": "string"}
|
||||
|
||||
|
||||
def test_process_items_prefixitems_mixed_types():
|
||||
"""
|
||||
Test that prefixItems with mixed types falls back to type string.
|
||||
"""
|
||||
from litellm.llms.vertex_ai.common_utils import process_items
|
||||
|
||||
schema = {
|
||||
"type": "array",
|
||||
"prefixItems": [
|
||||
{"type": "string"},
|
||||
{"type": "integer"},
|
||||
],
|
||||
}
|
||||
process_items(schema)
|
||||
assert "prefixItems" not in schema
|
||||
assert schema["items"] == {"type": "string"}
|
||||
|
||||
|
||||
def test_process_items_prefixitems_preserves_existing_items():
|
||||
"""
|
||||
Test that prefixItems is dropped when items already exists.
|
||||
"""
|
||||
from litellm.llms.vertex_ai.common_utils import process_items
|
||||
|
||||
schema = {
|
||||
"type": "array",
|
||||
"prefixItems": [{"type": "string"}],
|
||||
"items": {"type": "integer"},
|
||||
}
|
||||
process_items(schema)
|
||||
assert "prefixItems" not in schema
|
||||
assert schema["items"] == {"type": "integer"}
|
||||
|
||||
|
||||
def test_process_items_array_without_items_gets_default():
|
||||
"""
|
||||
Test that type=array schemas without items get a default items field.
|
||||
Gemini requires items on all array schemas.
|
||||
"""
|
||||
from litellm.llms.vertex_ai.common_utils import process_items
|
||||
|
||||
schema = {"type": "array"}
|
||||
process_items(schema)
|
||||
assert schema["items"] == {"type": "string"}
|
||||
|
||||
|
||||
def test_process_items_nested_prefixitems_in_anyof():
|
||||
"""
|
||||
Test that prefixItems conversion works inside anyOf entries, matching
|
||||
the real-world pattern from Pydantic's schema for list[tuple[str, str, str]] | None.
|
||||
"""
|
||||
from litellm.llms.vertex_ai.common_utils import process_items
|
||||
|
||||
schema = {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"prefixItems": [
|
||||
{"type": "string"},
|
||||
{"type": "string"},
|
||||
{"type": "string"},
|
||||
],
|
||||
"minItems": 3,
|
||||
"maxItems": 3,
|
||||
},
|
||||
},
|
||||
{"type": "null"},
|
||||
],
|
||||
}
|
||||
process_items(schema)
|
||||
inner = schema["anyOf"][0]["items"]
|
||||
assert "prefixItems" not in inner
|
||||
assert inner["items"] == {"type": "string"}
|
||||
|
||||
|
||||
def test_build_vertex_schema_with_tuple_filters():
|
||||
"""
|
||||
End-to-end test: a tool schema with list[tuple[str, str, str]] | None
|
||||
parameter should produce a valid Gemini schema with items on all arrays.
|
||||
|
||||
Reproduces: GenerateContentRequest.tools[0].function_declarations[N]
|
||||
.parameters.properties[filters].any_of[0].items.items: missing field
|
||||
"""
|
||||
from copy import deepcopy
|
||||
|
||||
from litellm.llms.vertex_ai.common_utils import _build_vertex_schema
|
||||
|
||||
# Schema generated by Pydantic for: filters: list[tuple[str, str, str]] | None
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filters": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"maxItems": 3,
|
||||
"minItems": 3,
|
||||
"prefixItems": [
|
||||
{"type": "string"},
|
||||
{"type": "string"},
|
||||
{"type": "string"},
|
||||
],
|
||||
"type": "array",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
{"type": "null"},
|
||||
],
|
||||
"default": None,
|
||||
"title": "Filters",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
result = _build_vertex_schema(deepcopy(schema))
|
||||
filters = result["properties"]["filters"]
|
||||
|
||||
# Should have anyOf with the array variant
|
||||
assert "anyOf" in filters
|
||||
array_variant = filters["anyOf"][0]
|
||||
assert array_variant["type"] == "array"
|
||||
assert "items" in array_variant
|
||||
|
||||
# Inner array (the tuple) must also have items
|
||||
inner = array_variant["items"]
|
||||
assert inner["type"] == "array"
|
||||
assert "items" in inner
|
||||
assert inner["items"]["type"] == "string"
|
||||
assert "prefixItems" not in inner
|
||||
Loading…
Add table
Reference in a new issue