Skip to content

Invalid-tool-call error text echoes provider special tokens back into history, causing cascading tool-call failures (Kimi K2 on vLLM providers) #37297

Closed as not planned

Description

@Czaruno

Environment

  • OpenCode 1.17.15 (Desktop app and Homebrew CLI, macOS / Darwin 25.5)
  • Custom provider: npm: "@ai-sdk/openai-compatible" → Crusoe.ai
    (api.inference.crusoecloud.com, vLLM 0.24)
  • Model: moonshotai/Kimi-K2.6

Problem

Some OpenAI-compatible servers (vLLM 0.24 with the kimi_k2 tool-call parser) occasionally return tool calls whose arguments are invalid JSON containing the model's raw control-token text, e.g.:

{"pattern": "8765", "path": "/repo/app <|tool_calls_section_begin|> <|tool_call_begin|> functions.read:3 <|tool_call_argument_begin|> {"filePath": "/repo/app/server.py"}

That first failure is the provider's bug, and OpenCode handles it gracefully in principle: the call is converted to the invalid tool and the model gets an error result so it can retry (this recovery design is great).

However, the error text quotes the raw unparseable input verbatim:

Invalid input for tool grep: JSON parsing failed:
Text: {"pattern": "8765", "path": "/repo/app <|tool_calls_section_begin|> <|tool_call_begin|> ...

On the next request this string is rendered into the prompt as tool-result content, so the model's own control tokens (<|tool_calls_section_begin|>, <|tool_call_begin|>, <|tool_call_argument_begin|>, </think>) reappear inside message text. Depending on server tokenization these either become real control tokens or strongly prime the model to emit more malformed tool sections. Either way the effect is visible in live sessions: invalid tool calls cluster immediately after the first one (in one session 5 of 16 calls were invalid, two of them consecutive; the raw echoed text was present in each follow-up request).

Steps to reproduce

  1. Configure a custom openai-compatible provider backed by vLLM 0.24 serving Kimi K2.x with the kimi_k2 tool parser (Crusoe.ai's hosted moonshotai/Kimi-K2.6 exhibits this).
  2. Run any multi-tool agentic task, e.g. opencode run --auto "explore this repo, edit two files, verify".
  3. Wait for a JSON parsing failed invalid-tool event (~1 in 10–20 calls on this provider; edit/write calls with multiline arguments fail most), then observe elevated/consecutive failures on the following turns. Inspect part rows ("tool":"invalid") to see the echoed <|tool_call...|> text.

Suggested fix

Sanitize provider special-token sequences from the raw text before embedding it in the invalid-tool error message (and/or truncate the echoed text):

const SPECIAL_TOKEN_RE = /<\|\/?[a-z0-9_]+\|>/gi;   // ChatML/Kimi/GLM-style control tokens
errorText = rawText.replace(SPECIAL_TOKEN_RE, "[token]");

The model does not need to see its own control tokens to correct a malformed call — the JSON error position plus the cleaned text is sufficient — and no legitimate tool argument should contain <|...|> sequences.

Workaround we're using today

A user plugin on experimental.chat.messages.transform that scrubs <|...tool_call...|> sequences from text parts and tool state in history before every request. Works, but this seems to belong in core since it affects any provider whose tool parser can leak (Kimi K2 family on vLLM being the current prominent case).

Related issues

Happy to provide raw captures, failure-rate measurements (126 instrumented agentic trials against the raw provider API), and the plugin source.

Activity

  1. github-actions commented on Sep 16, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions