Skip to content

feat(reasoning): add unified kimi_k2 reasoning parser - #1992

Open
ighutake-debug wants to merge 2 commits into
smg-project:mainfrom
ighutake-debug:feat/reasoning-kimi-k2
Open

feat(reasoning): add unified kimi_k2 reasoning parser#1992
ighutake-debug wants to merge 2 commits into
smg-project:mainfrom
ighutake-debug:feat/reasoning-kimi-k2

Conversation

@ighutake-debug

@ighutake-debug ighutake-debug commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Description

Problem

#1873: SMG ships three Kimi reasoning parsers (kimi, kimi_k25, kimi_thinking) with a static per-SKU always_in_reasoning flag, while the rest of the ecosystem (vLLM, SGLang, Moonshot's deploy guidance) uses one kimi_k2 parser for the whole K2 family. K2.5+ chat templates prefill <think> at the generation prompt (thinking on by default), so model output begins inside reasoning — and kimi_k25 (always_in_reasoning: false) waits for an opening <think> that never comes, mis-splitting reasoning vs content (the nightly τ²-bench 0/100 regression).

This is PR 1 of the sequence proposed in #1873 (comment): the new parser itself. Factory pattern-map changes, deprecated aliases, and the tool-parser rename land in follow-ups; the chat_template_kwargs.thinking toggle is the open design question there.

Refs: #1873

Solution

New KimiK2Parser in crates/reasoning_parser, matching current vLLM main (vllm/parser/kimi_k2.py) semantics:

  • Prefill-robust: starts in reasoning; a leading <think> is consumed when present (vLLM's self-correcting start-token behavior, both directions covered)
  • Dual end markers: reasoning ends on </think> or <|tool_calls_section_begin|> — Kimi can go straight from reasoning into a tool section without closing the think block. The tool-section marker is forwarded as content so SMG's downstream tool parser can parse it
  • Streaming-safe: holds back trailing suffixes that are proper prefixes of either end marker, so markers split across chunks never leak into reasoning text (a gap the shared BaseReasoningParser has)

Registered as kimi_k2 in the reasoning factory — selectable via --reasoning-parser kimi_k2. No behavior change to existing parsers.

Changes

  • crates/reasoning_parser/src/parsers/kimi_k2.rs — new parser + 8 tests
  • crates/reasoning_parser/src/parsers/mod.rs — module export
  • crates/reasoning_parser/src/factory.rs — register kimi_k2

Test Plan

  • Golden conformance test (kimi_k2_golden_k26_output_split): frozen K2.6-style output (starts mid-reasoning, </think>, then a full tool section) — asserts the exact reasoning/content split. This is the regression class that previously only surfaced as a benchmark score
  • kimi_k2_ends_reasoning_at_tool_section_without_think_end — reasoning runs straight into <|tool_calls_section_begin|>; marker forwarded verbatim
  • kimi_k2_consumes_leading_think_start_when_present + streaming variant — non-prefilled templates work too
  • kimi_k2_streaming_chunked_matches_non_streaming — golden output fed in chunks that split both end markers; streamed split identical to one-shot
  • kimi_k2_truncated_reasoning_is_all_reasoning, kimi_k2_reset_restores_initial_state, kimi_k2_model_type
  • TDD: 7/8 tests failed against the stub for the right reason (the 8th caught a bug in my own test data — chunks didn't reconstruct the golden string)

Gate output (macOS, rustc 1.97.1 stable):

  • cargo test -p reasoning-parser --lib102 passed (94 existing + 8 new)
  • cargo test -p smg --lib1323 passed, 0 failed
  • cargo +nightly fmt --all — silent success
  • cargo clippy -p reasoning-parser --all-targets -- -D warnings — zero warnings
Checklist
  • cargo +nightly fmt passes
  • cargo clippy --all-targets --all-features -- -D warnings passes (changed crate clean)
  • (Optional) Documentation updated — parser docs live in code; user-facing naming lands with the factory PR
  • (Optional) Please join us on Slack #sig-smg to discuss, review, and merge PRs

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added support for parsing reasoning output from Kimi K2 models.
    • Works for both streaming and non-streaming responses.
    • Correctly detects reasoning boundaries, handles tool-call sections, consumes leading reasoning tags, and supports truncated reasoning.
  • Tests

    • Added unit tests covering one-shot and streaming equivalence, edge cases around termination markers, and parser reset behavior.
  • Chores

    • Registered the Kimi K2 parser as an available reasoning parser.

One prefill-robust parser for the whole Kimi K2 family, matching
vLLM/SGLang's kimi_k2 semantics: starts in reasoning (a leading
<think> is consumed when present), and reasoning ends on </think> or
<|tool_calls_section_begin|> -- Kimi can go straight from reasoning
into a tool section without closing the think block. The tool-section
marker is forwarded as content so the downstream tool parser can parse
it.

Replaces the per-SKU guessing of kimi_k25/kimi_thinking (static
always_in_reasoning flag) that mis-split K2.5/K2.6 output when
thinking was on -- the regression behind the nightly tau2-bench 0/100
in smg-project#1873. Streaming holds back trailing partial end markers so split
tokens never leak into reasoning text.

Registered as kimi_k2 (selectable via --reasoning-parser kimi_k2).
Factory pattern-map changes, deprecated aliases, and the tool-parser
rename land in follow-up PRs.

Refs: smg-project#1873
Signed-off-by: ishan <ishanvgf@gmail.com>
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@github-actions github-actions Bot added the reasoning-parser Reasoning parser changes label Jul 28, 2026
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 812fb15a-2487-425c-b7a6-61487fb895cd

📥 Commits

Reviewing files that changed from the base of the PR and between d03bbda and 71f1caa.

📒 Files selected for processing (1)
  • crates/reasoning_parser/src/parsers/kimi_k2.rs

📝 Walkthrough

Walkthrough

Adds a unified KimiK2Parser with one-shot and streaming reasoning parsing, public exports, marker and lifecycle tests, and ParserFactory registration under kimi_k2.

Changes

Kimi K2 parser

Layer / File(s) Summary
Parser contract and boundaries
crates/reasoning_parser/src/parsers/kimi_k2.rs, crates/reasoning_parser/src/parsers/mod.rs
Defines Kimi K2 markers, parser state, boundary helpers, and public exports.
Parsing lifecycle and validation
crates/reasoning_parser/src/parsers/kimi_k2.rs
Adds one-shot and incremental parsing, buffer handling, lifecycle methods, and tests for markers, truncation, resets, and chunked streams.
Factory registration
crates/reasoning_parser/src/factory.rs
Registers KimiK2Parser under the kimi_k2 factory name.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ParserFactory
  participant KimiK2Parser
  participant ParserResult
  ParserFactory->>KimiK2Parser: construct kimi_k2 parser
  KimiK2Parser->>ParserResult: split reasoning and normal_text
Loading

Possibly related issues

  • lightseekorg/smg#1873 — Covers the unified kimi_k2 parser, prefilled <think> handling, tool-section termination, and factory registration.

Suggested reviewers: catherinesue

Poem

I’m a bunny parsing thoughts in a row,
Catching <think> wherever they flow.
Split markers with care,
Keep tool calls in air,
Kimi’s reasoning now hops to and fro!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 65.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a unified Kimi K2 reasoning parser.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d03bbda99a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +154 to +156
let normal = match kind {
EndKind::ThinkEnd => self.buffer[idx + THINK_END.len()..].to_string(),
EndKind::ToolSection => self.buffer[idx..].to_string(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve split tool markers after the think terminator

When a streaming chunk ends with something like </think><|tool_calls_se, this branch immediately emits the incomplete tool-section prefix as normal text and marks reasoning as ended, so the continuation arrives separately. In the inspected gRPC streaming pipeline, that first fragment is passed directly to KimiK2Parser::parse_incremental, whose no-marker path drains it as user-visible text; consequently the marker never reassembles and the tool call is not parsed. Hold a trailing prefix of TOOL_SECTION_START across the transition just as the reasoning path does.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 71f1caa — verified the claim against KimiK2Parser::parse_incremental first (its no-marker path mem::takes the buffer into normal_text, so a split marker is indeed lost). The think-end transition and the post-reasoning flush now hold back trailing partial tool-section prefixes, same hold-back as inside reasoning; new test kimi_k2_streaming_preserves_tool_marker_split_at_transition covers the exact chunk split you described. Worth noting for context: in production the section marker is a single special token so detokenization never actually splits it — this is defensive conformance for synthetic chunk boundaries. Crate suite 103 passed, clippy clean.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/reasoning_parser/src/factory.rs (1)

165-190: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Wire a K2-family model-ID pattern to kimi_k2.

kimi_k2 is registered and replaces the per-SKU parsers, but build_default_patterns still maps kimi-k2-thinkingkimi_thinking and kimi-k2.5kimi_k25, and no pattern maps to kimi_k2. Automatic model selection therefore falls through to passthrough; add/update the K2-family patterns to route these models to the unified parser now.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/reasoning_parser/src/factory.rs` around lines 165 - 190, Update
build_default_patterns so K2-family model IDs, including kimi-k2-thinking and
kimi-k2.5, map to the registered kimi_k2 parser instead of kimi_thinking or
kimi_k25. Ensure automatic model selection routes these variants to KimiK2Parser
rather than passthrough.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@crates/reasoning_parser/src/factory.rs`:
- Around line 165-190: Update build_default_patterns so K2-family model IDs,
including kimi-k2-thinking and kimi-k2.5, map to the registered kimi_k2 parser
instead of kimi_thinking or kimi_k25. Ensure automatic model selection routes
these variants to KimiK2Parser rather than passthrough.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1af90c45-8901-4519-be41-aa7db8929df8

📥 Commits

Reviewing files that changed from the base of the PR and between c5de762 and d03bbda.

📒 Files selected for processing (3)
  • crates/reasoning_parser/src/factory.rs
  • crates/reasoning_parser/src/parsers/kimi_k2.rs
  • crates/reasoning_parser/src/parsers/mod.rs

Review on lightseekorg#1992: when a streaming chunk ends right after </think> with
a partial <|tool_calls_section_begin|>, the fragment was emitted as
normal text. Downstream tool parsers drain marker-less deltas as
user-visible text, so the marker never reassembled and the tool call
was lost. Hold back trailing partial tool-section prefixes at the
think-end transition and in the post-reasoning flush, matching the
hold-back already used inside reasoning.

Refs: lightseekorg#1873
Signed-off-by: ishan <ishanvgf@gmail.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 71f1caa3fc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +179 to +181
let hold = Self::trailing_prefix_of(&self.buffer, &[THINK_END, TOOL_SECTION_START]);
let end = self.buffer.len() - hold;
let reasoning: String = self.buffer.drain(..end).collect();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Flush a held delimiter prefix at end of stream

When a streamed response ends with a proper prefix of either delimiter—for example, valid reasoning whose final character is <, or output truncated at </thi—this code retains the suffix in self.buffer. The inspected gRPC streaming paths only invoke parse_reasoning_streaming_incremental for decoded chunks and provide no end-of-stream parser flush, so the retained text is silently lost and streaming disagrees with the one-shot truncated-reasoning behavior. Add an EOF/finalization mechanism that emits the held prefix while continuing to hold it between live chunks.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid edge, and it is pre-existing ecosystem-wide rather than introduced by this parser: BaseReasoningParser drops a stream-ending partial identically (base.rs is_partial_token holds </th forever), so every base-derived parser has the same leak — the trait simply has no EOF signal. A proper fix is a trait-level finalization hook plus a call from the streaming paths, which would touch all parsers + model_gateway streaming — well beyond this PR. Filed as #1998 with a proposed flush() design; keeping this PR scoped per one-concern-per-PR.

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

Labels

reasoning-parser Reasoning parser changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants