mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-16 23:41:43 +00:00
fix(anthropic): strip uniqueItems + other unsupported array/object constraints from output_format schema (#33981)
Some checks failed
OSS Daily Guardrails / Run OSS daily safe checks (push) Has been cancelled
Some checks failed
OSS Daily Guardrails / Run OSS daily safe checks (push) Has been cancelled
* fix(anthropic): strip uniqueItems + other unsupported array/object constraints from output_format schema Anthropic's structured outputs (`output_format`) validate the JSON schema against a strict subset and reject cross-element / count constraints that a constrained-decoding grammar cannot enforce, returning a 400 `invalid_request_error`. `filter_anthropic_output_schema` already stripped the numeric / string / item-count constraints (minimum, maximum, exclusiveMinimum/Maximum, minLength, maxLength, minItems, maxItems) but still let these through: - uniqueItems - contains / minContains / maxContains - minProperties / maxProperties so a request using them fails with e.g. "output_format.schema: For 'array' type, property 'uniqueItems' is not supported". This is provider-visible: newer Claude models on the native `output_format` path (e.g. `azure_ai`) 400, while `vertex_ai` is unaffected because it is forced onto the permissive tool-use path (#18625 / #19201). Add the missing keywords to the unsupported-field set and the description map, and skip the advisory description note for a disabled boolean constraint (`uniqueItems: false`) so it isn't misdescribed as required. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(anthropic): serialize contains sub-schema in output_format advisory note Address Greptile review: the `contains` advisory note previously discarded the sub-schema, so the description only said an item must match "a schema" without saying which. It now serializes the sub-schema as JSON (e.g. "array must contain an item matching: {\"type\": \"integer\", \"const\": 1}"), matching the other stripped constraints which carry their value. Sub-schema (dict/list) values are json.dumps'd; scalar constraints are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(anthropic): apply ruff format to output_format filter change Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(test): ruff format anthropic schema filter tests Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(anthropic): cover output_format array/object constraint filtering in test_litellm tree Mirrors the schema-filter tests under tests/test_litellm/ so the coverage job exercises the new uniqueItems/contains/min-maxProperties handling and the uniqueItems: false branch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1ebf2a78a9
commit
f05f079e13
3 changed files with 187 additions and 17 deletions
|
|
@ -478,10 +478,15 @@ 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:
|
||||
- maxItems/minItems: Not supported for array types
|
||||
- minimum/maximum: Not supported for numeric types
|
||||
- minLength/maxLength: Not supported for string types
|
||||
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
|
||||
|
||||
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
|
||||
|
|
@ -502,16 +507,22 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
if not isinstance(schema, dict):
|
||||
return schema
|
||||
|
||||
# All numeric/string/array constraints not supported by Anthropic
|
||||
# All numeric/string/array/object constraints not supported by Anthropic
|
||||
unsupported_fields = {
|
||||
"maxItems",
|
||||
"minItems", # array constraints
|
||||
"minItems",
|
||||
"uniqueItems",
|
||||
"contains",
|
||||
"minContains",
|
||||
"maxContains", # array constraints
|
||||
"minimum",
|
||||
"maximum", # numeric constraints
|
||||
"exclusiveMinimum",
|
||||
"exclusiveMaximum", # numeric constraints
|
||||
"minLength",
|
||||
"maxLength", # string constraints
|
||||
"minProperties",
|
||||
"maxProperties", # object constraints
|
||||
}
|
||||
|
||||
# Build description additions from removed constraints
|
||||
|
|
@ -519,16 +530,31 @@ 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:
|
||||
constraint_descriptions.append(constraint_labels[field].format(schema[field]))
|
||||
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))
|
||||
|
||||
result: Dict[str, Any] = {}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,21 +45,14 @@ 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)
|
||||
|
|
@ -154,3 +147,81 @@ 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", "")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
"""
|
||||
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"]
|
||||
Loading…
Add table
Reference in a new issue