Bug Description
OpenClaw's "strict" tool call ID sanitization ([^a-zA-Z0-9] removal) corrupts Kimi K2.5's native tool call IDs, causing multi-turn agentic conversations to fail after 2–3 tool-calling rounds.
Severity: Critical — all multi-turn agentic flows break for Kimi K2.5 via CanopyWave.
Reproduction
- Configure Kimi K2.5 as primary model via CanopyWave (
openai-completions API)
- Send a task requiring multiple tool-calling rounds (e.g., reading several files then acting on results)
- Observe: first 1–2 turns produce tool calls normally. Subsequent turns generate thinking + text indicating intent to call tools, but
stopReason: "stop" is returned instead of "toolUse".
Minimal API repro (streaming):
import requests, json
# Sanitized IDs (how OpenClaw sends them) → FAILS ~80%
messages = [
{"role": "system", "content": "You are an assistant. " + "Context. " * 7000},
{"role": "user", "content": "Read voice.md"},
{"role": "assistant", "content": None, "tool_calls": [
{"id": "functionsread0", "type": "function", # ← sanitized ID
"function": {"name": "read", "arguments": '{"file_path": "voice.md"}'}}
]},
{"role": "tool", "tool_call_id": "functionsread0", "content": "Voice: be direct."},
{"role": "user", "content": "Now read targets.md"},
]
# → finish_reason: "stop", no tool_calls (80% of the time)
# Native Kimi IDs → PASSES 100%
# Same conversation but with "functions.read:0" instead of "functionsread0"
# → finish_reason: "tool_calls", structured tool_calls returned
Root Cause
Code path
-
Kimi K2.5 returns tool call IDs in native format: functions.<tool_name>:<index> (e.g., functions.read:0)
-
resolveTranscriptPolicy() in pi-embedded-BYdcxQ5A.js:27254-27270 applies strict sanitization to all openai-completions providers that aren't OpenAI:
const requiresOpenAiCompatibleToolIdSanitization = params.modelApi === "openai-completions" || ...
const sanitizeToolCallIds = isGoogle || isMistral || isAnthropic || requiresOpenAiCompatibleToolIdSanitization;
const toolCallIdMode = ... sanitizeToolCallIds ? "strict" : void 0;
-
Kimi provider capabilities (pi-embedded-BYdcxQ5A.js:18482-18486) have no transcriptToolCallIdMode override:
kimi: {
anthropicToolSchemaMode: "openai-functions",
anthropicToolChoiceMode: "openai-string-modes",
openAiPayloadNormalizationMode: "moonshot-thinking"
// No transcriptToolCallIdMode!
}
-
sanitizeToolCallId() in pi-embedded-helpers-CGU2Pfj9.js:1298-1311 strips all non-alphanumeric characters:
const alphanumericOnly = id.replace(/[^a-zA-Z0-9]/g, "");
functions.read:0 → functionsread0
-
When this mangled ID is sent back in conversation history, Kimi's serving layer cannot match it to the original tool definition, causing finish_reason: "stop" instead of "tool_calls".
Test results
| ID Format |
Pass Rate (5 trials, streaming, 66K system prompt) |
functionsread0 (OpenClaw strict) |
1/5 (20%) |
functions.read:0 (Kimi native) |
5/5 (100%) |
call_abc123def456 (OpenAI style) |
5/5 (100%) |
Proposed Fix
Add transcriptToolCallIdMode: "default" to Kimi/Moonshot provider capabilities:
kimi: {
anthropicToolSchemaMode: "openai-functions",
anthropicToolChoiceMode: "openai-string-modes",
openAiPayloadNormalizationMode: "moonshot-thinking",
transcriptToolCallIdMode: "default" // Preserve native IDs
}
The same should apply to the moonshot entry.
Alternatively, a new sanitization mode that preserves . and : (common in Kimi/Moonshot IDs) while still stripping other special characters.
Environment
- OpenClaw 2026.4.2 (commit d74a122)
- macOS Darwin 25.3.0, Apple Silicon
- Kimi K2.5 via CanopyWave (
https://inference.canopywave.io/v1)
- pi-ai library (bundled in
node_modules/@mariozechner/pi-ai)
Additional Context
- CanopyWave support independently confirmed that tool_call ID format matters for Kimi (they pointed to Kimi API docs)
- The bug is nondeterministic (~80% failure rate with sanitized IDs), likely due to internal serving-layer fallback behavior
- Single-turn tool calls are NOT affected (no conversation history to corrupt)
- The
openai style call_<UUID> format also works fine — only the stripped-alphanumeric format fails
Bug Description
OpenClaw's "strict" tool call ID sanitization (
[^a-zA-Z0-9]removal) corrupts Kimi K2.5's native tool call IDs, causing multi-turn agentic conversations to fail after 2–3 tool-calling rounds.Severity: Critical — all multi-turn agentic flows break for Kimi K2.5 via CanopyWave.
Reproduction
openai-completionsAPI)stopReason: "stop"is returned instead of"toolUse".Minimal API repro (streaming):
Root Cause
Code path
Kimi K2.5 returns tool call IDs in native format:
functions.<tool_name>:<index>(e.g.,functions.read:0)resolveTranscriptPolicy()inpi-embedded-BYdcxQ5A.js:27254-27270applies strict sanitization to allopenai-completionsproviders that aren't OpenAI:Kimi provider capabilities (
pi-embedded-BYdcxQ5A.js:18482-18486) have notranscriptToolCallIdModeoverride:sanitizeToolCallId()inpi-embedded-helpers-CGU2Pfj9.js:1298-1311strips all non-alphanumeric characters:functions.read:0→functionsread0When this mangled ID is sent back in conversation history, Kimi's serving layer cannot match it to the original tool definition, causing
finish_reason: "stop"instead of"tool_calls".Test results
functionsread0(OpenClaw strict)functions.read:0(Kimi native)call_abc123def456(OpenAI style)Proposed Fix
Add
transcriptToolCallIdMode: "default"to Kimi/Moonshot provider capabilities:The same should apply to the
moonshotentry.Alternatively, a new sanitization mode that preserves
.and:(common in Kimi/Moonshot IDs) while still stripping other special characters.Environment
https://inference.canopywave.io/v1)node_modules/@mariozechner/pi-ai)Additional Context
openaistylecall_<UUID>format also works fine — only the stripped-alphanumeric format fails