Skip to content

Commit 7ff586d

Browse files
committed
feat: Updated schema additional properties to false when strict tool use
1 parent 24d0f3f commit 7ff586d

8 files changed

Lines changed: 79 additions & 12 deletions

File tree

src/strands/models/anthropic.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from ..types.exceptions import ContextWindowOverflowException, ModelThrottledException
2121
from ..types.streaming import StreamEvent
2222
from ..types.tools import ToolChoice, ToolChoiceToolDict, ToolSpec
23+
from ._strict_schema import ensure_strict_json_schema
2324
from ._validation import _has_location_source, validate_config_keys
2425
from .model import BaseModelConfig, Model
2526

@@ -231,7 +232,11 @@ def format_request(
231232
{
232233
"name": tool_spec["name"],
233234
"description": tool_spec["description"],
234-
"input_schema": tool_spec["inputSchema"]["json"],
235+
"input_schema": (
236+
ensure_strict_json_schema(tool_spec["inputSchema"]["json"])
237+
if tool_spec.get("strict")
238+
else tool_spec["inputSchema"]["json"]
239+
),
235240
**({"strict": tool_spec["strict"]} if "strict" in tool_spec else {}),
236241
}
237242
for tool_spec in tool_specs or []

src/strands/models/openai.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from ..types.exceptions import ContextWindowOverflowException, ModelThrottledException
2222
from ..types.streaming import StreamEvent
2323
from ..types.tools import ToolChoice, ToolResult, ToolSpec, ToolUse
24+
from ._strict_schema import ensure_strict_json_schema
2425
from ._validation import _has_location_source, validate_config_keys
2526
from .model import BaseModelConfig, Model
2627

@@ -481,7 +482,11 @@ def format_request(
481482
"function": {
482483
"name": tool_spec["name"],
483484
"description": tool_spec["description"],
484-
"parameters": tool_spec["inputSchema"]["json"],
485+
"parameters": (
486+
ensure_strict_json_schema(tool_spec["inputSchema"]["json"], require_all_properties=True)
487+
if tool_spec.get("strict")
488+
else tool_spec["inputSchema"]["json"]
489+
),
485490
**({"strict": tool_spec["strict"]} if "strict" in tool_spec else {}),
486491
},
487492
}

src/strands/models/openai_responses.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
from ..types.exceptions import ContextWindowOverflowException, ModelThrottledException # noqa: E402
5959
from ..types.streaming import StreamEvent # noqa: E402
6060
from ..types.tools import ToolChoice, ToolResult, ToolSpec, ToolUse # noqa: E402
61+
from ._strict_schema import ensure_strict_json_schema # noqa: E402
6162
from ._validation import validate_config_keys # noqa: E402
6263
from .model import BaseModelConfig, Model # noqa: E402
6364

@@ -516,7 +517,11 @@ def _format_request(
516517
"type": "function",
517518
"name": tool_spec["name"],
518519
"description": tool_spec.get("description", ""),
519-
"parameters": tool_spec["inputSchema"]["json"],
520+
"parameters": (
521+
ensure_strict_json_schema(tool_spec["inputSchema"]["json"], require_all_properties=True)
522+
if tool_spec.get("strict")
523+
else tool_spec["inputSchema"]["json"]
524+
),
520525
**({"strict": tool_spec["strict"]} if "strict" in tool_spec else {}),
521526
}
522527
for tool_spec in tool_specs

tests/strands/models/test_anthropic.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -460,12 +460,22 @@ def test_format_request_tool_choice_auto(model, messages, model_id, max_tokens):
460460

461461

462462
def test_format_request_tool_specs_with_strict(model, messages, model_id, max_tokens):
463-
tool_specs = [
464-
{"description": "test tool", "name": "test_tool", "inputSchema": {"json": {"key": "value"}}, "strict": True}
465-
]
466-
tru_request = model.format_request(messages, tool_specs)
463+
strict_tool_spec = {
464+
"description": "description",
465+
"name": "name",
466+
"inputSchema": {
467+
"json": {
468+
"type": "object",
469+
"properties": {"x": {"type": "string"}},
470+
}
471+
},
472+
"strict": True,
473+
}
474+
tru_request = model.format_request(messages, tool_specs=[strict_tool_spec])
475+
tool_in_request = tru_request["tools"][0]
467476

468-
assert tru_request["tools"][0]["strict"] is True
477+
assert tool_in_request["strict"] is True
478+
assert tool_in_request["input_schema"]["additionalProperties"] is False
469479

470480

471481
def test_format_request_tool_specs_with_strict_false(model, messages, model_id, max_tokens):

tests/strands/models/test_openai.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -642,14 +642,21 @@ def test_format_request_tool_specs_with_strict(model, messages, system_prompt):
642642
"name": "test_tool",
643643
"description": "A test tool",
644644
"inputSchema": {
645-
"json": {"type": "object", "properties": {"input": {"type": "string"}}, "required": ["input"]}
645+
"json": {
646+
"type": "object",
647+
"properties": {"input": {"type": "string"}, "optional_input": {"type": "string"}},
648+
"required": ["input"],
649+
}
646650
},
647651
"strict": True,
648652
}
649653
]
650654
tru_request = model.format_request(messages, strict_tool_specs, system_prompt)
651655

652-
assert tru_request["tools"][0]["function"]["strict"] is True
656+
tool_function = tru_request["tools"][0]["function"]
657+
assert tool_function["strict"] is True
658+
assert tool_function["parameters"]["additionalProperties"] is False
659+
assert set(tool_function["parameters"]["required"]) == {"input", "optional_input"}
653660

654661

655662
def test_format_request_tool_specs_with_strict_false(model, messages, system_prompt):

tests/strands/models/test_openai_responses.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -355,14 +355,21 @@ def test_format_request_tool_specs_with_strict(model, messages, system_prompt):
355355
"name": "test_tool",
356356
"description": "A test tool",
357357
"inputSchema": {
358-
"json": {"type": "object", "properties": {"input": {"type": "string"}}, "required": ["input"]}
358+
"json": {
359+
"type": "object",
360+
"properties": {"input": {"type": "string"}, "optional_input": {"type": "string"}},
361+
"required": ["input"],
362+
}
359363
},
360364
"strict": True,
361365
}
362366
]
363367
tru_request = model._format_request(messages, strict_tool_specs, system_prompt)
364368

365-
assert tru_request["tools"][0]["strict"] is True
369+
tool = tru_request["tools"][0]
370+
assert tool["strict"] is True
371+
assert tool["parameters"]["additionalProperties"] is False
372+
assert set(tool["parameters"]["required"]) == {"input", "optional_input"}
366373

367374

368375
def test_format_request_tool_specs_with_strict_false(model, messages, system_prompt):

tests_integ/models/test_model_anthropic.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,3 +221,17 @@ async def test_count_tokens_with_tools_greater_than_without(self, model, message
221221
without = await model.count_tokens(messages=messages)
222222
with_tools = await model.count_tokens(messages=messages, tool_specs=tool_specs, system_prompt="Be helpful.")
223223
assert with_tools > without
224+
225+
226+
def test_strict_tool_integration(model):
227+
"""Test that a strict tool invocation is accepted by the Anthropic API without a 400 validation error."""
228+
229+
@strands.tool(strict=True)
230+
def strict_tool(text: str) -> str:
231+
"""A strict tool for testing."""
232+
return f"Echo: {text}"
233+
234+
agent = Agent(model=model, tools=[strict_tool])
235+
# The API should accept the request and respond normally without throwing a 400 validation error
236+
result = agent("Call the strict tool with the text 'hello'")
237+
assert result.message is not None

tests_integ/models/test_model_openai.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,3 +438,17 @@ async def test_count_tokens_with_tools_greater_than_without(self, model, message
438438
without = await model.count_tokens(messages=messages)
439439
with_tools = await model.count_tokens(messages=messages, tool_specs=tool_specs, system_prompt="Be helpful.")
440440
assert with_tools > without
441+
442+
443+
def test_strict_tool_integration(model):
444+
"""Test that a strict tool invocation is accepted by the OpenAI API without a 400 validation error."""
445+
446+
@strands.tool(strict=True)
447+
def strict_tool(text: str) -> str:
448+
"""A strict tool for testing."""
449+
return f"Echo: {text}"
450+
451+
agent = Agent(model=model, tools=[strict_tool])
452+
# The API should accept the request and respond normally without throwing a 400 validation error
453+
result = agent("Call the strict tool with the text 'hello'")
454+
assert result.message is not None

0 commit comments

Comments
 (0)