feat(bedrock): forward strict and additionalProperties to Converse toolSpec (#29814)

* feat(bedrock): forward strict and additionalProperties to Converse toolSpec

Bedrock Converse supports strict in toolSpec since 2026-02, but
_bedrock_tools_pt only whitelisted type/properties/required/name/description,
so strict: true was silently dropped and Claude-on-Bedrock ignored enum
constraints that GPT and direct-Anthropic honored. Forward strict from the
OpenAI function and additionalProperties from the schema (Bedrock requires
the latter alongside strict), passing each only when present.

https://claude.ai/code/session_01WQjWd8NfUB3vxERwudbHkv

* fix(bedrock): only forward strict tool schemas to Claude on Converse

Nova, Llama and GPT-OSS on Bedrock reject the strict field
(BedrockException 'This model doesn't support the strict field'), and the
GPT-OSS request-body test asserts strict/additionalProperties are stripped.
Forwarding them to every model broke the llm_translation suite, so gate the
forwarding on the anthropic base model since only Claude honours strict
tool schemas on Bedrock.
This commit is contained in:
Mateo Wang 2026-06-06 16:28:18 -07:00 committed by GitHub
parent 273855b4e2
commit d61f7747c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 74 additions and 6 deletions

View File

@ -5496,6 +5496,7 @@ def _bedrock_tools_pt(
]
"""
from litellm.llms.bedrock.common_utils import (
get_bedrock_base_model,
normalize_json_schema_custom_types_to_object,
)
from litellm.litellm_core_utils.prompt_templates.common_utils import unpack_defs
@ -5503,6 +5504,11 @@ def _bedrock_tools_pt(
_valid_json_schema_root_types = frozenset(
("array", "boolean", "integer", "null", "number", "object", "string")
)
# Only Claude on Bedrock honours strict tool schemas; other families
# (Nova, Llama, GPT-OSS) reject the strict field outright.
supports_strict_tools = bool(
model and get_bedrock_base_model(model).startswith("anthropic")
)
tool_block_list: List[BedrockToolBlock] = []
for tool_idx, tool in enumerate(tools):
# Check if tool is already a BedrockToolBlock (e.g., systemTool for Nova grounding)
@ -5548,16 +5554,21 @@ def _bedrock_tools_pt(
normalize_json_schema_custom_types_to_object(parameters)
if parameters.get("type") not in _valid_json_schema_root_types:
parameters["type"] = "object"
tool_input_schema = BedrockToolInputSchemaBlock(
json=BedrockToolJsonSchemaBlock(
type=parameters["type"],
properties=parameters.get("properties", {}),
required=parameters.get("required", []),
)
json_schema = BedrockToolJsonSchemaBlock(
type=parameters["type"],
properties=parameters.get("properties", {}),
required=parameters.get("required", []),
)
additional_properties = parameters.get("additionalProperties", None)
if supports_strict_tools and additional_properties is not None:
json_schema["additionalProperties"] = additional_properties
tool_input_schema = BedrockToolInputSchemaBlock(json=json_schema)
tool_spec = BedrockToolSpecBlock(
inputSchema=tool_input_schema, name=name, description=description
)
strict = tool.get("function", {}).get("strict", None)
if supports_strict_tools and strict is not None:
tool_spec["strict"] = strict
tool_block = BedrockToolBlock(toolSpec=tool_spec)
tool_block_list.append(tool_block)

View File

@ -250,6 +250,7 @@ class ToolJsonSchemaBlock(TypedDict, total=False):
type: Literal["object"]
properties: dict
required: List[str]
additionalProperties: bool
class ToolInputSchemaBlock(TypedDict):
@ -260,6 +261,7 @@ class ToolSpecBlock(TypedDict, total=False):
inputSchema: Required[ToolInputSchemaBlock]
name: Required[str]
description: str
strict: bool
class SystemToolBlock(TypedDict, total=False):

View File

@ -898,6 +898,61 @@ def test_bedrock_tools_unpack_defs():
_bedrock_tools_pt(tools=tools)
def test_bedrock_tools_pt_strict_parameter():
"""Regression for strict tools on the Bedrock Converse path.
Claude on Bedrock honours strict in toolSpec (with additionalProperties, which
Bedrock requires alongside strict); without forwarding it the model ignores the
enum constraint the caller asked for. Every other Bedrock family (Nova, Llama,
GPT-OSS) rejects the strict field, so it must only be forwarded for Claude.
"""
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, model="anthropic.claude-sonnet-4-5-20250929-v1:0"
)
assert result[0]["toolSpec"]["strict"] is True
assert result[0]["toolSpec"]["inputSchema"]["json"]["additionalProperties"] is False
result = _bedrock_tools_pt(tools_with_strict, model="us.amazon.nova-micro-v1:0")
assert "strict" not in result[0]["toolSpec"]
assert "additionalProperties" not in result[0]["toolSpec"]["inputSchema"]["json"]
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, model="anthropic.claude-sonnet-4-5-20250929-v1:0"
)
assert "strict" not in result[0]["toolSpec"]
assert "additionalProperties" not in result[0]["toolSpec"]["inputSchema"]["json"]
def test_bedrock_image_processor_content_type_fallback_url_extension():
"""
Test that _post_call_image_processing falls back to URL extension