diff --git a/litellm/litellm_core_utils/prompt_templates/factory.py b/litellm/litellm_core_utils/prompt_templates/factory.py index 1460dbaf0a9..588a183d58e 100644 --- a/litellm/litellm_core_utils/prompt_templates/factory.py +++ b/litellm/litellm_core_utils/prompt_templates/factory.py @@ -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) diff --git a/litellm/types/llms/bedrock.py b/litellm/types/llms/bedrock.py index 60e640636aa..ed80b44414b 100644 --- a/litellm/types/llms/bedrock.py +++ b/litellm/types/llms/bedrock.py @@ -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): diff --git a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py index 91fd07dcffc..3bf3b04bf14 100644 --- a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py +++ b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py @@ -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