Skip to content

fix(kosong): preserve Kimi's native tool-call id format - #1

Closed
dv8128-agent[bot] wants to merge 1 commit into
mainfrom
fix/kimi-native-tool-call-id
Closed

fix(kosong): preserve Kimi's native tool-call id format#1
dv8128-agent[bot] wants to merge 1 commit into
mainfrom
fix/kimi-native-tool-call-id

Conversation

@dv8128-agent

@dv8128-agent dv8128-agent Bot commented Aug 1, 2026

Copy link
Copy Markdown

Related Issue

Resolve MoonshotAI#520

Problem

See linked issue. Kimi models are trained to see tool-call ids echoed back in history in the canonical native shape functions.<name>:<idx>. The cross-provider id normalizer added in MoonshotAI#327 rewrites that shape (functions.Read:0functions_Read_0) on every provider, including the request back to Kimi itself, so the model stops recognizing its own prior tool call and replies with reasoning and no tool_calls — the turn dies as APIEmptyResponseError. Kimi served through an OpenAI-compatible endpoint is configured as a generic openai provider and takes that rewrite; the official endpoint masks the bug by normalizing server-side.

The session dump attached to MoonshotAI#520 matches this path exactly: the provider is type = "openai" (Azure), the recorded history carries the canonical id functions.Skill:0, and the turn issues that tool call on step 1 and then fails on the step continuing from the tool result. That id is rewritten to functions_Skill_0 before the follow-up request leaves, which is what this PR stops.

What changed

A canonical native id is now passed through verbatim by the OpenAI chat-completions, OpenAI responses, and Kimi tool-call-id policies, in both the kosong and agent-core-v2 engines; every other id keeps its exact current behavior. Preservation is keyed on the id shape because self-hosted Kimi does not self-identify at the policy layer, and Anthropic keeps sanitizing so MoonshotAI#327's cross-provider fix stays intact.

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.

Appendix

Evidence

Same real request, varying only the history id string, N=25 per row, against a self-hosted Kimi-K2 (vLLM/SGLang) endpoint:

history id format tool call emitted
functions.Read:0 (canonical native) 25 / 25
call_Read_0 (neutral) 9 / 25
functions_Read_0 (sanitized — current behavior) 0 / 25

Sampling parameters do not move this; only the id format does. Captured at the wire on that endpoint, stock kimi-code sends functions_Read_0 on the OpenAI-provider route; with the fix it sends functions.Read:0, and an end-to-end run reads a file and completes the turn 5/5 with the canonical id preserved on both tool_calls[].id and the matching tool_call_id.

This matches vLLM's Chasing 100% Accuracy: Debugging Kimi K2's Tool-Calling on vLLM, whose recommended practice is to normalize history ids to functions.func_name:idx (a 4.4× success-rate gain), and Moonshot's tool_call_guidance.md: "The format of the tool ID is functions.{func_name}:{idx} … Incorrect tool-call IDs in multi-turn scenarios commonly cause tool-call failures."

Possibly also the cause of the reasoning-only APIEmptyResponseError reports in MoonshotAI#149, though no tool-call id was captured there, so that link is unproven.

Design notes

  • Why not stop sanitizing for OpenAI entirely? Sanitization still protects genuine cross-provider replay onto stricter backends. Preserving only the canonical shape is the smaller, lower-risk change.
  • Scope of the carve-out. Preservation applies to any openai-type provider, not only Kimi, because the id shape is the only available signal. The cost is that a Kimi-authored id replayed onto a different, stricter OpenAI-compatible backend is passed through instead of sanitized; functions.Read:0 is a valid free-form tool_call.id under the chat-completions spec, so a spec-compliant backend accepts it.
  • Length follows each API. Chat Completions puts no length limit on tool_call.id (the 64-char cap is on the function name), so a canonical id is preserved at any length — a long MCP tool name or multi-digit idx must round-trip intact. The Responses call_id does carry maxLength: 64 and is sent verbatim, so an over-long canonical id normalizes rather than being rejected at the wire. Both boundaries are pinned by tests.
  • Anthropic and Google are untouched. Anthropic's ^[a-zA-Z0-9_-]+$ charset keeps calling sanitizeToolCallId, pinned by a test; Google matches on function names, not ids.
  • Only the documented canonical shape is preserved. fix: normalize cross-provider tool call ids MoonshotAI/kimi-code#327 describes Kimi ids in the bare Write:6 form, and its tests use Write:6 / Read:9 / Bash:7. Those keep being sanitized here — the carve-out is deliberately limited to functions.<name>:<idx>, the shape Moonshot documents and the one the APIEmptyResponseError: thinking model (Kimi-K2.6 via Azure / openai-compatible provider) returns reasoning-only completion on the step that continues after a tool call MoonshotAI/kimi-code#520 dump actually carries. Widening the pattern to bare <name>:<idx> would match ordinary non-Kimi ids, so it is left out; if the bare form still occurs in the wild against a Kimi endpoint, it needs its own evidence and a separate change.

What is not proven

The dump confirms the setup, the canonical id, and the failing step, and the rewrite itself is plain from the code path. It does not capture the outbound payload, so the final link — that the rewritten id is what makes this particular Azure deployment answer with reasoning only — rests on the reproduction above rather than on MoonshotAI#520's own trace. A competing reading was raised in the issue thread, that the adapter conflates a reasoning-only stop with an empty response; that concerns error classification and is orthogonal to this fix. Confirmation from the reporter's Azure deployment would settle it.

Known pre-existing limitation

When two Responses ids differ only by their |<itemId> suffix, they collapse to one call-id and the uniqueness pass suffixes the second one, which loses canonicality:

before this PR:  functions_Read_0   functions_Read_0_2    (both non-canonical)
after this PR:   functions.Read:0   functions.Read:0_2    (first one fixed)

This is the existing behavior of the shared uniqueness pass from MoonshotAI#327, not a regression — the PR strictly improves it, and ids with distinct idx values are unaffected. Confining uniqueness to a single tool-call batch would fix the remainder but is a larger change to the shared normalizer, so it is left out of this fix.

Kimi models are trained to see tool-call ids echoed back in conversation
history in the canonical shape `functions.<name>:<idx>`. The
cross-provider id normalizer added in MoonshotAI#327 sanitizes these ids
(`functions.Read:0` -> `functions_Read_0`), after which Kimi emits
reasoning with no tool call and stops the turn (`APIEmptyResponseError`
/ `finishReason=stop`).

Kimi served through an OpenAI-compatible endpoint (litellm / vLLM /
SGLang / Azure) is configured as an `openai` provider, so the fix must
cover the OpenAI chat-completions and responses policies, not just the
native Kimi policy: that is the path in the linked issue, whose session
dump carries the canonical id `functions.Skill:0` in history. The
failure is masked on the official endpoint (which normalizes
server-side) but reproducible on self-hosted deployments, where the
history id format is the model's only cue.

Add `sanitizeToolCallIdPreservingNative` /
`sanitizeOpenAIResponsesCallIdPreservingNative`: a canonical native id
is passed through verbatim, everything else keeps the existing
normalization. Wire them into the OpenAI chat, OpenAI responses, and
Kimi tool-call-id policies in both the default (kosong) and
agent-core-v2 engines.

The carve-out is keyed on the id shape, so it applies to any `openai`
provider (Kimi behind such an endpoint does not self-identify as Kimi at
the policy layer, so the shape is the only signal). Trade-off: a
Kimi-authored id replayed onto a different, stricter OpenAI-compatible
backend is now passed through rather than sanitized; a canonical id is
nonetheless a valid free-form tool_call id under the OpenAI
chat-completions spec. Charset-restricted providers (Anthropic,
`^[a-zA-Z0-9_-]+$`) keep sanitizing, so MoonshotAI#327's Kimi -> Anthropic replay
fix is intact; Google (function names, not ids) is unaffected.

Resolve MoonshotAI#520.

Refs: Moonshot Kimi-K2 tool_call_guidance.md; vLLM blog "Chasing 100%
Accuracy: Debugging Kimi K2's Tool-Calling on vLLM".

Acked-by: Andrey Akinshin <andrey.akinshin@jetbrains.com>
@dv8128-agent dv8128-agent Bot closed this Aug 1, 2026
@dv8128-agent dv8128-agent Bot reopened this Aug 1, 2026
@dv8128-agent

dv8128-agent Bot commented Aug 1, 2026

Copy link
Copy Markdown
Author

CI green on the fork; opening upstream.

@dv8128-agent dv8128-agent Bot closed this Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant