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
This commit is contained in:
Darien Kindlund 2026-08-04 16:17:01 -04:00
parent 487074f602
commit 46751ad83e
2 changed files with 72 additions and 2 deletions

View file

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

View file

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