Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions litellm/litellm_core_utils/prompt_templates/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -5496,13 +5496,19 @@ 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

_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)
Expand Down Expand Up @@ -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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't this mean that BedrockToolJsonSchemaBlock is incomplete / incorrect? this code basically sidesteps it and mutates the resulting dict directly

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I add both additionalProperties and strict to those TypedDicts, so the json_schema["additionalProperties"] = ... assignment is fully type-checked, not a bypass. I intended for this post-construction assignment. The keys are conditional (Claude-only, only when supplied) and should be absent rather than None when they don't apply

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea, why can't the constructor do this @mateo-berri

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it can't conditionally because a kwarg is always passed; the only constructor option is **-unpacking an extras dict, which still needs the same is not None guard and is less type-safe (mypy can check json_schema["key"] = v per-key, but ofc it cannot validate a **dict unpack). So the current post-construction assignment is actually the more type-checked choice

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)

Expand Down
2 changes: 2 additions & 0 deletions litellm/types/llms/bedrock.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ class ToolJsonSchemaBlock(TypedDict, total=False):
type: Literal["object"]
properties: dict
required: List[str]
additionalProperties: bool


class ToolInputSchemaBlock(TypedDict):
Expand All @@ -260,6 +261,7 @@ class ToolSpecBlock(TypedDict, total=False):
inputSchema: Required[ToolInputSchemaBlock]
name: Required[str]
description: str
strict: bool


class SystemToolBlock(TypedDict, total=False):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading