Skip to content

fix: repair anyOf tool schemas rejected by the Kimi wire - #14

Merged
elkaix merged 2 commits into
mainfrom
fix/kimi-anyof-tool-schema
Aug 4, 2026
Merged

fix: repair anyOf tool schemas rejected by the Kimi wire#14
elkaix merged 2 commits into
mainfrom
fix/kimi-anyof-tool-schema

Conversation

@elkaix

@elkaix elkaix commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Related Issue

No existing issue — the problem is described below. This is a reproducible bug fix with a focused diff.

Problem

Every request to a Kimi / Moonshot model fails before the model runs:

400 tools.function.parameters is not a valid moonshot flavored json schema,
details: <At path 'root': when using anyOf, type should be defined in anyOf items
instead of the parent schema>

The wire validator refuses a tool schema that uses anyOf as a refinement of the
node it sits on. Two rules are involved:

  • type may not appear next to anyOf on the same node.
  • No validation keyword (properties, items, additionalProperties, …) may
    appear on both a node and one of its anyOf branches.

Both are legal standard JSON Schema, so schemas that work on every other provider
are rejected here. TaskStop trips them: it adds a hand-written
anyOf: [{ required: ['task_id'] }, { required: ['shell_id'] }] on top of an
object schema that already declares type and properties. Because tool
definitions are sent with every request, a single offending tool takes down the
whole session.

normalizePythinkerToolSchema already existed for exactly this class of
provider-compatibility repair, but it only visited nested property schemas and
deliberately skipped the root, so it never saw the offending node.

What changed

The repair lives in the provider's schema normalizer rather than in TaskStop, so
MCP- and plugin-contributed schemas are covered by the same pass — this shape is
common enough that several other clients have hit it independently.

Nested nodes — distribute. A node's own constraints are pushed down into each
anyOf branch, and branches that already declare a keyword keep their own
(narrower) value. This is lossless: P ∧ (B₁ ∨ B₂) and (P ∧ B₁) ∨ (P ∧ B₂)
accept exactly the same instances. Sibling combinators (allOf / oneOf / not /
if) are left in place — the validator does not read them, so moving them would
only churn the schema.

Root — fold away. The root cannot use the branch form, because a tool's
parameters must be a plain object: the wire separately requires
parameters.type == "object", which cannot coexist with a root anyOf. The two
constraints are jointly unsatisfiable, so the root's anyOf is dropped. That only
ever widens what is accepted, and a root anyOf is in practice a "one of these
fields is required" hint that the tool re-checks at run time (TaskStop already
returns Missing required parameter: task_id). Branch properties are merged into
the root first, so a schema that kept its arguments inside the branches does not
silently lose them.

Verification

Rather than asserting against an assumed reading of the spec, every schema was
replayed through the provider's own validator:

  • Reproduced the original 400 against the live endpoint, then confirmed it is gone.
  • Replayed all 36 built-in tool schemas and all 70 tool schemas from five
    connected MCP servers
    through the normalizer and into the provider's validator:
    106 accepted, 0 rejected. Before the change, TaskStop was the single failure.
  • Full kosong suite passes (1185 tests); typecheck clean.
  • Each of the two new code paths was individually disabled to confirm the new tests
    actually fail without it.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update — no user-facing behavior or CLI surface changes.

Summary by CodeRabbit

  • Bug Fixes

    • Improved compatibility with Kimi and Moonshot models when tools use combined schema types.
    • Fixed invalid tool schema errors involving anyOf, oneOf, and allOf combinations.
    • Improved normalization of nested and object-based tool schemas while preserving metadata, constraints, references, and definitions.
    • Reduced duplicate schema alternatives and improved handling of required fields.
  • Tests

    • Added comprehensive coverage for root and nested schema normalization, cyclic definitions, and combinators.

Kimi rejects a tool whose parameters declare `anyOf` next to `type`, and
also rejects a validation keyword appearing on both a node and its `anyOf`
branches. TaskStop hits both: it adds a hand-written root `anyOf` to an
object schema, so every request carrying it failed with a 400 before the
model ever ran.

Repair the shape where all tools converge instead of at the one call site,
so MCP- and plugin-contributed schemas are covered too:

- Nested nodes distribute their own constraints into each branch, which
  accepts exactly the same instances as before.
- The root instead drops its `anyOf`. A tool's parameters must be an
  object, and the wire demands `type: "object"` there, so the branch form
  is unsatisfiable at that position; branch properties are merged up first
  so a tool cannot lose its arguments.

Verified by replaying every builtin and connected MCP tool schema through
the provider's own validator.
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e82b7357-994d-4dc4-a2d0-6c67909d8e0e

📥 Commits

Reviewing files that changed from the base of the PR and between 329e0b5 and 727d9d7.

📒 Files selected for processing (2)
  • packages/kosong/src/providers/pythinker-schema.ts
  • packages/kosong/test/providers/pythinker-schema.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/kosong/src/providers/pythinker-schema.ts
  • packages/kosong/test/providers/pythinker-schema.test.ts

📝 Walkthrough

Walkthrough

The schema normalizer folds root-level anyOf schemas into objects and distributes parent constraints into nested branches. Tests cover root, nested, cyclic, and combined combinator schemas. A patch changeset documents the Kimi and Moonshot validation fix.

Changes

Pythinker schema normalization

Layer / File(s) Summary
Root anyOf folding
packages/kosong/src/providers/pythinker-schema.ts
Root-level anyOf branches are merged into root properties. Distinct property schemas remain as nested alternatives. Duplicate alternatives are removed.
Nested anyOf constraint distribution
packages/kosong/src/providers/pythinker-schema.ts
Absent parent validation keywords are copied into anyOf branches and removed from the parent. Required fields are merged. Metadata, references, definitions, and combinators remain at the parent level.
Normalization coverage and release metadata
packages/kosong/test/providers/pythinker-schema.test.ts, .changeset/kimi-anyof-tool-schema.md
Tests cover root, nested, cyclic, and combined anyOf, oneOf, and allOf schemas. The changeset documents the Kimi and Moonshot fix.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title uses the fix prefix, stays within 72 characters, uses imperative wording, and accurately describes the schema repair.
Description check ✅ Passed The description covers the problem, changes, verification, related-issue rationale, tests, changeset, and documentation checklist.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Aug 4, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@pythoughts/pythinker-code@727d9d7
npx https://pkg.pr.new/@pythoughts/pythinker-code@727d9d7

commit: 727d9d7

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/kosong/test/providers/pythinker-schema.test.ts (1)

573-580: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use neutral test identifiers.

Replace TaskStop, task_id, and shell_id with neutral test identifiers. This test checks schema normalization and needs only two distinct generic field names.

As per coding guidelines, use neutral placeholders such as example.com, example.test, and YOUR_API_KEY instead of real internal identifiers in public text and test data.

Also applies to: 598-603

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/kosong/test/providers/pythinker-schema.test.ts` around lines 573 -
580, Update the test case around the root anyOf normalization scenario to use
neutral identifiers: replace the TaskStop reference and the task_id and shell_id
property names with two distinct generic field names, keeping the schema
structure and assertions unchanged.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/kosong/src/providers/pythinker-schema.ts`:
- Around line 354-358: Update the anyOf normalization logic around the
inherited-key loop in packages/kosong/src/providers/pythinker-schema.ts:354-358
so parent and branch constraints are combined rather than allowing an existing
branch key to replace the parent restriction; preserve conjunctive required
constraints across every branch. In
packages/kosong/test/providers/pythinker-schema.test.ts:615-638, replace the
current string-versus-integer items expectation with tests proving parent
constraints remain enforced in each normalized anyOf branch.
- Around line 163-169: The root anyOf merge in the branch-property loop must
preserve conflicting schemas instead of retaining only the first property
encountered. Update the merge logic around merged and cloneJsonValue so
duplicate names combine both alternatives without depending on iteration order,
preserving valid string-or-integer contracts for convertTool; add a regression
test covering conflicting root property names.

In `@packages/kosong/test/providers/pythinker-schema.test.ts`:
- Around line 654-671: Update the test case around normalizePythinkerToolSchema
to place the cyclic $defs and $ref directly on the object containing anyOf,
rather than on the root. Assert that both keys remain on the anyOf parent and
are absent from each branch, using non-vacuous checks that fail when the
expected retained values are missing.

---

Nitpick comments:
In `@packages/kosong/test/providers/pythinker-schema.test.ts`:
- Around line 573-580: Update the test case around the root anyOf normalization
scenario to use neutral identifiers: replace the TaskStop reference and the
task_id and shell_id property names with two distinct generic field names,
keeping the schema structure and assertions unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 38cfebe6-f821-4dc5-88b9-fc32980738d3

📥 Commits

Reviewing files that changed from the base of the PR and between 5ec7157 and 329e0b5.

📒 Files selected for processing (3)
  • .changeset/kimi-anyof-tool-schema.md
  • packages/kosong/src/providers/pythinker-schema.ts
  • packages/kosong/test/providers/pythinker-schema.test.ts

Comment thread packages/kosong/src/providers/pythinker-schema.ts Outdated
Comment thread packages/kosong/src/providers/pythinker-schema.ts
Comment thread packages/kosong/test/providers/pythinker-schema.test.ts Outdated
@elkaix
elkaix merged commit 0631ca4 into main Aug 4, 2026
12 checks passed
@elkaix
elkaix deleted the fix/kimi-anyof-tool-schema branch August 4, 2026 17:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant