Skip to content

fix(provider): harden Kimi Moonshot tool-call compatibility#378

Merged
Astro-Han merged 5 commits into
devfrom
codex/i377-kimi-protocol
May 2, 2026
Merged

fix(provider): harden Kimi Moonshot tool-call compatibility#378
Astro-Han merged 5 commits into
devfrom
codex/i377-kimi-protocol

Conversation

@Astro-Han

@Astro-Han Astro-Han commented May 2, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add a shared Kimi/Moonshot model detector for provider transform compatibility.
  • Filter empty message content for Kimi/Moonshot-compatible requests before provider calls.
  • Extend Moonshot/Kimi schema sanitization to cover Kimi for Coding aliases and infer missing property type values in schema positions, following Kimi Code CLI's stricter-schema compatibility pattern.
  • Sanitize final OpenAI-compatible chat request bodies so assistant tool-call messages with effectively empty content omit the content field, matching Kimi Code CLI's empty-content workaround.

Why

Kimi/Moonshot-compatible endpoints are stricter than many OpenAI-compatible providers about empty message content and JSON schema shape. PawWork already sanitized $ref siblings and preserved root $defs / definitions, but Kimi for Coding aliases such as kimi-for-coding/k2p6 could miss that path, and schemas with obvious intent but missing type could still be rejected.

This keeps the fix at the provider protocol layer. It does not add Kimi-specific GitHub behavior, prompt tuning, compaction changes, or loop containment. The repeated successful tool-call loop work remains tracked separately by #279.

Related Issue

Closes #377

Human Review Status

Pending. A human should make the final merge decision after reviewing the final diff and verification evidence.

Review Focus

Please focus on whether isKimiMoonshotModel is narrow enough for known Kimi/Moonshot aliases, whether final request-body content omission is correctly limited to empty assistant tool-call messages, and whether missing-type normalization stays limited to schema positions instead of metadata containers.

Risk Notes

Low to moderate provider-layer risk. The empty-content filtering, final request-body cleanup, and schema type normalization are gated to Kimi/Moonshot-compatible models, but that detector intentionally includes k2p* aliases so Kimi for Coding paths are covered.

How To Verify

Red test check: targeted Kimi/Moonshot tests failed before implementation with expected failures for alias coverage, missing schema types, and final empty content handling.
Targeted tests: bun --cwd packages/opencode test test/provider/transform.test.ts --test-name-pattern "Moonshot/Kimi|Kimi/Moonshot empty" -> 10 pass, 0 fail.
Provider transform tests: bun --cwd packages/opencode test test/provider/transform.test.ts -> 160 pass, 0 fail.
Typecheck: bun run --cwd packages/opencode typecheck -> exit 0.
Diff check: git diff --check -> exit 0.

Screenshots or Recordings

Not applicable. No visible UI changes.

Checklist

  • Human review status is stated above as pending, approved, or not required
  • I linked the related issue, or stated why there is no issue
  • This PR has type, scope, and priority labels, or I requested maintainer labeling
  • I described the review focus and any meaningful risks
  • I listed the relevant verification steps and the key result for each
  • I did not introduce unrelated refactors, dependencies, generated files, or file changes beyond the stated scope
  • I manually checked visible UI or copy changes when needed, with screenshots or recordings
  • I considered macOS and Windows impact for desktop, packaging, updater, signing, paths, shell, or permissions changes
  • I called out docs, release notes, dependencies, permissions, credentials, deletion behavior, generated content, or local file changes when relevant
  • I reviewed the final diff for unrelated changes and suspicious dependency changes
  • I am targeting dev, and my PR title and commit messages use Conventional Commits in English

Summary by CodeRabbit

  • Bug Fixes

    • Assistant messages with effectively empty content are now omitted when tool-calls are present for compatible models; POST payloads are rewritten to be OpenAI-compatible for those models.
    • JSON schema sanitization improved to better infer and populate missing type fields and remove improper sibling descriptions.
  • Tests

    • Added comprehensive tests covering message filtering, request-body behavior, and schema type inference.

@coderabbitai

coderabbitai Bot commented May 2, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 49616650-77a2-4645-a96f-60f600466d17

📥 Commits

Reviewing files that changed from the base of the PR and between 9553ce2 and fb8974e.

📒 Files selected for processing (1)
  • packages/opencode/test/provider/transform.test.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/opencode/test/provider/transform.test.ts

📝 Walkthrough

Walkthrough

Adds a Kimi/Moonshot model classifier and applies Kimi-specific message filtering and schema sanitization; introduces and exports openAICompatibleRequestBody and openAICompatibleRequestBodyText; and uses the text helper in the provider fetch wrapper to rewrite POST bodies for OpenAI-compatible models.

Changes

Kimi/Moonshot Protocol Compatibility

Layer / File(s) Summary
Classifier & Helpers
packages/opencode/src/provider/transform.ts
Add isKimiMoonshotModel(model) and helper utilities (isPlainObject, schema key constants, inferSchemaType, shouldSkipSchemaType, isEffectivelyEmptyOpenAIContent) used to drive message and schema logic.
Message Normalization
packages/opencode/src/provider/transform.ts
normalizeMessages now applies filterEmptyMessageContent when isKimiMoonshotModel(model) is true, removing assistant messages with empty content (empty string/array) and trimming empty text/reasoning parts around tool calls.
Schema Sanitization
packages/opencode/src/provider/transform.ts
Guard changed to isKimiMoonshotModel(model); sanitizeMoonshot refactored to accept a schemaPosition flag, traverse properties/$defs/definitions, respect combinators (anyOf/oneOf/allOf/not/if/then/else), and conservatively infer and inject missing type fields from enum/const shapes or schema-intent keys when safe; preserves $ref referenced $defs.
OpenAI-compatible Request Rewrite
packages/opencode/src/provider/transform.ts
Add and export openAICompatibleRequestBody(model, body) that, for Kimi/Moonshot models, removes an assistant message content when tool_calls exist and the content is effectively empty; add openAICompatibleRequestBodyText(model, body) that returns the JSON-stringified rewritten body (or undefined on non-string input / stringify failure).
Provider Fetch Wiring
packages/opencode/src/provider/provider.ts
In resolveSDKoptions.fetch wrapper, when opts.body exists, opts.method === "POST", and model.api.npm includes @ai-sdk/openai-compatible, replace opts.body with the result of ProviderTransform.openAICompatibleRequestBodyText(model, opts.body) if present. Existing @ai-sdk/openai id-field stripping remains unchanged.
Tests
packages/opencode/test/provider/transform.test.ts
Added/expanded tests: ensure Kimi coding aliases and k2p canonical aliases enter compatibility path (not triggered by display/name alone); verify $ref sibling description removal while preserving referenced $defs; validate conservative missing type inference from constraints and positions; assert empty-content filtering and openAICompatibleRequestBody/openAICompatibleRequestBodyText behavior (rewrites only for valid JSON and only for Kimi-compatible models).

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client as Client
    participant SDK as resolveSDK
    participant Transform as ProviderTransform
    participant ProviderAPI as Upstream Provider

    Client->>SDK: POST request (body JSON/string)
    SDK->>SDK: if body is string, try parse JSON
    SDK->>Transform: openAICompatibleRequestBodyText(model, body)
    Transform->>Transform: isKimiMoonshotModel? → if true, strip empty assistant content when tool_calls present
    Transform-->>SDK: transformed body string or undefined
    SDK->>ProviderAPI: forward POST with transformed or original body
    ProviderAPI-->>Client: response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

bug, P2

Poem

🐰 I hopped through schema groves at night,
I nudged lost types back into sight.
Empty assistant notes swept away,
Tool calls sing, tidy as day.
Kimi moons grin — the payloads are bright.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: hardening Kimi Moonshot tool-call compatibility by adding filtering and schema normalization.
Description check ✅ Passed The description is comprehensive and follows the template, covering summary, why, related issue, review focus, risk notes, and verification steps with detailed checklists.
Linked Issues check ✅ Passed All in-scope objectives from #377 are met: Kimi/Moonshot detector added, empty content filtering implemented, schema sanitization extended with type inference, and comprehensive tests added.
Out of Scope Changes check ✅ Passed All changes are tightly scoped to provider protocol compatibility; no GitHub-specific behavior, prompt tuning, context compaction, or loop containment changes are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/i377-kimi-protocol

Review rate limit: 5/10 reviews remaining, refill in 27 minutes and 49 seconds.

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

@Astro-Han Astro-Han added task Narrow execution, audit, spike, migration, tracking, or upstream follow-up work P1 High priority harness Model harness, prompts, tool descriptions, and session mechanics labels May 2, 2026

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a helper function isKimiMoonshotModel to identify Kimi and Moonshot models across various identifier fields and applies message normalization to filter empty content for these models. Additionally, the schema transformation logic is enhanced to automatically infer missing type fields (object, array, or string) based on the presence of properties, items, or enums. A review comment suggests refining the regex used for model identification to correctly handle space-separated identifiers within the joined string.

Comment thread packages/opencode/src/provider/transform.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/opencode/src/provider/transform.ts`:
- Around line 72-74: isKimiMoonshotModel currently includes model.name in the
identification string which allows display labels to opt into Kimi/Moonshot
behavior; remove model.name from the checked fields so only canonical
identifiers are considered. Update the function to build ids from model.id,
model.providerID, and model.api.id (filter(Boolean).join(" ").toLowerCase()) and
keep the existing substring checks and regex (/k2p.../) unchanged, ensuring
display label changes do not trigger Kimi/Moonshot rewrites.
- Around line 1113-1118: The missing-type inference branch (the if (!("type" in
result)) block) doesn't handle boolean enums; extend it to detect when
result.enum is an array of booleans (e.g., [true,false] or all values typeof
"boolean") and set result.type = "boolean" just like it sets "string" for string
enums; update the checks around result.enum in that block so it assigns
"boolean" when appropriate while leaving existing object/array/string handling
intact.
🪄 Autofix (Beta)

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 Plus

Run ID: 48a2d281-5727-412a-8245-c3821179dd25

📥 Commits

Reviewing files that changed from the base of the PR and between 0d8cae9 and a2c6352.

📒 Files selected for processing (2)
  • packages/opencode/src/provider/transform.ts
  • packages/opencode/test/provider/transform.test.ts

Comment thread packages/opencode/src/provider/transform.ts Outdated
Comment thread packages/opencode/src/provider/transform.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
packages/opencode/src/provider/transform.ts (1)

72-74: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep Kimi/Moonshot detection off display names.

Line 73 still includes model.name, so a label like "Kimi-compatible proxy" can opt an unrelated model into empty-message filtering and schema rewrites. That breaks the stated goal of leaving non-Kimi providers unchanged.

Suggested fix
-  const ids = [model.id, model.providerID, model.name, model.api.id].filter(Boolean).join(" ").toLowerCase()
+  const ids = [model.id, model.providerID, model.api.id].filter(Boolean).join(" ").toLowerCase()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/src/provider/transform.ts` around lines 72 - 74, The
isKimiMoonshotModel function is incorrectly checking model.name so display
labels like "Kimi-compatible proxy" can trigger Kimi/Moonshot logic; remove
model.name from the assembled ids string and only use model.id,
model.providerID, and model.api.id (preserve the filter(Boolean).join("
").toLowerCase() flow) so the includes("moonshot"), includes("kimi") checks and
the k2p regex run only against canonical identifiers, leaving providers with
Kimi-like display names unaffected.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/opencode/src/provider/provider.ts`:
- Around line 1516-1519: The conditional that rewrites POST bodies only checks
exact equality of model.api.npm to "@ai-sdk/openai-compatible", so
versioned/pinned specs like "npm:`@ai-sdk/openai-compatible`@2" are skipped;
update the condition around model.api.npm in the block that calls
ProviderTransform.openAICompatibleRequestBody (the if that also checks opts.body
and opts.method === "POST") to detect any OpenAI-compatible package spec (e.g.,
use a startsWith or regex match such as testing
model.api.npm?.includes("@ai-sdk/openai-compatible") or
/^(`@ai-sdk`\/openai-compatible)(@|$)/) so all aliased/versioned/pinned forms
trigger the same request-body rewrite.

In `@packages/opencode/src/provider/transform.ts`:
- Around line 95-117: schemaTypeFromValues and inferSchemaType currently default
ambiguous or mixed-type cases to "string"; change them to return undefined for
any non-conclusive inference so we don't falsely inject type: "string".
Specifically, update schemaTypeFromValues to return undefined when types.size >
1 (except the integer+number collapse which stays returning "number"), and
update inferSchemaType to return undefined as the final fallback instead of
"string" (keep existing branches for enum, const, OBJECT_SCHEMA_KEYS,
ARRAY_SCHEMA_KEYS, STRING_SCHEMA_KEYS, NUMBER_SCHEMA_KEYS). This ensures only
clear/obvious type inferences produce a concrete string type.

---

Duplicate comments:
In `@packages/opencode/src/provider/transform.ts`:
- Around line 72-74: The isKimiMoonshotModel function is incorrectly checking
model.name so display labels like "Kimi-compatible proxy" can trigger
Kimi/Moonshot logic; remove model.name from the assembled ids string and only
use model.id, model.providerID, and model.api.id (preserve the
filter(Boolean).join(" ").toLowerCase() flow) so the includes("moonshot"),
includes("kimi") checks and the k2p regex run only against canonical
identifiers, leaving providers with Kimi-like display names unaffected.
🪄 Autofix (Beta)

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 Plus

Run ID: 27958a11-6252-405a-93b9-ab4b787cee28

📥 Commits

Reviewing files that changed from the base of the PR and between a2c6352 and 08164da.

📒 Files selected for processing (3)
  • packages/opencode/src/provider/provider.ts
  • packages/opencode/src/provider/transform.ts
  • packages/opencode/test/provider/transform.test.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/opencode/test/provider/transform.test.ts

Comment thread packages/opencode/src/provider/provider.ts Outdated
Comment thread packages/opencode/src/provider/transform.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/opencode/src/provider/provider.ts`:
- Around line 1516-1519: The request-body rewrite assumes opts.body is a JSON
string and calls JSON.parse directly; wrap this in a safe guard: first check
opts.body is a string and the request has a JSON content-type (e.g., headers
include "application/json"), then attempt to parse inside a try/catch; on parse
success call ProviderTransform.openAICompatibleRequestBody(model, body) and
reassign opts.body to the stringified result, but on parse failure or non-JSON
content simply leave opts.body unchanged (fail open) so the request proceeds to
fetch. Target the conditional that checks
model.api.npm.includes("@ai-sdk/openai-compatible") && opts.body && opts.method
=== "POST" and update the parse logic there.
🪄 Autofix (Beta)

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 Plus

Run ID: 0de195ba-7571-474b-8282-e2a23c351db6

📥 Commits

Reviewing files that changed from the base of the PR and between 08164da and ec636c5.

📒 Files selected for processing (3)
  • packages/opencode/src/provider/provider.ts
  • packages/opencode/src/provider/transform.ts
  • packages/opencode/test/provider/transform.test.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/opencode/test/provider/transform.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/opencode/src/provider/transform.ts

Comment thread packages/opencode/src/provider/provider.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/opencode/test/provider/transform.test.ts (1)

2014-2046: ⚡ Quick win

Add one negative test to lock the tool-call guard scope

Please add a regression asserting Kimi assistant messages with empty content but without tool_calls are left unchanged by openAICompatibleRequestBody. That directly protects the “assistant tool-call messages only” boundary.

Proposed test addition
   test("omits empty content from final OpenAI-compatible assistant tool-call payloads", () => {
@@
     expect(result.messages[1].content).toEqual([{ type: "text", text: "not empty" }])
   })
+
+  test("does not omit empty content when assistant message has no tool calls", () => {
+    const result = ProviderTransform.openAICompatibleRequestBody(kimiModel, {
+      model: "k2p6",
+      messages: [
+        {
+          role: "assistant",
+          content: "",
+        },
+      ],
+    }) as any
+
+    expect(result.messages[0].content).toBe("")
+  })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/test/provider/transform.test.ts` around lines 2014 - 2046,
Add a negative regression in the existing test that calls
ProviderTransform.openAICompatibleRequestBody (using kimiModel) to verify that
an assistant message with empty content but no tool_calls is NOT stripped—i.e.,
include an assistant message where content is "" and tool_calls is
undefined/absent, call openAICompatibleRequestBody, and assert the resulting
message still has the content property (equal to "" or unchanged) so the removal
logic only applies when tool_calls are present; use the same test setup pattern
as the surrounding assertions to locate where to place this extra expect.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/opencode/test/provider/transform.test.ts`:
- Around line 2014-2046: Add a negative regression in the existing test that
calls ProviderTransform.openAICompatibleRequestBody (using kimiModel) to verify
that an assistant message with empty content but no tool_calls is NOT
stripped—i.e., include an assistant message where content is "" and tool_calls
is undefined/absent, call openAICompatibleRequestBody, and assert the resulting
message still has the content property (equal to "" or unchanged) so the removal
logic only applies when tool_calls are present; use the same test setup pattern
as the surrounding assertions to locate where to place this extra expect.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 848c8e18-c89f-4791-9682-e7c391be49cc

📥 Commits

Reviewing files that changed from the base of the PR and between ec636c5 and 9553ce2.

📒 Files selected for processing (3)
  • packages/opencode/src/provider/provider.ts
  • packages/opencode/src/provider/transform.ts
  • packages/opencode/test/provider/transform.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/opencode/src/provider/provider.ts
  • packages/opencode/src/provider/transform.ts

@Astro-Han Astro-Han merged commit 65f0229 into dev May 2, 2026
27 checks passed
@Astro-Han Astro-Han deleted the codex/i377-kimi-protocol branch May 2, 2026 11:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

harness Model harness, prompts, tool descriptions, and session mechanics P1 High priority task Narrow execution, audit, spike, migration, tracking, or upstream follow-up work

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Task] Harden Kimi/Moonshot protocol compatibility for tool calls

1 participant