Revert "fix(anthropic): strip uniqueItems + other unsupported array/object co…"

This reverts commit f05f079e13.
This commit is contained in:
Mateo Wang 2026-07-22 15:49:52 -07:00 committed by GitHub
parent f05f079e13
commit 8a735264a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 17 additions and 187 deletions

View file

@ -478,15 +478,10 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
"""
Filter out unsupported fields from JSON schema for Anthropic's output_format API.
Anthropic's output_format doesn't support certain JSON schema properties.
These are cross-element / count constraints that cannot be enforced by the
constrained-decoding grammar Anthropic compiles the schema into, so the API
rejects them with a 400 ``invalid_request_error`` (e.g. "output_format.schema:
For 'array' type, property 'uniqueItems' is not supported"):
- maxItems/minItems/uniqueItems/contains/minContains/maxContains: array constraints
- minimum/maximum/exclusiveMinimum/exclusiveMaximum: numeric constraints
- minLength/maxLength: string constraints
- minProperties/maxProperties: object constraints
Anthropic's output_format doesn't support certain JSON schema properties:
- maxItems/minItems: Not supported for array types
- minimum/maximum: Not supported for numeric types
- minLength/maxLength: Not supported for string types
This mirrors the transformation done by the Anthropic Python SDK.
See: https://platform.claude.com/docs/en/build-with-claude/structured-outputs#how-sdk-transformation-works
@ -507,22 +502,16 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
if not isinstance(schema, dict):
return schema
# All numeric/string/array/object constraints not supported by Anthropic
# All numeric/string/array constraints not supported by Anthropic
unsupported_fields = {
"maxItems",
"minItems",
"uniqueItems",
"contains",
"minContains",
"maxContains", # array constraints
"minItems", # array constraints
"minimum",
"maximum", # numeric constraints
"exclusiveMinimum",
"exclusiveMaximum", # numeric constraints
"minLength",
"maxLength", # string constraints
"minProperties",
"maxProperties", # object constraints
}
# Build description additions from removed constraints
@ -530,31 +519,16 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
constraint_labels = {
"minItems": "minimum number of items: {}",
"maxItems": "maximum number of items: {}",
"uniqueItems": "all array items must be unique",
"contains": "array must contain an item matching: {}",
"minContains": "minimum number of matching items: {}",
"maxContains": "maximum number of matching items: {}",
"minimum": "minimum value: {}",
"maximum": "maximum value: {}",
"exclusiveMinimum": "exclusive minimum value: {}",
"exclusiveMaximum": "exclusive maximum value: {}",
"minLength": "minimum length: {}",
"maxLength": "maximum length: {}",
"minProperties": "minimum number of properties: {}",
"maxProperties": "maximum number of properties: {}",
}
for field in unsupported_fields:
if field in schema:
value = schema[field]
# A falsy boolean constraint (e.g. ``uniqueItems: false``) imposes no
# real requirement, so don't add a misleading advisory note for it.
if isinstance(value, bool) and not value:
continue
# Sub-schema constraints (e.g. ``contains``) are serialized as JSON so
# the advisory note preserves what the constraint actually required,
# instead of just noting that it existed.
note_value = json.dumps(value) if isinstance(value, (dict, list)) else value
constraint_descriptions.append(constraint_labels[field].format(note_value))
constraint_descriptions.append(constraint_labels[field].format(schema[field]))
result: Dict[str, Any] = {}

View file

@ -45,14 +45,21 @@ class TestFilterAnthropicOutputSchema:
assert "minimum value: 0" in result["properties"]["age"]["description"]
assert "maximum value: 150" in result["properties"]["age"]["description"]
# Score had no description, should get one from constraints
assert "exclusive minimum value: 0" in result["properties"]["score"]["description"]
assert "exclusive maximum value: 100" in result["properties"]["score"]["description"]
assert (
"exclusive minimum value: 0" in result["properties"]["score"]["description"]
)
assert (
"exclusive maximum value: 100"
in result["properties"]["score"]["description"]
)
def test_removes_string_constraints(self):
"""Test that minLength/maxLength are removed from string schemas."""
schema = {
"type": "object",
"properties": {"name": {"type": "string", "minLength": 1, "maxLength": 100}},
"properties": {
"name": {"type": "string", "minLength": 1, "maxLength": 100}
},
}
result = AnthropicConfig.filter_anthropic_output_schema(schema)
@ -147,81 +154,3 @@ class TestFilterAnthropicOutputSchema:
result = AnthropicConfig.filter_anthropic_output_schema(schema)
assert result == schema # Should be unchanged
def test_removes_uniqueitems(self):
"""Test that uniqueItems is removed from array schemas.
Reproduces the 400 ``invalid_request_error``:
"output_format.schema: For 'array' type, property 'uniqueItems' is not
supported".
"""
schema = {
"type": "object",
"properties": {
"tags": {
"type": "array",
"items": {"type": "string"},
"uniqueItems": True,
}
},
}
result = AnthropicConfig.filter_anthropic_output_schema(schema)
assert "uniqueItems" not in result["properties"]["tags"]
assert result["properties"]["tags"]["items"] == {"type": "string"}
# Constraint intent preserved in the description
assert "all array items must be unique" in result["properties"]["tags"]["description"]
def test_removes_contains_constraints(self):
"""Test that contains/minContains/maxContains are removed from arrays."""
schema = {
"type": "array",
"items": {"type": "integer"},
"contains": {"type": "integer", "const": 1},
"minContains": 1,
"maxContains": 3,
}
result = AnthropicConfig.filter_anthropic_output_schema(schema)
assert "contains" not in result
assert "minContains" not in result
assert "maxContains" not in result
assert result["items"] == {"type": "integer"}
# The contains sub-schema is serialized into the advisory note so the model
# knows what item the array must contain.
assert "array must contain an item matching:" in result["description"]
assert '"const": 1' in result["description"]
assert "minimum number of matching items: 1" in result["description"]
assert "maximum number of matching items: 3" in result["description"]
def test_removes_object_property_constraints(self):
"""Test that minProperties/maxProperties are removed from object schemas."""
schema = {
"type": "object",
"properties": {"a": {"type": "string"}},
"minProperties": 1,
"maxProperties": 5,
}
result = AnthropicConfig.filter_anthropic_output_schema(schema)
assert "minProperties" not in result
assert "maxProperties" not in result
assert "minimum number of properties: 1" in result["description"]
assert "maximum number of properties: 5" in result["description"]
def test_uniqueitems_false_skips_misleading_note(self):
"""``uniqueItems: false`` is stripped but must not add a 'unique' note."""
schema = {
"type": "array",
"items": {"type": "string"},
"uniqueItems": False,
}
result = AnthropicConfig.filter_anthropic_output_schema(schema)
assert "uniqueItems" not in result
# A disabled constraint imposes no requirement -> no advisory note
assert "unique" not in result.get("description", "")

View file

@ -1,73 +0,0 @@
"""
Coverage for filter_anthropic_output_schema's array/object constraint stripping.
Mirrors tests/litellm/llms/anthropic/test_anthropic_schema_filter.py, but lives
under tests/test_litellm/ so the coverage-uploading CI job exercises the newly
added keyword handling (uniqueItems / contains / minProperties / maxProperties)
and the ``uniqueItems: false`` branch.
"""
from litellm.llms.anthropic.chat.transformation import AnthropicConfig
class TestOutputFormatArrayObjectConstraints:
def test_removes_uniqueitems(self):
schema = {
"type": "object",
"properties": {
"tags": {
"type": "array",
"items": {"type": "string"},
"uniqueItems": True,
}
},
}
result = AnthropicConfig.filter_anthropic_output_schema(schema)
assert "uniqueItems" not in result["properties"]["tags"]
assert "all array items must be unique" in result["properties"]["tags"]["description"]
def test_uniqueitems_false_skips_misleading_note(self):
schema = {
"type": "array",
"items": {"type": "string"},
"uniqueItems": False,
}
result = AnthropicConfig.filter_anthropic_output_schema(schema)
assert "uniqueItems" not in result
assert "unique" not in result.get("description", "")
def test_removes_contains_constraints(self):
schema = {
"type": "array",
"items": {"type": "integer"},
"contains": {"type": "integer", "const": 1},
"minContains": 1,
"maxContains": 3,
}
result = AnthropicConfig.filter_anthropic_output_schema(schema)
assert "contains" not in result
assert "minContains" not in result
assert "maxContains" not in result
assert "array must contain an item matching:" in result["description"]
assert '"const": 1' in result["description"]
def test_removes_object_property_constraints(self):
schema = {
"type": "object",
"properties": {"a": {"type": "string"}},
"minProperties": 1,
"maxProperties": 5,
}
result = AnthropicConfig.filter_anthropic_output_schema(schema)
assert "minProperties" not in result
assert "maxProperties" not in result
assert "minimum number of properties: 1" in result["description"]
assert "maximum number of properties: 5" in result["description"]