fix(tools): keep an empty required: [] in sanitized tool schemas

`_sanitize_node` deleted the `required` key whenever the pruned list came
out empty. Four built-in tools (skills_list, todo, delegate_task,
session_search) declare `required: []`, so they left the sanitizer with no
key at all. Strict OpenAI-compatible proxies read the missing key as
`null` and 400 the whole request ("null is not of type array"), which is
non-retryable and kills the session on its first call.

An empty array is valid for every backend; the pruning was added (34c3e67)
to drop names that are not in `properties`, not to delete the key. Keep the
key with the filtered list, even when that list is empty.

Fixes #111684
Fixes #59386
Co-authored-by: Cr4ckMe <jiqing.liu@whu.edu.cn>
This commit is contained in:
teknium1
2026-09-15 11:39:59 -07:00
committed by Teknium
parent 0724a6a0fd
commit decf8e3f26
2 changed files with 17 additions and 5 deletions
+14
View File
@@ -156,6 +156,20 @@ def test_required_pruned_to_existing_properties():
assert out[0]["function"]["parameters"]["required"] == ["name"]
def test_empty_required_key_survives_sanitization():
"""A ``required`` list that is empty (or emptied by pruning) is kept as ``[]`` — strict
OpenAI-compatible proxies read a missing key as ``null`` and 400 the whole request."""
declared_empty = _tool("t", {"type": "object", "properties": {}, "required": []})
all_pruned = _tool("u", {
"type": "object",
"properties": {"name": {"type": "string"}},
"required": ["missing_field"],
})
out = sanitize_tool_schemas([declared_empty, all_pruned])
assert out[0]["function"]["parameters"]["required"] == []
assert out[1]["function"]["parameters"]["required"] == []
def test_well_formed_schema_unchanged():
schema = {
"type": "object",
+3 -5
View File
@@ -315,11 +315,9 @@ def _sanitize_node(node: Any, path: str) -> Any:
if not isinstance(out.get("properties"), dict):
out["properties"] = {}
if isinstance(out.get("required"), list):
valid = [r for r in out["required"] if isinstance(r, str) and r in out["properties"]]
if valid:
out["required"] = valid
else:
del out["required"]
# Keep the key even when nothing survives: ``required: []`` is valid everywhere,
# while a missing key reads as ``null`` on strict OpenAI-compatible proxies.
out["required"] = [r for r in out["required"] if isinstance(r, str) and r in out["properties"]]
return out