fix: Preserved nullable object fields by carrying schema properties

This commit is contained in:
Sameer Kankute 2026-01-15 13:30:43 +05:30
parent dca42047b9
commit 99ef233a69
2 changed files with 70 additions and 2 deletions

View File

@ -1,4 +1,5 @@
import re
from copy import deepcopy
from enum import Enum
from typing import Any, Dict, List, Literal, Optional, Set, Tuple, Union, get_type_hints
@ -617,7 +618,7 @@ def convert_anyof_null_to_nullable(schema, depth=0):
if anyof is not None:
contains_null = False
for atype in anyof:
if atype == {"type": "null"}:
if isinstance(atype, dict) and atype.get("type") == "null":
# remove null type
anyof.remove(atype)
contains_null = True
@ -735,7 +736,20 @@ def _convert_schema_types(schema, depth=0):
type_val = schema["type"]
if isinstance(type_val, list) and len(type_val) > 1:
# Convert ["string", "number"] -> {"anyOf": [{"type": "STRING"}, {"type": "NUMBER"}]}
schema["anyOf"] = [{"type": t} for t in type_val if isinstance(t, str)]
# Preserve other schema fields by copying them into each non-null anyOf item.
base_schema = {k: v for k, v in schema.items() if k not in {"type", "anyOf"}}
any_of: List[Dict[str, Any]] = []
for t in type_val:
if not isinstance(t, str):
continue
if t == "null":
# Keep null entry minimal so we can strip it later.
any_of.append({"type": "null"})
continue
item_schema = deepcopy(base_schema)
item_schema["type"] = t
any_of.append(item_schema)
schema["anyOf"] = any_of
schema.pop("type")
elif isinstance(type_val, list) and len(type_val) == 1:
schema["type"] = type_val[0]

View File

@ -3598,6 +3598,60 @@ def test_vertex_schema_test():
print(response)
def test_gemini_nullable_object_tool_schema_httpx():
"""
Ensure nullable object tool params preserve nested properties in Vertex schema conversion.
"""
load_vertex_ai_credentials()
litellm._turn_on_debug()
tools = [{
"type": "function",
"strict": True,
"function": {
"name": "create_support_ticket",
"description": "Create a paid user support ticket",
"parameters": {
"type": "object",
"additionalProperties": False,
"required": ["ticket_id", "customer_context"],
"properties": {
"ticket_id": {
"type": "string",
"description": "Unique identifier for the support ticket"
},
"customer_context": {
"type": ["object", "null"],
"description": "Context about the paid customer, if available",
"additionalProperties": False,
"required": ["user_id", "plan"],
"properties": {
"user_id": {
"type": "string",
"description": "Internal user identifier"
},
"plan": {
"type": "string",
"description": "Subscription plan name (e.g. pro, enterprise)"
}
}
}
}
}
}
}]
response = litellm.completion(
model="vertex_ai/gemini-2.5-flash",
messages=[{"role": "user", "content": "call the tool"}],
tools=tools,
tool_choice="required",
)
print(response)
def test_vertex_ai_response_id():
"""Test that litellm preserves the response ID from Vertex AI's API for non-streaming responses"""
from litellm.llms.custom_httpx.http_handler import HTTPHandler