Skip to content

Strict tool_call ID sanitization breaks Kimi K2.5 multi-turn tool calling #62319

@milo-operator

Description

@milo-operator

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

  1. Configure Kimi K2.5 as primary model via CanopyWave (openai-completions API)
  2. Send a task requiring multiple tool-calling rounds (e.g., reading several files then acting on results)
  3. 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

  1. Kimi K2.5 returns tool call IDs in native format: functions.<tool_name>:<index> (e.g., functions.read:0)

  2. 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;
  3. 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!
    }
  4. 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:0functionsread0

  5. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions