Make transformed tool required order deterministic (#4564)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Marcelo Trylesinski 2026-07-20 14:12:45 +02:00 committed by GitHub
commit 7934124fb5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 24 additions and 2 deletions

View file

@ -707,7 +707,8 @@ class TransformedTool(Tool):
schema = {
"type": "object",
"properties": new_props,
"required": list(new_required),
# Iterate props (not the set) for deterministic ordering
"required": [p for p in new_props if p in new_required],
"additionalProperties": False,
}
@ -899,7 +900,11 @@ class TransformedTool(Tool):
result = {
"type": "object",
"properties": merged_props,
"required": list(final_required),
# Iterate props (not the set) for deterministic ordering; keep any
# required names not present in properties (sorted) rather than
# silently dropping them.
"required": [p for p in merged_props if p in final_required]
+ sorted(final_required - set(merged_props)),
"additionalProperties": False,
}

View file

@ -41,6 +41,23 @@ def test_tool_from_tool_no_change(add_tool):
assert new_tool.description == add_tool.description
def test_transformed_tool_required_order_is_deterministic():
"""`required` must follow property order, not set iteration order.
Set iteration order varies with PYTHONHASHSEED, which broke snapshot
tests of tools/list output across processes.
"""
def fn(alpha: int, beta: str, gamma: float, delta: bool, epsilon: int) -> str:
return "x"
base = Tool.from_function(fn)
transformed = Tool.from_tool(base, transform_args={"alpha": ArgTransform(name="a")})
props = list(transformed.parameters["properties"])
assert transformed.parameters["required"] == props
assert props == ["a", "beta", "gamma", "delta", "epsilon"]
def test_from_tool_accepts_decorated_function():
@tool
def search(q: str, limit: int = 10) -> list[str]: