Filter out unsupported fields from JSON schema for Anthropic's output_format API.

This commit is contained in:
Sameer Kankute 2026-01-21 15:48:22 +05:30
parent a5ea08a0bf
commit c571a02f3b
2 changed files with 239 additions and 1 deletions

View file

@ -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(

View file

@ -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