This commit is contained in:
Bharadwaj Pendyala 2026-08-26 21:06:13 -04:00 committed by GitHub
commit a2fff49c0f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 100 additions and 0 deletions

View file

@ -597,6 +597,8 @@ def _build_vertex_schema(parameters: dict, add_property_ordering: bool = False):
# refs recursively and correctly detects/skips circular references.
unpack_defs(parameters, defs)
_normalize_tuple_items(parameters)
# 5. Nullable fields:
# * https://github.com/pydantic/pydantic/issues/1270
# * https://stackoverflow.com/a/58841311
@ -770,6 +772,39 @@ def filter_schema_fields(schema_dict: dict[str, Any], valid_fields: set[str], pr
return result
def _normalize_tuple_items(schema: dict[str, Any], depth: int = 0) -> None:
"""Rewrite tuple-form `items` (a list of sub-schemas) as `anyOf`.
JSON Schema allows it, Vertex's Schema type does not, and every walker below
reads `items` as a single sub-schema, so a list there crashes them.
"""
if depth > DEFAULT_MAX_RECURSE_DEPTH:
raise ValueError(f"Max depth of {DEFAULT_MAX_RECURSE_DEPTH} exceeded while processing schema.")
tuple_items: Final = schema.get("items", None)
if isinstance(tuple_items, list):
if tuple_items:
schema["items"] = {"anyOf": tuple_items} # mutable-ok: the walkers below rewrite this subtree in place
else:
schema.pop("items")
properties: Final = schema.get("properties", None)
if properties is not None:
for value in properties.values():
_normalize_tuple_items(value, depth=depth + 1)
items: Final = schema.get("items", None)
if items is not None:
_normalize_tuple_items(items, depth=depth + 1)
for key in ("anyOf", "oneOf", "allOf"):
values = schema.get(key, None)
if values is not None and isinstance(values, list):
for value in values:
if isinstance(value, dict):
_normalize_tuple_items(value, depth=depth + 1)
def convert_anyof_null_to_nullable(schema, depth=0):
if depth > DEFAULT_MAX_RECURSE_DEPTH:
raise ValueError(

View file

@ -11,6 +11,7 @@ IGNORE_FUNCTIONS = [
"clean_message",
"unpack_defs",
"convert_anyof_null_to_nullable", # has a set max depth
"_normalize_tuple_items", # has a set max depth
"add_object_type",
"strip_field",
"_transform_prompt",

View file

@ -372,6 +372,70 @@ def test_build_vertex_schema_array_branch_missing_items_in_anyof():
}, f"array branch must have items synthesized; got {branch}"
def test_build_vertex_schema_tuple_form_items_become_anyof():
"""
Regression: tuple-form `items` (`"items": [{...}, {...}]`, valid JSON Schema)
used to crash the transform with `'list' object has no attribute 'get'`,
surfacing as litellm.APIConnectionError on every vertex_ai request carrying
such a tool schema.
"""
from litellm.llms.vertex_ai.common_utils import _build_vertex_schema
parameters = {
"type": "object",
"properties": {
"pair": {
"type": "array",
"items": [{"type": "integer"}, {"type": "string"}],
}
},
}
result = _build_vertex_schema(parameters)
assert result["properties"]["pair"]["items"] == {
"anyOf": [{"type": "integer"}, {"type": "string"}]
}
def test_build_vertex_schema_tuple_form_items_nested_in_anyof_branch():
"""Tuple-form `items` on an anyOf branch is reachable only if the walk
descends into anyOf, which is where the original crash was raised."""
from litellm.llms.vertex_ai.common_utils import _build_vertex_schema
parameters = {
"type": "object",
"properties": {
"pair": {
"anyOf": [
{"type": "array", "items": [{"type": "integer"}]},
{"type": "null"},
]
}
},
}
result = _build_vertex_schema(parameters)
array_branch = result["properties"]["pair"]["anyOf"][0]
assert array_branch["items"] == {"anyOf": [{"type": "integer"}]}
def test_build_vertex_schema_empty_tuple_form_items():
"""An empty `items: []` has no branches to fold into anyOf, so it takes the
same path as `items: {}` rather than raising on a zero-length anyOf."""
from litellm.llms.vertex_ai.common_utils import _build_vertex_schema
parameters = {
"type": "object",
"properties": {"pair": {"type": "array", "items": []}},
}
result = _build_vertex_schema(parameters)
assert result["properties"]["pair"]["items"] == {"type": "object"}
def test_vertex_ai_complex_response_schema():
import json
from copy import deepcopy