fix(bedrock): handle definitions keyword and schemaless json_object in native structured outputs

- Add `definitions` handling alongside `$defs` in schema normalization
  (older JSON Schema drafts use `definitions` instead of `$defs`)
- Fall back to tool-call approach when `response_format: {type: json_object}`
  has no explicit schema, since the native API requires one
- Add tests for both cases
This commit is contained in:
Nicholas Gigliotti 2026-02-14 16:17:53 -05:00
parent 212906118a
commit da8c1bd111
2 changed files with 61 additions and 7 deletions

View file

@ -757,11 +757,12 @@ class AmazonConverseConfig(BaseConfig):
result["items"] = AmazonConverseConfig._add_additional_properties_to_schema(
result["items"]
)
if "$defs" in result and isinstance(result["$defs"], dict):
result["$defs"] = {
k: AmazonConverseConfig._add_additional_properties_to_schema(v)
for k, v in result["$defs"].items()
}
for defs_key in ("$defs", "definitions"):
if defs_key in result and isinstance(result[defs_key], dict):
result[defs_key] = {
k: AmazonConverseConfig._add_additional_properties_to_schema(v)
for k, v in result[defs_key].items()
}
for key in ("anyOf", "allOf", "oneOf"):
if key in result and isinstance(result[key], list):
result[key] = [
@ -955,9 +956,11 @@ class AmazonConverseConfig(BaseConfig):
if "type" in value and value["type"] == "text":
return optional_params
if self._supports_native_structured_outputs(model):
if self._supports_native_structured_outputs(model) and json_schema is not None:
# Use Bedrock's native structured outputs API (outputConfig.textFormat)
# No synthetic tool injection, no fake_stream needed
# No synthetic tool injection, no fake_stream needed.
# Requires an explicit schema — json_object with no schema falls through
# to the tool-call path below.
output_config = self._create_output_config_for_response_format(
json_schema=json_schema,
name=name,

View file

@ -3308,6 +3308,57 @@ def test_add_additional_properties_non_object():
assert result == {"type": "string"}
def test_add_additional_properties_definitions():
"""Recursively processes object types inside 'definitions' (not just '$defs')."""
schema = {
"type": "object",
"properties": {
"item": {"$ref": "#/definitions/Item"},
},
"definitions": {
"Item": {
"type": "object",
"properties": {
"name": {"type": "string"},
"details": {
"type": "object",
"properties": {"weight": {"type": "number"}},
},
},
}
},
}
result = AmazonConverseConfig._add_additional_properties_to_schema(schema)
# Top-level
assert result["additionalProperties"] is False
# definitions object
assert result["definitions"]["Item"]["additionalProperties"] is False
# Nested object inside definitions
assert result["definitions"]["Item"]["properties"]["details"]["additionalProperties"] is False
def test_json_object_no_schema_falls_back_to_tool_call():
"""response_format: {type: json_object} with no schema should use tool-call fallback,
even for models that support native structured outputs."""
config = AmazonConverseConfig()
optional_params: dict = {}
non_default_params = {"response_format": {"type": "json_object"}}
result = config._translate_response_format_param(
value=non_default_params["response_format"],
model="anthropic.claude-sonnet-4-5-20250929-v1:0",
optional_params=optional_params,
non_default_params=non_default_params,
is_thinking_enabled=False,
)
# Should NOT use native outputConfig (no schema provided)
assert "outputConfig" not in result
# Should use tool-call fallback
assert "tools" in result
assert result["json_mode"] is True
def test_output_config_applies_additional_properties():
"""_create_output_config_for_response_format normalizes the schema."""
schema = {