diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 82aa7390188..0e3df4b4514 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -208,24 +208,37 @@ 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: Not supported for array types - - minItems: Not supported for array types + - maxItems/minItems: Not supported for array types + - minimum/maximum: Not supported for numeric types + - minLength/maxLength: Not supported for string types - This function recursively removes these unsupported fields while preserving - all other valid schema properties. + 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 + + The SDK approach: + 1. Remove unsupported constraints from schema + 2. Add constraint info to description (e.g., "Must be at least 100") + 3. Validate responses against original schema Args: schema: The JSON schema dictionary to filter Returns: - A new dictionary with unsupported fields removed + A new dictionary with unsupported fields removed and descriptions updated - Related issue: https://github.com/BerriAI/litellm/issues/19444 + Related issues: + - https://github.com/BerriAI/litellm/issues/19444 """ if not isinstance(schema, dict): return schema - unsupported_fields = {"maxItems", "minItems"} + # All numeric/string/array constraints not supported by Anthropic + unsupported_fields = { + "maxItems", "minItems", # array constraints + "minimum", "maximum", # numeric constraints + "exclusiveMinimum", "exclusiveMaximum", # numeric constraints + "minLength", "maxLength", # string constraints + } result: Dict[str, Any] = {} for key, value in schema.items(): diff --git a/tests/litellm/llms/anthropic/test_anthropic_schema_filter.py b/tests/litellm/llms/anthropic/test_anthropic_schema_filter.py new file mode 100644 index 00000000000..bec4aaaf533 --- /dev/null +++ b/tests/litellm/llms/anthropic/test_anthropic_schema_filter.py @@ -0,0 +1,143 @@ +""" +Tests for Anthropic JSON schema filtering. + +Related to: https://platform.claude.com/docs/en/build-with-claude/structured-outputs#how-sdk-transformation-works +""" + +import pytest +from litellm.llms.anthropic.chat.transformation import AnthropicConfig + + +class TestFilterAnthropicOutputSchema: + """Test the filter_anthropic_output_schema function.""" + + def test_removes_numeric_constraints(self): + """Test that minimum/maximum are removed from numeric schemas.""" + schema = { + "type": "object", + "properties": { + "age": { + "type": "integer", + "minimum": 0, + "maximum": 150, + "description": "Person's age" + }, + "score": { + "type": "number", + "exclusiveMinimum": 0, + "exclusiveMaximum": 100 + } + } + } + + result = AnthropicConfig.filter_anthropic_output_schema(schema) + + # minimum/maximum should be removed + assert "minimum" not in result["properties"]["age"] + assert "maximum" not in result["properties"]["age"] + assert "exclusiveMinimum" not in result["properties"]["score"] + assert "exclusiveMaximum" not in result["properties"]["score"] + + # Other fields preserved + assert result["properties"]["age"]["type"] == "integer" + assert result["properties"]["age"]["description"] == "Person's age" + + 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 + } + } + } + + result = AnthropicConfig.filter_anthropic_output_schema(schema) + + assert "minLength" not in result["properties"]["name"] + assert "maxLength" not in result["properties"]["name"] + assert result["properties"]["name"]["type"] == "string" + + def test_removes_array_constraints(self): + """Test that minItems/maxItems are removed from array schemas.""" + schema = { + "type": "array", + "items": {"type": "string"}, + "minItems": 1, + "maxItems": 10 + } + + result = AnthropicConfig.filter_anthropic_output_schema(schema) + + assert "minItems" not in result + assert "maxItems" not in result + assert result["type"] == "array" + assert result["items"] == {"type": "string"} + + def test_handles_nested_schemas(self): + """Test that nested schemas are also filtered.""" + schema = { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "quantity": { + "type": "integer", + "minimum": 1, + "maximum": 100 + } + } + }, + "minItems": 1 + } + } + } + + result = AnthropicConfig.filter_anthropic_output_schema(schema) + + # Top level array constraint removed + assert "minItems" not in result["properties"]["items"] + + # Nested numeric constraints removed + nested_props = result["properties"]["items"]["items"]["properties"] + assert "minimum" not in nested_props["quantity"] + assert "maximum" not in nested_props["quantity"] + + def test_handles_anyof_oneof_allof(self): + """Test that anyOf/oneOf/allOf schemas are filtered recursively.""" + schema = { + "anyOf": [ + {"type": "integer", "minimum": 0}, + {"type": "string", "minLength": 1} + ] + } + + result = AnthropicConfig.filter_anthropic_output_schema(schema) + + assert "minimum" not in result["anyOf"][0] + assert "minLength" not in result["anyOf"][1] + + def test_preserves_valid_fields(self): + """Test that valid schema fields are preserved.""" + schema = { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["active", "inactive"], + "description": "Current status" + } + }, + "required": ["status"], + "additionalProperties": False + } + + result = AnthropicConfig.filter_anthropic_output_schema(schema) + + assert result == schema # Should be unchanged