mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-06 20:25:29 +00:00
Merge pull request #22589 from Chesars/fix/vertex-preserve-any-type-schema
fix(vertex): preserve type schema semantics for JsonValuefields
This commit is contained in:
@@ -571,14 +571,38 @@ def _filter_anyof_fields(schema_dict: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return schema_dict
|
||||
|
||||
|
||||
def _is_any_type_schema(schema: dict) -> bool:
|
||||
"""
|
||||
Detect schemas that represent "any JSON value" (no type constraints).
|
||||
|
||||
In JSON Schema, an empty schema {} means "any value is valid".
|
||||
Schemas with only metadata keys (title, description, default, examples)
|
||||
but no type-constraining keywords also represent "any type".
|
||||
|
||||
Gemini's Schema proto uses TYPE_UNSPECIFIED (0) as default,
|
||||
so omitting the type field is valid and means "any type".
|
||||
"""
|
||||
type_constraining_keys = {
|
||||
"type",
|
||||
"properties",
|
||||
"items",
|
||||
"anyOf",
|
||||
"oneOf",
|
||||
"allOf",
|
||||
"enum",
|
||||
"required",
|
||||
"$ref",
|
||||
"$schema",
|
||||
}
|
||||
return not any(key in type_constraining_keys for key in schema.keys())
|
||||
|
||||
|
||||
def process_items(schema, depth=0):
|
||||
if depth > DEFAULT_MAX_RECURSE_DEPTH:
|
||||
raise ValueError(
|
||||
f"Max depth of {DEFAULT_MAX_RECURSE_DEPTH} exceeded while processing schema. Please check the schema for excessive nesting."
|
||||
)
|
||||
if isinstance(schema, dict):
|
||||
if "items" in schema and schema["items"] == {}:
|
||||
schema["items"] = {"type": "object"}
|
||||
for key, value in schema.items():
|
||||
if isinstance(value, dict):
|
||||
process_items(value, depth + 1)
|
||||
@@ -677,9 +701,8 @@ def convert_anyof_null_to_nullable(schema, depth=0):
|
||||
# remove null type
|
||||
anyof.remove(atype)
|
||||
contains_null = True
|
||||
elif "type" not in atype and len(atype) == 0:
|
||||
# Handle empty object case
|
||||
atype["type"] = "object"
|
||||
elif isinstance(atype, dict) and _is_any_type_schema(atype):
|
||||
pass # preserve "any type" semantics — don't coerce to object
|
||||
|
||||
if len(anyof) == 0:
|
||||
# Edge case: response schema with only null type present is invalid in Vertex AI
|
||||
@@ -714,7 +737,8 @@ def add_object_type(schema):
|
||||
# Gemini requires all function parameters to be type OBJECT
|
||||
# Handle case where schema has no properties and no type (e.g. tools with no arguments)
|
||||
if "type" not in schema and "anyOf" not in schema and "oneOf" not in schema and "allOf" not in schema:
|
||||
schema["type"] = "object"
|
||||
if not _is_any_type_schema(schema):
|
||||
schema["type"] = "object"
|
||||
|
||||
properties = schema.get("properties", None)
|
||||
if properties is not None:
|
||||
|
||||
@@ -212,7 +212,7 @@ def test_build_vertex_schema():
|
||||
"properties": {
|
||||
"state": {
|
||||
"properties": {
|
||||
"messages": {"items": {"type": "object"}, "type": "array"},
|
||||
"messages": {"items": {}, "type": "array"},
|
||||
"conversation_id": {"type": "string"},
|
||||
},
|
||||
"required": ["messages", "conversation_id"],
|
||||
@@ -226,7 +226,7 @@ def test_build_vertex_schema():
|
||||
"callbacks": {
|
||||
"anyOf": [
|
||||
{"type": "array", "nullable": True},
|
||||
{"type": "object", "nullable": True},
|
||||
{"nullable": True},
|
||||
]
|
||||
},
|
||||
"run_name": {"type": "string"},
|
||||
@@ -270,23 +270,28 @@ def test_process_items_basic():
|
||||
"""Test basic functionality of process_items."""
|
||||
from litellm.llms.vertex_ai.common_utils import process_items
|
||||
|
||||
# Test empty items
|
||||
# Test empty items — should preserve "any type" semantics (not coerce to object)
|
||||
schema = {"type": "array", "items": {}}
|
||||
process_items(schema)
|
||||
assert schema["items"] == {"type": "object"}
|
||||
assert schema["items"] == {}
|
||||
|
||||
# Test nested items
|
||||
# Test nested items — should preserve "any type" semantics
|
||||
schema = {"type": "array", "items": {"type": "array", "items": {}}}
|
||||
process_items(schema)
|
||||
assert schema["items"]["items"] == {"type": "object"}
|
||||
assert schema["items"]["items"] == {}
|
||||
|
||||
# Test items in properties
|
||||
# Test items in properties — should preserve "any type" semantics
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {"nested": {"type": "array", "items": {}}},
|
||||
}
|
||||
process_items(schema)
|
||||
assert schema["properties"]["nested"]["items"] == {"type": "object"}
|
||||
assert schema["properties"]["nested"]["items"] == {}
|
||||
|
||||
# Test items with actual type — should not be modified
|
||||
schema = {"type": "array", "items": {"type": "string"}}
|
||||
process_items(schema)
|
||||
assert schema["items"] == {"type": "string"}
|
||||
|
||||
|
||||
def test_vertex_ai_complex_response_schema():
|
||||
@@ -1402,3 +1407,89 @@ def test_add_object_type_does_not_add_type_when_anyof_present():
|
||||
|
||||
# Verify type was not added (anyOf handles the type)
|
||||
assert "type" not in input_schema, "type should not be added when anyOf is present"
|
||||
|
||||
|
||||
def test_is_any_type_schema():
|
||||
"""Test _is_any_type_schema correctly identifies unconstrained schemas."""
|
||||
from litellm.llms.vertex_ai.common_utils import _is_any_type_schema
|
||||
|
||||
# Empty schema = any type
|
||||
assert _is_any_type_schema({}) is True
|
||||
|
||||
# Only metadata keys = any type
|
||||
assert _is_any_type_schema({"description": "Any value"}) is True
|
||||
assert _is_any_type_schema({"title": "MyField"}) is True
|
||||
assert _is_any_type_schema({"title": "X", "description": "Y", "default": 0}) is True
|
||||
|
||||
# Has type-constraining keys = NOT any type
|
||||
assert _is_any_type_schema({"type": "object"}) is False
|
||||
assert _is_any_type_schema({"type": "string"}) is False
|
||||
assert _is_any_type_schema({"properties": {"a": {}}}) is False
|
||||
assert _is_any_type_schema({"items": {"type": "string"}}) is False
|
||||
assert _is_any_type_schema({"anyOf": [{"type": "string"}]}) is False
|
||||
assert _is_any_type_schema({"$schema": "https://json-schema.org/draft/2020-12/schema"}) is False
|
||||
assert _is_any_type_schema({"enum": ["a", "b"]}) is False
|
||||
|
||||
|
||||
def test_add_object_type_preserves_any_type_schema():
|
||||
"""Test add_object_type does NOT add type:object to empty schemas (any type)."""
|
||||
from litellm.llms.vertex_ai.common_utils import add_object_type
|
||||
|
||||
# Empty schema should be preserved (any type)
|
||||
schema = {}
|
||||
add_object_type(schema)
|
||||
assert "type" not in schema, "Empty schema (any type) should not get type: object"
|
||||
|
||||
# Schema with only description should be preserved
|
||||
schema = {"description": "Any JSON value"}
|
||||
add_object_type(schema)
|
||||
assert "type" not in schema
|
||||
|
||||
# Schema with $schema key should still get type: object (tool with no args)
|
||||
schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"}
|
||||
add_object_type(schema)
|
||||
assert schema["type"] == "object"
|
||||
|
||||
|
||||
def test_convert_anyof_preserves_any_type_members():
|
||||
"""Test convert_anyof_null_to_nullable does NOT coerce empty anyOf members to object."""
|
||||
from litellm.llms.vertex_ai.common_utils import convert_anyof_null_to_nullable
|
||||
|
||||
# anyOf with empty schema and null — empty should be preserved
|
||||
schema = {
|
||||
"anyOf": [
|
||||
{},
|
||||
{"type": "null"},
|
||||
]
|
||||
}
|
||||
convert_anyof_null_to_nullable(schema)
|
||||
# null should be removed, empty schema should be preserved (not coerced to object)
|
||||
assert len(schema["anyOf"]) == 1
|
||||
assert "type" not in schema["anyOf"][0] or schema["anyOf"][0].get("type") != "object"
|
||||
assert schema["anyOf"][0].get("nullable") is True
|
||||
|
||||
|
||||
def test_build_vertex_schema_jsonvalue():
|
||||
"""
|
||||
End-to-end: Pydantic JsonValue generates {} in $defs.
|
||||
_build_vertex_schema should preserve any-type semantics.
|
||||
Regression test for https://github.com/BerriAI/litellm/issues/22391
|
||||
"""
|
||||
from litellm.llms.vertex_ai.common_utils import _build_vertex_schema
|
||||
|
||||
# Simulates what Pydantic generates for a model with JsonValue field
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"value": {}, # after $ref resolution, this is what JsonValue becomes
|
||||
},
|
||||
"required": ["name", "value"],
|
||||
}
|
||||
result = _build_vertex_schema(schema)
|
||||
|
||||
# The "value" field should NOT have been coerced to type: object
|
||||
value_schema = result["properties"]["value"]
|
||||
assert value_schema.get("type") != "object", (
|
||||
"JsonValue schema {} should not be coerced to {type: object}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user