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:
espinetandreu 2026-04-15 00:07:16 +02:00
parent b8f7d61400
commit 452061ec8b
2 changed files with 181 additions and 0 deletions

View file

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

View file

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