mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
Filter out unsupported fields from JSON schema for Anthropic's output_format API.
This commit is contained in:
parent
a5ea08a0bf
commit
c571a02f3b
2 changed files with 239 additions and 1 deletions
|
|
@ -200,6 +200,71 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
|
||||
return params
|
||||
|
||||
@staticmethod
|
||||
def filter_anthropic_output_schema(schema: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Filter out unsupported fields from JSON schema for Anthropic's output_format API.
|
||||
|
||||
Anthropic's output_format doesn't support certain JSON schema properties:
|
||||
- maxItems: Not supported for array types
|
||||
- minItems: Not supported for array types
|
||||
|
||||
This function recursively removes these unsupported fields while preserving
|
||||
all other valid schema properties.
|
||||
|
||||
Args:
|
||||
schema: The JSON schema dictionary to filter
|
||||
|
||||
Returns:
|
||||
A new dictionary with unsupported fields removed
|
||||
|
||||
Related issue: https://github.com/BerriAI/litellm/issues/19444
|
||||
"""
|
||||
if not isinstance(schema, dict):
|
||||
return schema
|
||||
|
||||
# Fields that Anthropic doesn't support in output_format
|
||||
unsupported_fields = {"maxItems", "minItems"}
|
||||
|
||||
result = {}
|
||||
for key, value in schema.items():
|
||||
# Skip unsupported fields
|
||||
if key in unsupported_fields:
|
||||
continue
|
||||
|
||||
# Recursively filter nested structures
|
||||
if key == "properties" and isinstance(value, dict):
|
||||
result[key] = {
|
||||
k: AnthropicConfig.filter_anthropic_output_schema(v)
|
||||
for k, v in value.items()
|
||||
}
|
||||
elif key == "items" and isinstance(value, dict):
|
||||
result[key] = AnthropicConfig.filter_anthropic_output_schema(value)
|
||||
elif key == "$defs" and isinstance(value, dict):
|
||||
result[key] = {
|
||||
k: AnthropicConfig.filter_anthropic_output_schema(v)
|
||||
for k, v in value.items()
|
||||
}
|
||||
elif key == "anyOf" and isinstance(value, list):
|
||||
result[key] = [
|
||||
AnthropicConfig.filter_anthropic_output_schema(item)
|
||||
for item in value
|
||||
]
|
||||
elif key == "allOf" and isinstance(value, list):
|
||||
result[key] = [
|
||||
AnthropicConfig.filter_anthropic_output_schema(item)
|
||||
for item in value
|
||||
]
|
||||
elif key == "oneOf" and isinstance(value, list):
|
||||
result[key] = [
|
||||
AnthropicConfig.filter_anthropic_output_schema(item)
|
||||
for item in value
|
||||
]
|
||||
else:
|
||||
result[key] = value
|
||||
|
||||
return result
|
||||
|
||||
def get_json_schema_from_pydantic_object(
|
||||
self, response_format: Union[Any, Dict, None]
|
||||
) -> Optional[dict]:
|
||||
|
|
@ -636,9 +701,13 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
)
|
||||
if json_schema is None:
|
||||
return None
|
||||
|
||||
# Filter out unsupported fields for Anthropic's output_format API
|
||||
filtered_schema = self.filter_anthropic_output_schema(json_schema)
|
||||
|
||||
return AnthropicOutputSchema(
|
||||
type="json_schema",
|
||||
schema=json_schema,
|
||||
schema=filtered_schema,
|
||||
)
|
||||
|
||||
def map_response_format_to_anthropic_tool(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,169 @@
|
|||
"""
|
||||
Test Anthropic structured output with Pydantic models.
|
||||
|
||||
This test file verifies that Pydantic models with various constraints
|
||||
are properly converted to Anthropic-compatible JSON schemas.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List
|
||||
|
||||
|
||||
class TestAnthropicStructuredOutput:
|
||||
"""Test Anthropic structured output schema transformations."""
|
||||
|
||||
def test_max_length_on_list_field_filtered(self):
|
||||
"""
|
||||
Test that max_length on List fields is filtered out for Anthropic models.
|
||||
|
||||
Anthropic doesn't support 'maxItems' property for array types in their
|
||||
output_format.schema, so we need to filter it out.
|
||||
|
||||
Related issue: https://github.com/BerriAI/litellm/issues/19444
|
||||
"""
|
||||
from litellm.llms.anthropic.chat.transformation import AnthropicConfig
|
||||
|
||||
# Define a Pydantic model with max_length on a List field
|
||||
class ResponseModel(BaseModel):
|
||||
items: List[str] = Field(max_length=5, description="List of items")
|
||||
name: str = Field(description="Name field")
|
||||
|
||||
config = AnthropicConfig()
|
||||
|
||||
# Get the JSON schema from the Pydantic model
|
||||
json_schema = config.get_json_schema_from_pydantic_object(ResponseModel)
|
||||
|
||||
# Extract the actual schema
|
||||
schema = json_schema["json_schema"]["schema"]
|
||||
|
||||
# Verify that maxItems is present in the raw schema (from Pydantic)
|
||||
assert "maxItems" in schema["properties"]["items"]
|
||||
|
||||
# Now apply the Anthropic output format transformation
|
||||
response_format = {
|
||||
"type": "json_schema",
|
||||
"json_schema": json_schema["json_schema"]
|
||||
}
|
||||
|
||||
output_format = config.map_response_format_to_anthropic_output_format(
|
||||
response_format
|
||||
)
|
||||
|
||||
# Verify that maxItems is filtered out for Anthropic
|
||||
assert output_format is not None
|
||||
assert "schema" in output_format
|
||||
transformed_schema = output_format["schema"]
|
||||
|
||||
# maxItems should be removed from the items property
|
||||
assert "maxItems" not in transformed_schema["properties"]["items"]
|
||||
|
||||
# But other properties should remain
|
||||
assert "type" in transformed_schema["properties"]["items"]
|
||||
assert transformed_schema["properties"]["items"]["type"] == "array"
|
||||
assert "description" in transformed_schema["properties"]["items"]
|
||||
|
||||
def test_min_length_on_list_field_filtered(self):
|
||||
"""
|
||||
Test that min_length on List fields is filtered out for Anthropic models.
|
||||
|
||||
Anthropic likely doesn't support 'minItems' either.
|
||||
"""
|
||||
from litellm.llms.anthropic.chat.transformation import AnthropicConfig
|
||||
|
||||
class ResponseModel(BaseModel):
|
||||
items: List[str] = Field(min_length=2, description="List of items")
|
||||
|
||||
config = AnthropicConfig()
|
||||
json_schema = config.get_json_schema_from_pydantic_object(ResponseModel)
|
||||
|
||||
response_format = {
|
||||
"type": "json_schema",
|
||||
"json_schema": json_schema["json_schema"]
|
||||
}
|
||||
|
||||
output_format = config.map_response_format_to_anthropic_output_format(
|
||||
response_format
|
||||
)
|
||||
|
||||
assert output_format is not None
|
||||
transformed_schema = output_format["schema"]
|
||||
|
||||
# minItems should be removed
|
||||
assert "minItems" not in transformed_schema["properties"]["items"]
|
||||
|
||||
def test_nested_array_constraints_filtered(self):
|
||||
"""
|
||||
Test that array constraints are filtered at all nesting levels.
|
||||
"""
|
||||
from litellm.llms.anthropic.chat.transformation import AnthropicConfig
|
||||
|
||||
class NestedItem(BaseModel):
|
||||
tags: List[str] = Field(max_length=3)
|
||||
|
||||
class ResponseModel(BaseModel):
|
||||
items: List[NestedItem] = Field(max_length=5)
|
||||
|
||||
config = AnthropicConfig()
|
||||
json_schema = config.get_json_schema_from_pydantic_object(ResponseModel)
|
||||
|
||||
response_format = {
|
||||
"type": "json_schema",
|
||||
"json_schema": json_schema["json_schema"]
|
||||
}
|
||||
|
||||
output_format = config.map_response_format_to_anthropic_output_format(
|
||||
response_format
|
||||
)
|
||||
|
||||
assert output_format is not None
|
||||
transformed_schema = output_format["schema"]
|
||||
|
||||
# Top-level maxItems should be removed
|
||||
assert "maxItems" not in transformed_schema["properties"]["items"]
|
||||
|
||||
# Nested maxItems should also be removed
|
||||
if "$defs" in transformed_schema:
|
||||
nested_item_schema = transformed_schema["$defs"].get("NestedItem", {})
|
||||
if "properties" in nested_item_schema and "tags" in nested_item_schema["properties"]:
|
||||
assert "maxItems" not in nested_item_schema["properties"]["tags"]
|
||||
|
||||
def test_other_constraints_preserved(self):
|
||||
"""
|
||||
Test that other valid constraints are preserved.
|
||||
"""
|
||||
from litellm.llms.anthropic.chat.transformation import AnthropicConfig
|
||||
|
||||
class ResponseModel(BaseModel):
|
||||
name: str = Field(max_length=100, min_length=1, description="Name")
|
||||
age: int = Field(ge=0, le=150, description="Age")
|
||||
items: List[str] = Field(description="Items list")
|
||||
|
||||
config = AnthropicConfig()
|
||||
json_schema = config.get_json_schema_from_pydantic_object(ResponseModel)
|
||||
|
||||
response_format = {
|
||||
"type": "json_schema",
|
||||
"json_schema": json_schema["json_schema"]
|
||||
}
|
||||
|
||||
output_format = config.map_response_format_to_anthropic_output_format(
|
||||
response_format
|
||||
)
|
||||
|
||||
assert output_format is not None
|
||||
transformed_schema = output_format["schema"]
|
||||
|
||||
# String constraints should be preserved
|
||||
name_schema = transformed_schema["properties"]["name"]
|
||||
assert "maxLength" in name_schema
|
||||
assert "minLength" in name_schema
|
||||
assert name_schema["maxLength"] == 100
|
||||
assert name_schema["minLength"] == 1
|
||||
|
||||
# Number constraints should be preserved
|
||||
age_schema = transformed_schema["properties"]["age"]
|
||||
assert "minimum" in age_schema
|
||||
assert "maximum" in age_schema
|
||||
assert age_schema["minimum"] == 0
|
||||
assert age_schema["maximum"] == 150
|
||||
Loading…
Add table
Reference in a new issue