fix(moonshot): also strip nullable/enum after anyOf collapse

The anyOf collapse in _repair_schema returned early, skipping the
nullable-strip and enum-cleanup steps. When a schema had anyOf
[{enum: [..., null, '']}, {type: null}] alongside a parent-level
'nullable: true', collapsing to the single non-null branch produced a
merged node that still had both 'nullable' and the bad enum values —
Moonshot would still 400 on it.

Fix: fall through to Rules 1/3 when the collapse produces a single
merged node; only return early for the multi-branch case (pure
anyOf preservation) or when there was no null branch to remove.

Adds a test that locks in the combined-case expectation.
This commit is contained in:
teknium1 2026-04-30 23:10:25 -07:00 committed by Teknium
parent 9cb5baeacf
commit 2af8b8ff37
2 changed files with 33 additions and 2 deletions

View File

@ -90,14 +90,20 @@ def _repair_schema(node: Any, is_schema: bool = True) -> Any:
if isinstance(b, dict) and b.get("type") != "null"]
if non_null and len(non_null) < len(repaired["anyOf"]):
# Drop the anyOf wrapper — keep only the non-null branch.
# If there's a single non-null branch, promote it.
# If there's a single non-null branch, promote it and fall
# through to Rules 1/3 so nullable/enum cleanup still applies
# to the merged node.
if len(non_null) == 1:
merge = {k: v for k, v in repaired.items() if k != "anyOf"}
merge.update(non_null[0])
repaired = merge
else:
repaired["anyOf"] = non_null
return repaired
return repaired
else:
# Nothing to collapse — parent type stripped, children already
# repaired by the recursive walk above.
return repaired
# Moonshot also rejects non-standard keywords like ``nullable`` on
# parameter schemas — strip it.

View File

@ -372,3 +372,28 @@ class TestEnumNullStripping:
out = sanitize_moonshot_tool_parameters(params)
# object-typed enum should pass through unchanged
assert "enum" in out["properties"]["config"]
def test_anyof_collapse_still_runs_nullable_and_enum_cleanup(self):
"""After anyOf collapses to a single non-null branch, the merged
node must still have ``nullable`` stripped and null/empty-string
values removed from enum not skipped by the early anyOf return.
"""
params = {
"type": "object",
"properties": {
"db_type": {
"anyOf": [
{"enum": ["mysql", "postgresql", "", None]},
{"type": "null"},
],
"nullable": True,
},
},
}
out = sanitize_moonshot_tool_parameters(params)
db_type = out["properties"]["db_type"]
assert "anyOf" not in db_type
assert "nullable" not in db_type, "nullable must be stripped after anyOf collapse"
assert db_type["type"] == "string"
assert db_type["enum"] == ["mysql", "postgresql"], \
"null/empty enum values must be stripped after anyOf collapse"