mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-15 10:24:33 +00:00
fix: also populate required for all properties in strict mode
OpenAI strict mode requires both additionalProperties:false AND all property keys in required. Without required, OpenAI rejects the schema even with additionalProperties:false set.
This commit is contained in:
@@ -843,15 +843,18 @@ class LiteLLMAnthropicMessagesAdapter:
|
||||
@staticmethod
|
||||
def _add_additional_properties_false(schema: dict) -> None:
|
||||
"""
|
||||
Recursively add 'additionalProperties': false to all object schemas.
|
||||
Recursively ensure object schemas comply with OpenAI strict mode.
|
||||
|
||||
OpenAI's strict mode requires this at every object nesting level.
|
||||
OpenAI's strict mode requires:
|
||||
1. 'additionalProperties': false at every object nesting level
|
||||
2. All property keys listed in 'required'
|
||||
"""
|
||||
if not isinstance(schema, dict):
|
||||
return
|
||||
|
||||
if schema.get("type") == "object" and "properties" in schema:
|
||||
schema["additionalProperties"] = False
|
||||
schema["required"] = list(schema["properties"].keys())
|
||||
for prop in schema["properties"].values():
|
||||
LiteLLMAnthropicMessagesAdapter._add_additional_properties_false(prop)
|
||||
|
||||
|
||||
+26
@@ -2004,6 +2004,7 @@ class TestTranslateAnthropicOutputFormatToOpenAI:
|
||||
assert result is not None
|
||||
schema = result["json_schema"]["schema"]
|
||||
assert schema["additionalProperties"] is False
|
||||
assert schema["required"] == ["name"]
|
||||
|
||||
def test_nested_objects_adds_additional_properties_false(self):
|
||||
output_format = {
|
||||
@@ -2028,8 +2029,11 @@ class TestTranslateAnthropicOutputFormatToOpenAI:
|
||||
assert result is not None
|
||||
schema = result["json_schema"]["schema"]
|
||||
assert schema["additionalProperties"] is False
|
||||
assert schema["required"] == ["user"]
|
||||
assert schema["properties"]["user"]["additionalProperties"] is False
|
||||
assert schema["properties"]["user"]["required"] == ["name", "address"]
|
||||
assert schema["properties"]["user"]["properties"]["address"]["additionalProperties"] is False
|
||||
assert schema["properties"]["user"]["properties"]["address"]["required"] == ["city"]
|
||||
|
||||
def test_array_items_object_adds_additional_properties_false(self):
|
||||
output_format = {
|
||||
@@ -2061,6 +2065,7 @@ class TestTranslateAnthropicOutputFormatToOpenAI:
|
||||
output_format = {"type": "json_schema", "schema": original_schema}
|
||||
self.adapter.translate_anthropic_output_format_to_openai(output_format)
|
||||
assert "additionalProperties" not in original_schema
|
||||
assert "required" not in original_schema
|
||||
|
||||
def test_defs_adds_additional_properties_false(self):
|
||||
output_format = {
|
||||
@@ -2080,6 +2085,27 @@ class TestTranslateAnthropicOutputFormatToOpenAI:
|
||||
assert result is not None
|
||||
schema = result["json_schema"]["schema"]
|
||||
assert schema["$defs"]["Item"]["additionalProperties"] is False
|
||||
assert schema["$defs"]["Item"]["required"] == ["value"]
|
||||
|
||||
def test_incomplete_required_gets_completed(self):
|
||||
"""OpenAI strict mode requires ALL properties in required."""
|
||||
output_format = {
|
||||
"type": "json_schema",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"age": {"type": "integer"},
|
||||
"email": {"type": "string"},
|
||||
},
|
||||
"required": ["name"], # only 1 of 3
|
||||
},
|
||||
}
|
||||
result = self.adapter.translate_anthropic_output_format_to_openai(output_format)
|
||||
assert result is not None
|
||||
schema = result["json_schema"]["schema"]
|
||||
assert schema["additionalProperties"] is False
|
||||
assert sorted(schema["required"]) == ["age", "email", "name"]
|
||||
|
||||
def test_invalid_output_format_returns_none(self):
|
||||
assert self.adapter.translate_anthropic_output_format_to_openai("invalid") is None
|
||||
|
||||
Reference in New Issue
Block a user