From decf8e3f263534f316116a84bb70dcefa4c82fdb Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Tue, 15 Sep 2026 11:39:59 -0700 Subject: [PATCH] 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 --- tests/tools/test_schema_sanitizer.py | 14 ++++++++++++++ tools/schema_sanitizer.py | 8 +++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/tests/tools/test_schema_sanitizer.py b/tests/tools/test_schema_sanitizer.py index 018f621d97..e6b11bea81 100644 --- a/tests/tools/test_schema_sanitizer.py +++ b/tests/tools/test_schema_sanitizer.py @@ -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", diff --git a/tools/schema_sanitizer.py b/tools/schema_sanitizer.py index 644232882e..a8e8d8e627 100644 --- a/tools/schema_sanitizer.py +++ b/tools/schema_sanitizer.py @@ -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