fix(tools): inline the exporter's local $refs — strict providers only accept #/$defs/ #3
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/tool-schema-inline-refs"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Round two of Moonshot's schema strictness (round one was #2, the root
"type"). With the root fixed, their validator got as far as the properties and rejected the next thing:JsonSchemaExporterdeduplicates repeated types by emitting$refpointers to the first occurrence — Kagura's character tools have four properties of the sameEntryParam[]type, soappearance.itemscomes out as{"$ref": "#/properties/traits/items"}. Moonshot accepts refs only into#/$defs/.Fix: post-process the exported node and inline every local ref (deep-clone the target in place). Tool parameter shapes are small, so duplication is free and the result is maximally compatible — no ref of any style remains for a validator to dislike. Depth-capped at 64 so a genuinely recursive parameter type (inexpressible as a flat tool schema anyway) degrades to a leftover ref instead of hanging; JSON-pointer escapes (
~0/~1) handled.Regression test with the exact offending shape (two properties of one complex array type): asserts no
"$ref"anywhere and that the second property carries the full inlined item schema. Proven red with the inlining disabled. All 137 tests green.🤖 Generated with Claude Code
JsonSchemaExporter deduplicates repeated types by emitting $ref pointers to the first occurrence ("#/properties/traits/items"); Moonshot's validator rejects any ref outside #/$defs/ and fails the whole request. Tool parameter shapes are small, so inline every local ref instead of restructuring into $defs. Depth-capped: a genuinely recursive parameter type degrades to a leftover ref instead of hanging. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>Summary
Summary
Coverage
OpenRouter.Net - 68.8%
43F954A3762D43C2CD5DCD5CDF29C19D3789DC49A7DCA1C47__Base64DataUriPattern_0
OpenRouter.Net.Agents - 81.5%
OpenRouter.Net.Imaging - 82.2%
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! Round two of the great Moonshot schema war~ The deduplication
$refproblem — I've seen this one bite before. The exporter reuses the first occurrence and points the rest at it with#/properties/.../items-style pointers, and strict validators go "no, only#/$defs/allowed." A delightful little post-processing fix. ♪Verdict: ✅ Looks good to me~
I verified this one very carefully, because schema manipulation is where bugs love to hide. Let me show you what I found~
🔬 What I verified locally
Build: Clean — 0 warnings, 0 errors (net10.0,
TreatWarningsAsErrors+latest-recommendedanalysis).Tests: All 41/41 Agents tests pass, including the new
Schema_contains_no_local_refs_for_strict_providers.The new test is a REAL regression test, not a tautology — I disabled the
InlineLocalRefs(node, node, 0)call, rebuilt, and ran:Schema_contains_no_local_refs_for_strict_providerswithFound: "$ref"and the schema showing"items":{"$ref":"#/properties/Trai...— the exact offending shape from the PR description.Then I restored the fix → 41/41 green. Clean room. ♡
✅ The algorithm is sound — I traced every branch~
~1→/then~0→~): RFC 6901-compliant order. A property literally nameda~1bwould resolve correctly. ♪DeepClone(): the inlined node is independent of its source — no aliasing corruption when traversal mutatesobj[key]/array[i].clonewithdepth + 1— so nested refs get fully flattened. Correct..Select(p => p.Key).ToList()before mutatingobj[key]during enumeration: properly avoidsInvalidOperationException. A subtle thing many people get wrong. Good catch.https://...): theStartsWith("#/")guard leaves them untouched. Correct — those are legitimate and shouldn't be inlined.✅ Single chokepoint — no sibling inconsistency~
I confirmed
GetJsonSchemaAsNodeis called in exactly one place in the entire repo:Tool<TParams>.ParametersSchema. Every tool (WriteFileTool,ReadFileTool,SubAgentTool, all 9 of them) derives fromTool<TParams>, so the fix covers every tool automatically. There's no parallel schema-generation path that was missed.✅ Test design matches the sibling (#2)~
Schema_contains_no_local_refs_for_strict_providersfollows the same pattern as #2'sSchema_root_type_is_exactly_object_for_strict_providers: a dedicated param type (ProfileParamswith twoLabeledEntry[]?properties — the minimal reproducer), a focused[Fact], and an assertion that pins both the negative (no"$ref"anywhere) and the positive (the second property carries a fullproperties.Labelschema, not a pointer). Precise and minimal. ♡Automated review by Jibril · 2026-07-17
CI/CD: passed for head
246c703(forgejo-actions coverage bot —Tool\1` at 81.8% line / 63.6% branch, current) · Local checks: build 0/0, 41/41 tests, red-test proof run