Skip to content

[#17156][fix] Flush buffered text in DeepSeekR1Parser.finish() - #17157

Open
Yigtwxx wants to merge 2 commits into
NVIDIA:mainfrom
Yigtwxx:fix/reasoning-parser-finish-flush
Open

[#17156][fix] Flush buffered text in DeepSeekR1Parser.finish()#17157
Yigtwxx wants to merge 2 commits into
NVIDIA:mainfrom
Yigtwxx:fix/reasoning-parser-finish-flush

Conversation

@Yigtwxx

@Yigtwxx Yigtwxx commented Aug 1, 2026

Copy link
Copy Markdown

Description

Fixes #17156.

DeepSeekR1Parser.parse_delta withholds a trailing fragment that could still grow into
a <think> / </think> tag, keeping it in self._buffer until the next delta arrives.
BaseReasoningParser.finish() exists so a parser can flush that state when the stream
ends, and the serving layer calls it (serve/postprocess_handlers.py:177,
serve/responses_utils.py:963). DeepSeekR1Parser never overrode finish(), so a
stream that ended while a fragment was buffered silently dropped those characters from
content or reasoning_content — for example a response ending in a literal <, or
one truncated by max_tokens partway through </thin.

This adds the missing finish() override: the withheld text is emitted and attributed
to the block it was withheld in (reasoning content inside a reasoning block, visible
content otherwise). A buffer holding exactly a complete tag is a delimiter rather than
model output, so it is still discarded, which keeps the existing behavior for a stray
closing tag arriving as the final delta.

This is a conformance fix rather than a behavior change: two sibling parsers in the same
file already implement exactly this flush — NemotronV3ReasoningParser.finish() and
Gemma4ReasoningParser.finish() — and Gemma4ReasoningParser.finish() has the same
shape as the implementation added here. parse_delta is untouched.

Scope: every parser key backed by DeepSeekR1Parser (deepseek-r1, qwen3, qwen3_5,
laguna, minimax_m2, minimax_m2_append_think) plus the subclasses
MiniMaxM3ReasoningParser (minimax_m3) and DeepSeekV4ReasoningParser
(deepseek_v4), whose finish() delegates to the base parser and was a no-op until now.
NemotronV3ReasoningParser overrides finish() in full and is unaffected.

Test Coverage

tests/unittest/llmapi/test_reasoning_parser.py (CPU-only, no model weights):

  • test_deepseek_r1_reasoning_parser_finish_flushes_reasoning — a withheld partial
    closing tag is emitted as reasoning content at end of stream. Parametrized over the
    keys that stream from inside the reasoning block: deepseek-r1, qwen3_5,
    minimax_m2, minimax_m2_append_think. qwen3_5 and minimax_m2_append_think were
    not referenced by any test in this file before.
  • test_deepseek_r1_reasoning_parser_finish_flushes_content — a withheld partial opening
    tag is emitted as visible content, for qwen3 and laguna.
  • test_deepseek_r1_reasoning_parser_finish_drops_complete_tag — a buffer holding only a
    delimiter does not leak into the output.
  • test_deepseek_r1_reasoning_parser_finish_is_idempotent — a second finish() returns
    an empty result.
  • test_deepseek_r1_reasoning_parser_stream_matches_non_stream — streaming one character
    at a time and then finishing produces the same content / reasoning_content split as
    parse() on the whole text. This is the contract the missing flush violated.

Results on this branch: 173 passed. Against main with only the test changes applied,
19 of the new cases fail, so they do guard the fix. All 136 pre-existing cases in the
file pass unchanged.

PR Checklist

Please review the following before submitting your PR:

  • PR description clearly explains what and why. If using CodeRabbit's summary, please make sure it makes sense.

  • PR Follows TRT-LLM CODING GUIDELINES to the best of your knowledge.

  • Test cases are provided for new code paths (see test instructions)

  • If PR introduces API changes, an appropriate PR label is added - either api-compatible or api-breaking. For api-breaking, include BREAKING in the PR title.

  • Any new dependencies have been scanned for license and vulnerabilities

  • CODEOWNERS updated if ownership changes

  • Documentation updated as needed

  • Update tava architecture diagram if there is a significant design change in PR.

  • The reviewers assigned automatically/manually are appropriate for the PR.

  • Please check this after reviewing the above items as appropriate for this PR.

GitHub Bot Help

To see a list of available CI bot commands, please comment /bot help.

Dev Engineer Review

  • DeepSeekR1Parser.finish() flushes buffered text at end-of-stream.
  • Complete <think> and </think> tags remain discarded.
  • Partial tag fragments emit to the active reasoning_content or content field.
  • The implementation applies to parser aliases and delegating subclasses.
  • The method is idempotent and preserves streaming/non-streaming equivalence.
  • No configuration or test-list files changed.

QA Engineer Review

  • Added tests for:
    • Buffered reasoning-content flushing.
    • Buffered visible-content flushing.
    • Parser aliases and delegating subclasses.
    • Complete delimiter suppression.
    • Idempotent finish().
    • Streaming and non-streaming output equivalence.
  • No test-list coverage entry was added or modified.
  • Verdict: needs follow-up because test-db/ and qa/ coverage data is unavailable.

parse_delta withholds a trailing fragment that could still grow into a
<think>/</think> tag. DeepSeekR1Parser never overrode finish(), so when a
stream ended while such a fragment was buffered the characters were
silently dropped from content or reasoning_content.

Override finish() to emit the withheld text, attributing it to the block
it was withheld in. A buffer holding exactly a complete tag is a
delimiter rather than model output and is still discarded.

NemotronV3ReasoningParser and Gemma4ReasoningParser already implement
this flush; this brings the shared base parser in line with them.

Signed-off-by: Yigtwxx <yigiterdogan023@gmail.com>
@Yigtwxx
Yigtwxx requested a review from a team as a code owner August 1, 2026 10:05
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 2f2a77b7-c013-44ca-831a-a7b6368be7b0

📥 Commits

Reviewing files that changed from the base of the PR and between cfbc7ce and f61b492.

📒 Files selected for processing (1)
  • tests/unittest/llmapi/test_reasoning_parser.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/unittest/llmapi/test_reasoning_parser.py

Walkthrough

DeepSeekR1Parser.finish() now flushes buffered streaming fragments based on parsing state and discards complete delimiters. Tests cover aliases, idempotency, delimiter handling, and parity with non-streaming parsing.

Changes

Reasoning parser end-of-stream handling

Layer / File(s) Summary
Buffered-text finalization
tensorrt_llm/llmapi/reasoning_parser.py
DeepSeekR1Parser.finish() emits incomplete buffered text as reasoning or visible content and discards complete <think> and </think> delimiters.
Finalization behavior tests
tests/unittest/llmapi/test_reasoning_parser.py
Tests cover parser aliases, reasoning and content flushing, delimiter suppression, repeated finish() calls, and equivalence between streaming and non-streaming parsing.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: junyixu-nv, allisonlim-nv

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies issue #17156, the fix type, and the buffered-text flush in DeepSeekR1Parser.finish().
Description check ✅ Passed The description explains the defect, solution, affected parser keys, tests, and checklist status.
Linked Issues check ✅ Passed The implementation and tests satisfy issue #17156 by flushing partial buffered fragments while discarding complete delimiter tags.
Out of Scope Changes check ✅ Passed The changes are limited to the required parser fix and focused tests for the linked issue objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
tests/unittest/llmapi/test_reasoning_parser.py (2)

78-79: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add complete type annotations to the new test functions.

Add -> None to each test function. Use list[str] for delta_texts.

As per coding guidelines, "Annotate every function" and "use precise ... types."

Also applies to: 95-96, 108-109, 119-119, 135-135

🤖 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 `@tests/unittest/llmapi/test_reasoning_parser.py` around lines 78 - 79, Update
the new test functions, including
test_deepseek_r1_reasoning_parser_finish_flushes_reasoning and the additional
functions at the referenced locations, with complete annotations: use list[str]
for delta_texts and add -> None to each function signature.

Source: Coding guidelines


65-67: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the test-only constant private.

R1_AT_START_KEYS only supports parametrization in this module. Rename it to _R1_AT_START_KEYS.

As per coding guidelines, "Prefix non-public names with _."

🤖 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 `@tests/unittest/llmapi/test_reasoning_parser.py` around lines 65 - 67, Rename
the test-only constant R1_AT_START_KEYS to _R1_AT_START_KEYS and update every
reference to it in the module, preserving its parametrization behavior.

Source: Coding guidelines

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

Nitpick comments:
In `@tests/unittest/llmapi/test_reasoning_parser.py`:
- Around line 78-79: Update the new test functions, including
test_deepseek_r1_reasoning_parser_finish_flushes_reasoning and the additional
functions at the referenced locations, with complete annotations: use list[str]
for delta_texts and add -> None to each function signature.
- Around line 65-67: Rename the test-only constant R1_AT_START_KEYS to
_R1_AT_START_KEYS and update every reference to it in the module, preserving its
parametrization behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 88d07722-0ea2-4ab1-9d81-098fc96cceae

📥 Commits

Reviewing files that changed from the base of the PR and between a9544e0 and cfbc7ce.

📒 Files selected for processing (2)
  • tensorrt_llm/llmapi/reasoning_parser.py
  • tests/unittest/llmapi/test_reasoning_parser.py

…t helpers

Follow-up on review feedback: the tests added for DeepSeekR1Parser.finish()
lacked return annotations and used a bare list type, and the parser-key
constant is module-internal. CODING_GUIDELINES requires every function to be
annotated and non-public names to be prefixed with an underscore.

Signed-off-by: Yigtwxx <yigiterdogan023@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Streaming reasoning parser drops text buffered at the end of the stream

2 participants