Skip to content

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

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

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions