mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
fix(vertex): drop bare {} schemas from anyOf before adding nullable=True
When anyOf contains a mix of concrete types, bare {} (any-type), and null,
convert_anyof_null_to_nullable was adding nullable=True to the {} entry,
producing {nullable: True} with no type field. Gemini rejects this as an
anyOf entry without a concrete type, breaking tool calls that use
Optional[List[...]] or similar union types (common in LangChain/Pydantic).
Fix: strip any-type schemas from anyOf before the nullable=True pass.
If only any-type schemas remain after null removal (anyOf: [{}, null]),
collapse the anyOf entirely and set nullable=True on the parent schema
instead — correctly representing 'any nullable value' for Gemini.
Regression introduced by da941e4261.
This commit is contained in:
parent
6dd2a9a3ea
commit
4c95ac2ca0
3 changed files with 87 additions and 27 deletions
|
|
@ -712,6 +712,20 @@ def convert_anyof_null_to_nullable(schema, depth=0):
|
|||
)
|
||||
|
||||
if contains_null:
|
||||
# Drop any-type schemas (bare {}) from anyOf before adding nullable=True.
|
||||
# Adding nullable=True to {} produces {"nullable": True} with no type field,
|
||||
# which Gemini rejects as an anyOf entry without a concrete type.
|
||||
for atype in list(anyof):
|
||||
if isinstance(atype, dict) and _is_any_type_schema(atype):
|
||||
anyof.remove(atype)
|
||||
|
||||
if len(anyof) == 0:
|
||||
# All remaining entries were any-type schemas (e.g. anyOf: [{}, null]).
|
||||
# This means "any nullable value" — collapse anyOf and mark parent nullable.
|
||||
del schema["anyOf"]
|
||||
schema["nullable"] = True
|
||||
return
|
||||
|
||||
# set all types to nullable following guidance found here: https://cloud.google.com/vertex-ai/generative-ai/docs/samples/generativeaionvertexai-gemini-controlled-generation-response-schema-3#generativeaionvertexai_gemini_controlled_generation_response_schema_3-python
|
||||
for atype in anyof:
|
||||
# Remove items field if type is array and items is empty
|
||||
|
|
|
|||
|
|
@ -2110,7 +2110,8 @@
|
|||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
"supports_vision": true,
|
||||
"supports_none_reasoning_effort": true
|
||||
},
|
||||
"azure/eu/gpt-5.1-chat": {
|
||||
"cache_read_input_token_cost": 1.4e-07,
|
||||
|
|
@ -2143,7 +2144,8 @@
|
|||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
"supports_vision": true,
|
||||
"supports_none_reasoning_effort": true
|
||||
},
|
||||
"azure/eu/gpt-5.1-codex": {
|
||||
"cache_read_input_token_cost": 1.4e-07,
|
||||
|
|
@ -2410,7 +2412,8 @@
|
|||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
"supports_vision": true,
|
||||
"supports_none_reasoning_effort": true
|
||||
},
|
||||
"azure/global/gpt-5.1-chat": {
|
||||
"cache_read_input_token_cost": 1.25e-07,
|
||||
|
|
@ -2443,7 +2446,8 @@
|
|||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
"supports_vision": true,
|
||||
"supports_none_reasoning_effort": true
|
||||
},
|
||||
"azure/global/gpt-5.1-codex": {
|
||||
"cache_read_input_token_cost": 1.25e-07,
|
||||
|
|
@ -3456,7 +3460,8 @@
|
|||
"supports_system_messages": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_service_tier": true,
|
||||
"supports_vision": true
|
||||
"supports_vision": true,
|
||||
"supports_none_reasoning_effort": true
|
||||
},
|
||||
"azure/gpt-5.1-chat-2025-11-13": {
|
||||
"cache_read_input_token_cost": 1.25e-07,
|
||||
|
|
@ -3491,7 +3496,8 @@
|
|||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_tool_choice": false,
|
||||
"supports_vision": true
|
||||
"supports_vision": true,
|
||||
"supports_none_reasoning_effort": true
|
||||
},
|
||||
"azure/gpt-5.1-codex-2025-11-13": {
|
||||
"cache_read_input_token_cost": 1.25e-07,
|
||||
|
|
@ -3906,7 +3912,8 @@
|
|||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
"supports_vision": true,
|
||||
"supports_none_reasoning_effort": true
|
||||
},
|
||||
"azure/gpt-5.1-chat": {
|
||||
"cache_read_input_token_cost": 1.25e-07,
|
||||
|
|
@ -3939,7 +3946,8 @@
|
|||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
"supports_vision": true,
|
||||
"supports_none_reasoning_effort": true
|
||||
},
|
||||
"azure/gpt-5.1-codex": {
|
||||
"cache_read_input_token_cost": 1.25e-07,
|
||||
|
|
@ -5273,7 +5281,8 @@
|
|||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
"supports_vision": true,
|
||||
"supports_none_reasoning_effort": true
|
||||
},
|
||||
"azure/us/gpt-5.1-chat": {
|
||||
"cache_read_input_token_cost": 1.4e-07,
|
||||
|
|
@ -5306,7 +5315,8 @@
|
|||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
"supports_vision": true,
|
||||
"supports_none_reasoning_effort": true
|
||||
},
|
||||
"azure/us/gpt-5.1-codex": {
|
||||
"cache_read_input_token_cost": 1.4e-07,
|
||||
|
|
@ -21068,18 +21078,18 @@
|
|||
"input_cost_per_token_flex": 1.5e-05,
|
||||
"input_cost_per_token_batches": 1.5e-05,
|
||||
"input_cost_per_token_priority": 6e-05,
|
||||
"input_cost_per_token_above_272k_tokens_priority": 1.2e-04,
|
||||
"input_cost_per_token_above_272k_tokens_priority": 0.00012,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 1050000,
|
||||
"max_output_tokens": 128000,
|
||||
"max_tokens": 128000,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.8e-04,
|
||||
"output_cost_per_token_above_272k_tokens": 2.7e-04,
|
||||
"output_cost_per_token": 0.00018,
|
||||
"output_cost_per_token_above_272k_tokens": 0.00027,
|
||||
"output_cost_per_token_flex": 9e-05,
|
||||
"output_cost_per_token_batches": 9e-05,
|
||||
"output_cost_per_token_priority": 2.7e-04,
|
||||
"output_cost_per_token_above_272k_tokens_priority": 4.05e-04,
|
||||
"output_cost_per_token_priority": 0.00027,
|
||||
"output_cost_per_token_above_272k_tokens_priority": 0.000405,
|
||||
"supported_endpoints": [
|
||||
"/v1/chat/completions",
|
||||
"/v1/batch",
|
||||
|
|
@ -21117,18 +21127,18 @@
|
|||
"input_cost_per_token_flex": 1.5e-05,
|
||||
"input_cost_per_token_batches": 1.5e-05,
|
||||
"input_cost_per_token_priority": 6e-05,
|
||||
"input_cost_per_token_above_272k_tokens_priority": 1.2e-04,
|
||||
"input_cost_per_token_above_272k_tokens_priority": 0.00012,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 1050000,
|
||||
"max_output_tokens": 128000,
|
||||
"max_tokens": 128000,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.8e-04,
|
||||
"output_cost_per_token_above_272k_tokens": 2.7e-04,
|
||||
"output_cost_per_token": 0.00018,
|
||||
"output_cost_per_token_above_272k_tokens": 0.00027,
|
||||
"output_cost_per_token_flex": 9e-05,
|
||||
"output_cost_per_token_batches": 9e-05,
|
||||
"output_cost_per_token_priority": 2.7e-04,
|
||||
"output_cost_per_token_above_272k_tokens_priority": 4.05e-04,
|
||||
"output_cost_per_token_priority": 0.00027,
|
||||
"output_cost_per_token_above_272k_tokens_priority": 0.000405,
|
||||
"supported_endpoints": [
|
||||
"/v1/chat/completions",
|
||||
"/v1/batch",
|
||||
|
|
|
|||
|
|
@ -1451,11 +1451,15 @@ def test_add_object_type_preserves_any_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."""
|
||||
def test_convert_anyof_pure_any_type_collapses_to_nullable_parent():
|
||||
"""
|
||||
anyOf: [{}, null] means "any nullable value".
|
||||
The empty schema {} (any-type) inside anyOf cannot receive nullable=True and be
|
||||
sent to Gemini — Gemini rejects {"nullable": True} with no type field as an anyOf
|
||||
entry. Instead we collapse the anyOf and set nullable=True on the parent.
|
||||
"""
|
||||
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": [
|
||||
{},
|
||||
|
|
@ -1463,10 +1467,42 @@ def test_convert_anyof_preserves_any_type_members():
|
|||
]
|
||||
}
|
||||
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
|
||||
# anyOf should be removed; parent gets nullable=True to represent "any nullable value"
|
||||
assert "anyOf" not in schema
|
||||
assert schema.get("nullable") is True
|
||||
|
||||
|
||||
def test_convert_anyof_drops_empty_schema_when_concrete_type_present():
|
||||
"""
|
||||
Regression test for tool schemas with anyOf: [array, {}, null].
|
||||
The bare {} (any-type) inside anyOf must be dropped — Gemini rejects
|
||||
{"nullable": True} with no type field. The concrete array type should
|
||||
remain with nullable=True.
|
||||
|
||||
Broken by da941e4261c5 (preserve type schema semantics for JsonValue fields).
|
||||
"""
|
||||
from litellm.llms.vertex_ai.common_utils import convert_anyof_null_to_nullable
|
||||
|
||||
schema = {
|
||||
"properties": {
|
||||
"callbacks": {
|
||||
"anyOf": [
|
||||
{"items": {}, "type": "array"},
|
||||
{},
|
||||
{"type": "null"},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
convert_anyof_null_to_nullable(schema)
|
||||
|
||||
callbacks_anyof = schema["properties"]["callbacks"]["anyOf"]
|
||||
# {} and {"type": "null"} should both be gone; only the array entry remains
|
||||
assert len(callbacks_anyof) == 1
|
||||
assert callbacks_anyof[0].get("type") == "array"
|
||||
assert callbacks_anyof[0].get("nullable") is True
|
||||
# empty items should have been stripped too
|
||||
assert "items" not in callbacks_anyof[0]
|
||||
|
||||
|
||||
def test_build_vertex_schema_jsonvalue():
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue