Skip to content

fix(moonshot): strip $ref siblings and collapse tuple items in tool schemas#18130

Closed
teknium1 wants to merge 1 commit into
mainfrom
opencode-port/moonshot-ref-siblings
Closed

fix(moonshot): strip $ref siblings and collapse tuple items in tool schemas#18130
teknium1 wants to merge 1 commit into
mainfrom
opencode-port/moonshot-ref-siblings

Conversation

@teknium1

@teknium1 teknium1 commented May 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Extends Hermes's Moonshot/Kimi tool-schema sanitizer with two repairs that MCP-sourced tool schemas routinely trip over, ported from anomalyco/opencode#24730.

Before: a subset of MCP tool schemas (any containing $ref with a description sibling, or any with tuple-style items) caused Moonshot/Kimi to return HTTP 400 tools.function.parameters is not a valid moonshot flavored json schema even though every other provider accepted them.

After: those two shapes are normalized in-place before the request goes out.

Root cause

Moonshot's schema validator expands $ref before validation, then rejects the node when any sibling keyword survives alongside $ref (including description, which MCP tools use heavily to expose the field hint to the model). Separately, its engine refuses tuple-style items arrays and requires one schema applied to every array element.

Hermes's existing sanitizer already handled Rules 1–2 (missing type, type at anyOf parent). The $ref path previously return repaired verbatim — passing any siblings straight through. Tuple items wasn't touched at all.

Changes

File What
agent/moonshot_schema.py Rule 3: $ref nodes collapse to {"$ref": "…"} only. Rule 4: items lists collapse to the first element schema (then run through the normal repair).
tests/agent/test_moonshot_schema.py +10 test cases across TestRefSiblingStripping and TestTupleItems. The pre-existing test_ref_node_is_not_given_synthetic_type still passes — it asserted a plain $ref passes through, and it still does (now exactly as {"$ref": "…"}).

Validation

  • scripts/run_tests.sh tests/agent/test_moonshot_schema.py → 35/35 passed.
  • E2E with real imports on a realistic mixed schema ($ref+description, tuple items, anyOf parent type, missing type, nested $defs): all four rules fire together, output is Moonshot-compatible, target definitions' own descriptions are preserved.

Source

Ported from anomalyco/opencode#24730 ("fix: sanitize tools for moonshot"), adapted from the TypeScript sanitizeMoonshot helper to fit Hermes's recursive _repair_schema walker. The opencode version also runs on kimi models routed through aggregators (OpenRouter); Hermes's existing is_moonshot_model() already covers that case.

…chemas

Port from anomalyco/opencode#24730: Moonshot's JSON Schema validator rejects
two shapes that the rest of the JSON Schema ecosystem accepts:

1. $ref nodes with sibling keywords. Moonshot expands the reference before
   validation and then rejects the node if keys like `description`, `type`,
   or `default` appear alongside $ref. MCP-sourced tool schemas commonly
   put a `description` on $ref-typed properties so the model sees the
   field hint — which worked on every provider except Moonshot.

2. Tuple-style `items` arrays (positional element schemas). Moonshot's
   engine requires ONE schema applied to every array element. Common in
   tool schemas generated from Go/Protobuf that model fixed-length arrays
   as `[{type:number}, {type:number}]`.

Repairs applied in `agent/moonshot_schema.py`:

- Rule 3: when a node has `$ref`, return `{"$ref": <value>}` only
  (strip every sibling). The referenced definition still carries its own
  description on the target node, which Moonshot accepts.
- Rule 4: when `items` is a list, collapse to the first element schema
  (falling back to `{}` which is then filled by the generic missing-type
  rule). Preserves `minItems` / `maxItems` / other siblings.

Tests: 10 new cases across TestRefSiblingStripping + TestTupleItems,
plus the existing TestMissingTypeFilled::test_ref_node_is_not_given_synthetic_type
still passes (it asserted plain $ref passes through; now it passes through
as exactly `{"$ref": "..."}` which is strictly compatible).

All 35 tests in test_moonshot_schema.py pass.
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists provider/kimi Kimi / Moonshot comp/agent Core agent loop, run_agent.py, prompt builder tool/mcp MCP client and OAuth labels May 1, 2026
@teknium1

Copy link
Copy Markdown
Contributor Author

Closing in favor of the merged salvage PR. Conflict resolution: main added an enum-null cleanup (its own Rule 3) between this PR's branch and current main; renumbered so enum=Rule 3, $ref-sibling=Rule 4, tuple-items=Rule 5. One test expectation updated to match main's anyOf-with-null collapse. 42 moonshot-schema tests pass, 98 adjacent moonshot/kimi tests pass, E2E with all 5 rules on one schema confirms correct interaction.

@teknium1 teknium1 closed this May 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent loop, run_agent.py, prompt builder P2 Medium — degraded but workaround exists provider/kimi Kimi / Moonshot tool/mcp MCP client and OAuth type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants