feat(bedrock): pass strict and additionalProperties to Converse API toolSpec

Bedrock Converse API supports strict: true in toolSpec since 2026-02-04 (GA).
Previously, both strict and additionalProperties were implicitly dropped
during OpenAI-to-Bedrock tool conversion because ToolSpecBlock and
ToolJsonSchemaBlock only whitelisted a subset of fields.

This change:
- Adds strict to ToolSpecBlock and passes it through when present
- Adds additionalProperties to ToolJsonSchemaBlock and passes it through
  when present (required by Bedrock when strict: true is set)

Without additionalProperties: false, Bedrock rejects strict: true requests
with: "For 'object' type, 'additionalProperties' must be explicitly set to false"

Fixes the gap where Anthropic direct API path already supports strict
(PR #16725) but Bedrock Converse path does not.
This commit is contained in:
Ryze0323 2026-04-08 12:51:43 +09:00 committed by tj
parent 62757ff48f
commit c51fb46d85
3 changed files with 62 additions and 2 deletions

View file

@ -5174,15 +5174,25 @@ def _bedrock_tools_pt(tools: List) -> List[BedrockToolBlock]:
# with circular references (see issue #19098). unpack_defs handles nested
# refs recursively and correctly detects/skips circular references.
unpack_defs(parameters, defs_copy)
_additional_properties = parameters.get("additionalProperties", None)
tool_input_schema = BedrockToolInputSchemaBlock(
json=BedrockToolJsonSchemaBlock(
type=parameters.get("type", ""),
properties=parameters.get("properties", {}),
required=parameters.get("required", []),
**(
{"additionalProperties": _additional_properties}
if _additional_properties is not None
else {}
),
)
)
_strict = tool.get("function", {}).get("strict", None)
tool_spec = BedrockToolSpecBlock(
inputSchema=tool_input_schema, name=name, description=description
inputSchema=tool_input_schema,
name=name,
description=description,
**({"strict": _strict} if _strict is not None else {}),
)
tool_block = BedrockToolBlock(toolSpec=tool_spec)
tool_block_list.append(tool_block)

View file

@ -203,13 +203,16 @@ class ConverseResponseBlock(TypedDict, total=False):
str
] # end_turn | tool_use | max_tokens | stop_sequence | content_filtered
usage: Required[ConverseTokenUsageBlock]
serviceTier: ServiceTierBlock # Optional - only present when serviceTier was sent in request
serviceTier: (
ServiceTierBlock # Optional - only present when serviceTier was sent in request
)
class ToolJsonSchemaBlock(TypedDict, total=False):
type: Literal["object"]
properties: dict
required: List[str]
additionalProperties: bool
class ToolInputSchemaBlock(TypedDict):
@ -220,6 +223,7 @@ class ToolSpecBlock(TypedDict, total=False):
inputSchema: Required[ToolInputSchemaBlock]
name: Required[str]
description: str
strict: Optional[bool]
class SystemToolBlock(TypedDict, total=False):

View file

@ -1116,6 +1116,52 @@ def test_bedrock_tools_transformation_valid_params():
assert "test" in result[0]["toolSpec"]["inputSchema"]["json"]["required"]
def test_bedrock_tools_pt_strict_parameter():
"""Test that strict and additionalProperties are passed through to Bedrock toolSpec."""
tools_with_strict = [
{
"type": "function",
"function": {
"name": "generate_sql",
"strict": True,
"description": "Generate a SQL query",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string"},
},
"required": ["query"],
"additionalProperties": False,
},
},
}
]
result = _bedrock_tools_pt(tools_with_strict)
assert result[0]["toolSpec"]["strict"] is True
assert result[0]["toolSpec"]["inputSchema"]["json"]["additionalProperties"] is False
# Test without strict - should not have strict key or additionalProperties
tools_without_strict = [
{
"type": "function",
"function": {
"name": "generate_sql",
"description": "Generate a SQL query",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string"},
},
"required": ["query"],
},
},
}
]
result = _bedrock_tools_pt(tools_without_strict)
assert "strict" not in result[0]["toolSpec"]
assert "additionalProperties" not in result[0]["toolSpec"]["inputSchema"]["json"]
def test_not_found_error():
with pytest.raises(litellm.NotFoundError):
completion(