mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-14 12:26:25 +00:00
fix: add additionalProperties: false for OpenAI strict mode in Anthropic adapter
When translating Anthropic output_format to OpenAI response_format, the adapter sets strict: true but didn't add additionalProperties: false, which OpenAI requires at every object nesting level. This caused BadRequestError for structured output requests routed to OpenAI models. Fixes #20997
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
from typing import (
|
||||
@@ -824,6 +825,11 @@ class LiteLLMAnthropicMessagesAdapter:
|
||||
if not schema:
|
||||
return None
|
||||
|
||||
# Deep copy to avoid mutating the original schema
|
||||
schema = copy.deepcopy(schema)
|
||||
# OpenAI strict mode requires additionalProperties: false on every object
|
||||
self._add_additional_properties_false(schema)
|
||||
|
||||
# Convert to OpenAI response_format structure
|
||||
return {
|
||||
"type": "json_schema",
|
||||
@@ -834,6 +840,37 @@ class LiteLLMAnthropicMessagesAdapter:
|
||||
},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _add_additional_properties_false(schema: dict) -> None:
|
||||
"""
|
||||
Recursively add 'additionalProperties': false to all object schemas.
|
||||
|
||||
OpenAI's strict mode requires this at every object nesting level.
|
||||
"""
|
||||
if not isinstance(schema, dict):
|
||||
return
|
||||
|
||||
if schema.get("type") == "object" and "properties" in schema:
|
||||
schema["additionalProperties"] = False
|
||||
for prop in schema["properties"].values():
|
||||
LiteLLMAnthropicMessagesAdapter._add_additional_properties_false(prop)
|
||||
|
||||
# Handle array items
|
||||
if "items" in schema:
|
||||
LiteLLMAnthropicMessagesAdapter._add_additional_properties_false(schema["items"])
|
||||
|
||||
# Handle anyOf/oneOf/allOf
|
||||
for key in ("anyOf", "oneOf", "allOf"):
|
||||
if key in schema:
|
||||
for sub_schema in schema[key]:
|
||||
LiteLLMAnthropicMessagesAdapter._add_additional_properties_false(sub_schema)
|
||||
|
||||
# Handle $defs / definitions
|
||||
for key in ("$defs", "definitions"):
|
||||
if key in schema:
|
||||
for def_schema in schema[key].values():
|
||||
LiteLLMAnthropicMessagesAdapter._add_additional_properties_false(def_schema)
|
||||
|
||||
def _add_system_message_to_messages(
|
||||
self,
|
||||
new_messages: List[AllMessageValues],
|
||||
|
||||
+101
@@ -1984,3 +1984,104 @@ def test_translate_anthropic_to_openai_with_mixed_tools():
|
||||
|
||||
# tool_name_mapping should be empty for short tool names
|
||||
assert tool_name_mapping == {}
|
||||
|
||||
|
||||
class TestTranslateAnthropicOutputFormatToOpenAI:
|
||||
"""Tests for translate_anthropic_output_format_to_openai adding additionalProperties: false."""
|
||||
|
||||
def setup_method(self):
|
||||
self.adapter = LiteLLMAnthropicMessagesAdapter()
|
||||
|
||||
def test_simple_object_adds_additional_properties_false(self):
|
||||
output_format = {
|
||||
"type": "json_schema",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
},
|
||||
}
|
||||
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
|
||||
|
||||
def test_nested_objects_adds_additional_properties_false(self):
|
||||
output_format = {
|
||||
"type": "json_schema",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"address": {
|
||||
"type": "object",
|
||||
"properties": {"city": {"type": "string"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
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 schema["properties"]["user"]["additionalProperties"] is False
|
||||
assert schema["properties"]["user"]["properties"]["address"]["additionalProperties"] is False
|
||||
|
||||
def test_array_items_object_adds_additional_properties_false(self):
|
||||
output_format = {
|
||||
"type": "json_schema",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {"id": {"type": "integer"}},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
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 schema["properties"]["items"]["items"]["additionalProperties"] is False
|
||||
|
||||
def test_does_not_mutate_original_schema(self):
|
||||
original_schema = {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
}
|
||||
output_format = {"type": "json_schema", "schema": original_schema}
|
||||
self.adapter.translate_anthropic_output_format_to_openai(output_format)
|
||||
assert "additionalProperties" not in original_schema
|
||||
|
||||
def test_defs_adds_additional_properties_false(self):
|
||||
output_format = {
|
||||
"type": "json_schema",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"ref": {"$ref": "#/$defs/Item"}},
|
||||
"$defs": {
|
||||
"Item": {
|
||||
"type": "object",
|
||||
"properties": {"value": {"type": "string"}},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
result = self.adapter.translate_anthropic_output_format_to_openai(output_format)
|
||||
assert result is not None
|
||||
schema = result["json_schema"]["schema"]
|
||||
assert schema["$defs"]["Item"]["additionalProperties"] is False
|
||||
|
||||
def test_invalid_output_format_returns_none(self):
|
||||
assert self.adapter.translate_anthropic_output_format_to_openai("invalid") is None
|
||||
assert self.adapter.translate_anthropic_output_format_to_openai({"type": "text"}) is None
|
||||
assert self.adapter.translate_anthropic_output_format_to_openai({"type": "json_schema"}) is None
|
||||
|
||||
Reference in New Issue
Block a user