vllm - ✅(Solved) Fix [Bug]: kimi-k2 tool parser regex is off a tiny bit [1 pull requests, 1 comments, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
vllm-project/vllm#38441Fetched 2026-04-08 01:45:38
View on GitHub
Comments
1
Participants
1
Timeline
8
Reactions
0
Participants
Timeline (top)
referenced ×4cross-referenced ×2commented ×1labeled ×1

Root Cause

(I am unable to provide the full trace here because it has a lot of personal crap in it, and I only observe this with long contexts and unable to repro as a canonical example).

Fix Action

Fixed

PR fix notes

PR #38443: [Bugfix][Tool Parser] Fix Kimi-K2 streaming regex to handle leading newline before tool call ID

Description (problem / solution / changelog)

Summary

Fixes #38441.

The model (Kimi K2 / K2.5) occasionally emits a stray \n between <|tool_call_begin|> and the function name during streaming (observed on long-context inference with tool_choice: auto, without constrained decoding):

# What the model sometimes produces:
<|tool_call_begin|>
functions.edit:15<|tool_call_argument_begin|>{"path": "..."}

# Instead of:
<|tool_call_begin|>functions.edit:15<|tool_call_argument_begin|>{"path": "..."}

Because Python regex's . does not match \n by default, both stream_tool_call_portion_regex and stream_tool_call_name_regex silently failed to match, causing the tool call to be entirely dropped during streaming.

Root cause

# Before (broken on leading \n)
self.stream_tool_call_portion_regex = re.compile(
    r"(?P<tool_call_id>.+:\d+)\s*<\|tool_call_argument_begin\|>\s*(?P<function_arguments>.*)"
)
self.stream_tool_call_name_regex = re.compile(r"(?P<tool_call_id>.+:\d+)\s*")

Fix

# After
self.stream_tool_call_portion_regex = re.compile(
    r"\s*(?P<tool_call_id>.+:\d+)\s*"
    r"<\|tool_call_argument_begin\|>\s*(?P<function_arguments>.*)",
    re.DOTALL,
)
self.stream_tool_call_name_regex = re.compile(
    r"\s*(?P<tool_call_id>.+:\d+)\s*", re.DOTALL
)

Two changes per regex:

  1. Leading \s* — consumes any leading whitespace/newlines before the function name.
  2. re.DOTALL — makes . match \n so the tool_call_id capture group spans newlines.

Why this is not a duplicate

Checked open PRs: #37384, #37445, #32504, #24847, #26918, #36891.

  • PR #37384 adds re.DOTALL only to stream_tool_call_portion_regex (for multi-line arguments), but does not add the leading \s* that handles a newline before the tool_call_id, and does not fix stream_tool_call_name_regex at all. The \s* prefix is the critical fix for this issue.
  • No other open PR addresses stream_tool_call_name_regex.

Test plan

Two new tests added to tests/tool_parsers/test_kimi_k2_tool_parser.py:

  • test_stream_tool_call_portion_regex_handles_leading_newline — unit test: both regexes must match with/without a leading \n, and correctly extract tool_call_id and function_arguments.
  • test_streaming_tool_call_with_newline_after_begin_token — end-to-end streaming simulation of the exact failure scenario from the issue; asserts at least one tool-call delta is emitted (i.e., the tool call is not silently dropped).

Existing tests: pre-commit run ruff-format and pre-commit run ruff-check pass clean.

AI assistance disclosure

This fix was investigated, reproduced, and implemented with AI assistance (GitHub Copilot / Claude Sonnet 4.6). Every changed line has been reviewed. The reviewer (submitter) understands the change end-to-end.

Changed files

  • tests/tool_parsers/test_kimi_k2_tool_parser.py (modified, +102/-0)
  • vllm/tool_parsers/kimi_k2_tool_parser.py (modified, +8/-4)

Code Example

<|tool_call_begin|>functions.edit:15<|tool_call_argument_begin|>{"path": "..."}

---

<|tool_call_begin|>
functions.edit:15<|tool_call_argument_begin|>{"path": "..."}

---

self.stream_tool_call_portion_regex = re.compile(
    r"(?P<tool_call_id>.+:\d+)\s*<\|tool_call_argument_begin\|>\s*(?P<function_arguments>.*)"
)

self.stream_tool_call_name_regex = re.compile(r"(?P<tool_call_id>.+:\d+)\s*")

---

self.stream_tool_call_portion_regex = _re.compile(
    r"\s*(?P<tool_call_id>.+:\d+)\s*" r"<\|tool_call_argument_begin\|>\s*(?P<function_arguments>.*)",
    _re.DOTALL,
)
self.stream_tool_call_name_regex = _re.compile(r"\s*(?P<tool_call_id>.+:\d+)\s*", _re.DOTALL)
RAW_BUFFERClick to expand / collapse

Your current environment

current main - irrespective of env

🐛 Describe the bug

Sometimes with Kimi K2.5, the parser drops the tool calls in tool_choice: auto (without constrained decoding).

When the model generates tool calls during streaming, the model sometimes emits a \n (newline) between the <|tool_call_begin|> token and the function name. For example, instead of producing:

<|tool_call_begin|>functions.edit:15<|tool_call_argument_begin|>{"path": "..."}

it produces:

<|tool_call_begin|>
functions.edit:15<|tool_call_argument_begin|>{"path": "..."}

(I am unable to provide the full trace here because it has a lot of personal crap in it, and I only observe this with long contexts and unable to repro as a canonical example).


https://github.com/vllm-project/vllm/blob/main/vllm/tool_parsers/kimi_k2_tool_parser.py#L70

self.stream_tool_call_portion_regex = re.compile(
    r"(?P<tool_call_id>.+:\d+)\s*<\|tool_call_argument_begin\|>\s*(?P<function_arguments>.*)"
)

self.stream_tool_call_name_regex = re.compile(r"(?P<tool_call_id>.+:\d+)\s*")

From my reading:

  1. stream_tool_call_portion_regex -- matches the full pattern: tool call ID + arguments
  2. stream_tool_call_name_regex -- matches just the tool call ID portion

Both use .+ to match the tool call ID (e.g. functions.edit:15). The problem is that in Python, . does not match \n by default. So when the model inserts that stray newline, .+ fails to match across it, and the parser silently fails to detect the tool call -- the streaming response just drops the tool call entirely.


Resolution: adding a \s* and _re.DOTALL worked for me.

With DOTALL, . matches any character including \n, so .+ in the capture group (?P<tool_call_id>.+:\d+) now correctly spans the newline and captures the full tool call ID.

self.stream_tool_call_portion_regex = _re.compile(
    r"\s*(?P<tool_call_id>.+:\d+)\s*" r"<\|tool_call_argument_begin\|>\s*(?P<function_arguments>.*)",
    _re.DOTALL,
)
self.stream_tool_call_name_regex = _re.compile(r"\s*(?P<tool_call_id>.+:\d+)\s*", _re.DOTALL)

Call for further investigation: I dont think this has side-effects from my testing on Kimi K2.5. But maybe, something leaks into Kimi K2 Thinking - but I cannot find enough time to get this testing in.

Before submitting a new issue...

  • Make sure you already searched for relevant issues, and asked the chatbot living at the bottom right corner of the documentation page, which can answer lots of frequently asked questions.

extent analysis

!!!!!!!!!!!!!!!!!!!!!!!

Fix Plan

To fix the issue of the parser dropping tool calls!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

  • Update the regular expressions to use the _re.DOTALL flag, which!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! allows the . character to match any character, including newlines.
  • Modify the `stream_tool!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING