From 46751ad83ed3307fdc0e966d7e850660df2446d9 Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Tue, 4 Aug 2026 16:17:01 -0400 Subject: [PATCH] fix(anthropic): coerce explicit additionalProperties to false in output_format schema Anthropic's structured outputs reject any `additionalProperties` value other than `false` ("output_format.schema: For 'object' type, 'additionalProperties: true' is not supported. Please set 'additionalProperties' to false") `filter_anthropic_output_schema` only added the key when it was absent, so an explicit `true` (or a sub-schema) was copied verbatim into output_format.schema and 400'd. Coerce it for object schemas instead, at every recursion depth, matching what the Anthropic Python/TypeScript SDKs do The permissive tool-use path (map_response_format_to_anthropic_tool, used for vertex_ai) is deliberately left alone Fixes #35808 --- litellm/llms/anthropic/chat/transformation.py | 2 +- .../anthropic/test_anthropic_schema_filter.py | 72 ++++++++++++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 51b862e79d9..19f5174579b 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -597,7 +597,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): # Anthropic requires additionalProperties=false for object schemas # See: https://docs.anthropic.com/en/docs/build-with-claude/structured-outputs - if result.get("type") == "object" and "additionalProperties" not in result: + if result.get("type") == "object": result["additionalProperties"] = False return result diff --git a/tests/litellm/llms/anthropic/test_anthropic_schema_filter.py b/tests/litellm/llms/anthropic/test_anthropic_schema_filter.py index c10ac5532a0..71c9cfe8f41 100644 --- a/tests/litellm/llms/anthropic/test_anthropic_schema_filter.py +++ b/tests/litellm/llms/anthropic/test_anthropic_schema_filter.py @@ -281,7 +281,10 @@ class TestFilterAnthropicOutputSchema: "unevaluatedProperties", ): assert field not in result - assert 'properties whose names match each pattern must satisfy: {"^x": {"type": "string"}}' in result["description"] + assert ( + 'properties whose names match each pattern must satisfy: {"^x": {"type": "string"}}' + in result["description"] + ) assert 'property names must satisfy: {"pattern": "^[a-z]+$"}' in result["description"] assert 'dependent required properties: {"first": ["last"]}' in result["description"] assert 'dependent schemas: {"first": {"required": ["last"]}}' in result["description"] @@ -347,3 +350,70 @@ class TestFilterAnthropicOutputSchema: "all array items must be unique, minimum number of matching items: 2, " "maximum number of matching items: 3." ) + + def test_coerces_explicit_additional_properties_true(self): + """An explicit ``additionalProperties: true`` must be coerced to false. + + Anthropic rejects anything other than false with: + "output_format.schema: For 'object' type, 'additionalProperties: true' is + not supported". + """ + schema = { + "type": "object", + "additionalProperties": True, + "properties": {"a": {"type": "string"}}, + } + + result = AnthropicConfig.filter_anthropic_output_schema(schema) + + assert result["additionalProperties"] is False + + def test_coerces_additional_properties_true_when_nested(self): + """Nested object schemas are coerced too, at every recursion site.""" + schema = { + "type": "object", + "properties": { + "obj": { + "type": "object", + "additionalProperties": True, + "properties": {"a": {"type": "string"}}, + }, + "rows": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": True, + "properties": {"b": {"type": "string"}}, + }, + }, + }, + } + + result = AnthropicConfig.filter_anthropic_output_schema(schema) + + assert result["properties"]["obj"]["additionalProperties"] is False + assert result["properties"]["rows"]["items"]["additionalProperties"] is False + + def test_coerces_additional_properties_sub_schema(self): + """A sub-schema value (free-form map) is also rejected by Anthropic.""" + schema = { + "type": "object", + "additionalProperties": {"type": "string"}, + "properties": {"a": {"type": "string"}}, + } + + result = AnthropicConfig.filter_anthropic_output_schema(schema) + + assert result["additionalProperties"] is False + + def test_explicit_additional_properties_false_is_preserved(self): + """The already-correct value must survive untouched.""" + schema = { + "type": "object", + "additionalProperties": False, + "properties": {"a": {"type": "string"}}, + } + + result = AnthropicConfig.filter_anthropic_output_schema(schema) + + assert result["additionalProperties"] is False