Skip to content

[#17572][fix] Emit the withheld buffer in DeepSeek streaming tool parsers - #17573

Open
Yigtwxx wants to merge 4 commits into
NVIDIA:mainfrom
Yigtwxx:fix/deepseek-tool-parser-buffer-drop
Open

[#17572][fix] Emit the withheld buffer in DeepSeek streaming tool parsers#17573
Yigtwxx wants to merge 4 commits into
NVIDIA:mainfrom
Yigtwxx:fix/deepseek-tool-parser-buffer-drop

Conversation

@Yigtwxx

@Yigtwxx Yigtwxx commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes #17572.

parse_streaming_increment in the DeepSeek tool parsers accumulates deltas into
self._buffer and withholds the buffer while it could still grow into a tool-call start
token. When the ambiguity cleared, the buffer was cleared but only new_text, the current
delta, was returned, so everything withheld by an earlier increment was dropped from the
streamed response. Both start tokens begin with <, so the trigger is ordinary assistant
text containing <:

from tensorrt_llm.serve.tool_parser.tool_parser_factory import ToolParserFactory

parser = ToolParserFactory.create_tool_parser("deepseek_v31")
"".join(parser.parse_streaming_increment(d, []).normal_text
        for d in ["Use ", "<", "div> for a block element."])
# before: 'Use div> for a block element.'
# after:  'Use <div> for a block element.'

detect_and_parse is unaffected because it sees the whole response and never buffers, so
the streamed and non-streamed content differ for the same generation.

The change is to emit the buffer instead of the delta. In DeepSeekV3Parser and
DeepSeekV31Parser the partial-token check is also moved onto the buffer via the existing
BaseToolParser._ends_with_partial_token helper; testing e_token.startswith(new_text)
is only correct when the buffer is empty, and it additionally means a start token split
across two deltas is emitted as normal text instead of being recognised.
DeepSeekV32Parser used substring heuristics instead: it withheld the buffer whenever a
DSML marker appeared anywhere in it, and whenever the right-stripped buffer ended with one
of <, <|, </, </|, and it treated a bare <|DSML|invoke anywhere in the buffer
as a tool call. None of those release the buffer once ordinary text diverges from a
delimiter, so "Use <|DSML|function" followed by "ality" is withheld forever and, with
no end-of-stream flush, lost. The same holds for "Use <|DSML|invoke" followed by
"ality".

They are replaced by the same _ends_with_partial_token check over the opening and
closing tokens, so a delimiter split across deltas is still withheld and stripped rather
than leaking into content, and the tool-call predicate now keys off
<|DSML|invoke name=" rather than the bare prefix. That is the longest fixed prefix of
an invoke header, since everything after the opening quote is the arbitrary function name,
and it is also in the partial-token list so a header split anywhere inside it stays
buffered. DeepSeekV4Parser inherits this path.

This is the pattern the rest of the directory already uses.
BaseToolParser.parse_streaming_increment, gemma4_parser.py, minimax_m3_parser.py,
poolside_v1_parser.py, qwen3_tool_parser.py and kimi_k3_tool_parser.py all check
_ends_with_partial_token against the buffer and emit the buffer. The four DeepSeek
parsers were the only ones that did not.

Scope is limited to tool-calling traffic, since apply_tool_parser only runs when a tool
parser is configured and the request carries tools. There is no API change and no change
to the non-streaming path.

Test Coverage

tests/unittest/llmapi/apps/test_tool_parsers.py, which is already registered as
cpu_only in tests/integration/test_lists/test-db/l0_cpu.yml:

  • test_deepseek_streaming_preserves_withheld_text asserts that concatenating the
    normal_text of every streamed increment reproduces the input. It runs over the four
    DeepSeek parsers and three delta shapes: a delta that is itself a prefix of a start
    token, a delta that ends on one after other text, and text that starts like a start
    token and then diverges from it. The second shape is what makes DeepSeekV32Parser and
    DeepSeekV4Parser discard a longer run of text at once; the third is the case that
    withheld the buffer permanently, and it diverges from both tokens those two parsers
    look for.

Eight of the twelve cases fail on main and all twelve pass with this change. The rest of
the file is unchanged and still passes, including the existing DeepSeek tool-call tests.

One thing found in review is deliberately left out: a delta that contains ordinary text
before a complete tool call drops that text. It is caused by the tool-parsing branch
rather than by the withhold guard, it affects all four parsers, and it needs its own test
shape, so it is filed separately as #17580.

The four DeepSeek test classes previously inherited only
test_parse_streaming_increment_normal_text and
test_parse_streaming_increment_partial_bot_token from BaseToolParserTestClass, and
neither streams text across more than one delta, which is why this was not caught.

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

  • The four DeepSeek streaming parsers now preserve buffered assistant text.
  • Partial-token checks use the accumulated buffer.
  • DeepSeek V3.2 and V4 delimiter handling reduces false tool-call detection.
  • No public API or configuration changes are present.
  • No test-list files were modified.
  • The implementation is consistent with the stated streaming-only scope.
  • Pipeline validation remains required.

QA Engineer Review

  • Added parameterized streaming regression coverage in tests/unittest/llmapi/apps/test_tool_parsers.py.
  • Coverage includes DeepSeek V3, V3.1, V3.2, and V4.
  • Tests cover partial and diverging tool-call prefixes across multiple deltas.
  • Tests verify preservation of concatenated normal_text and one-shot parsing.
  • No corresponding test-db/ or qa/ entries are reported.
  • Per-case runtime and CI coverage data are unavailable.

Verdict: needs follow-up

@Yigtwxx
Yigtwxx requested a review from a team as a code owner August 12, 2026 19:11
@Yigtwxx

Yigtwxx commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

@tongyuantongyu @asfiyab-nvidia when you get a chance, could one of you kick off a pipeline run? /bot run is ignored from my account.

The change is confined to the four DeepSeek tool parsers and the new cases are cpu_only, so the CPU pre-merge stage covers them. Six of the eight new cases fail on main and all eight pass here. If eight parametrizations is more than you want in the CPU stage I can cut it down to the two that fail on every parser.

@coderabbitai

coderabbitai Bot commented Aug 12, 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: abad0f10-134f-4167-be91-6ce8766fea3a

📥 Commits

Reviewing files that changed from the base of the PR and between a943dba and 35b42bb.

📒 Files selected for processing (3)
  • tensorrt_llm/serve/tool_parser/deepseekv31_parser.py
  • tensorrt_llm/serve/tool_parser/deepseekv32_parser.py
  • tensorrt_llm/serve/tool_parser/deepseekv3_parser.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • tensorrt_llm/serve/tool_parser/deepseekv31_parser.py
  • tensorrt_llm/serve/tool_parser/deepseekv3_parser.py
  • tensorrt_llm/serve/tool_parser/deepseekv32_parser.py

Walkthrough

DeepSeek V3, V3.1, and V3.2 streaming parsers now preserve accumulated text while checking partial tool-call markers. End markers are removed from emitted text. Parameterized tests cover all four DeepSeek parser variants.

Changes

DeepSeek streaming parsing

Layer / File(s) Summary
Buffered parser output
tensorrt_llm/serve/tool_parser/deepseekv3_parser.py, tensorrt_llm/serve/tool_parser/deepseekv31_parser.py, tensorrt_llm/serve/tool_parser/deepseekv32_parser.py
The parsers check partial markers against the accumulated buffer, emit the full buffered text, remove end markers, and clear the buffer.
Streaming regression coverage
tests/unittest/llmapi/apps/test_tool_parsers.py
Parameterized tests verify that partial or divergent tool-token prefixes remain in streamed output and match one-shot parsing across DeepSeek parser variants.

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

Mergeability Score: ⚪ Minimal · up to 35b42

This change preserves ordinary assistant text that was previously lost during DeepSeek streaming while retaining tool-call parsing behavior; no actionable merge-blocking risk remains after normal checks and review.

Suggested reviewers: asfiyab-nvidia

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the fix for withheld-buffer emission in DeepSeek streaming tool parsers.
Description check ✅ Passed The description explains the cause, solution, scope, affected parsers, regression tests, and checklist completion.
Linked Issues check ✅ Passed The changes address issue #17572 by preserving buffered text, correcting partial-token checks, and adding coverage for all four DeepSeek parsers.
Out of Scope Changes check ✅ Passed The parser updates and CPU-only regression tests are directly related to the linked issue and stated pull request 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/unittest/llmapi/apps/test_tool_parsers.py (1)

1798-1799: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the required function annotations.

Annotate sample_tools, parser_cls, and deltas with precise types. Add -> None. Do not use Any.

As per coding guidelines, “Annotate every function” and “avoid unnecessary Any.”

🤖 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/apps/test_tool_parsers.py` around lines 1798 - 1799,
Update test_deepseek_streaming_preserves_withheld_text by adding precise,
non-Any type annotations for sample_tools, parser_cls, and deltas, and annotate
its return type as None.

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.

Inline comments:
In `@tensorrt_llm/serve/tool_parser/deepseekv32_parser.py`:
- Around line 185-193: Update the diverged-prefix handling in the DeepSeek V3.2
parser so potentially_dsml remains true only when the buffered text ends with a
partial self.bot_token or <|DSML|invoke suffix, not merely when those markers
appear anywhere; preserve ordinary text such as split ["Use <|DSML|function",
"ality"] through the normal-text path. In
tests/unittest/llmapi/apps/test_tool_parsers.py lines 1791-1808, add this
split-input case and assert normal-text preservation for every parser class
listed there.

---

Nitpick comments:
In `@tests/unittest/llmapi/apps/test_tool_parsers.py`:
- Around line 1798-1799: Update test_deepseek_streaming_preserves_withheld_text
by adding precise, non-Any type annotations for sample_tools, parser_cls, and
deltas, and annotate its return type as None.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 48a01eb1-be42-47bb-a453-4d4287207a85

📥 Commits

Reviewing files that changed from the base of the PR and between 3612c80 and a4c8e08.

📒 Files selected for processing (4)
  • tensorrt_llm/serve/tool_parser/deepseekv31_parser.py
  • tensorrt_llm/serve/tool_parser/deepseekv32_parser.py
  • tensorrt_llm/serve/tool_parser/deepseekv3_parser.py
  • tests/unittest/llmapi/apps/test_tool_parsers.py

Comment thread tensorrt_llm/serve/tool_parser/deepseekv32_parser.py Outdated

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
tensorrt_llm/serve/tool_parser/deepseekv32_parser.py (1)

178-181: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Handle the intentional Unicode delimiters in Ruff configuration.

Ruff reports RUF003 for the comment at Line 178 and RUF001 for the string at Line 181. Do not replace with ASCII |; the fullwidth character is required by the DeepSeek DSML token.

If these rules are enabled in CI, add a narrow suppression or per-file ignore. Verify the configured Ruff rules before changing the code.

Ruff reported these warnings in the changed lines.

🤖 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 `@tensorrt_llm/serve/tool_parser/deepseekv32_parser.py` around lines 178 - 181,
Preserve the intentional fullwidth `|` delimiters in the comment and
`partial_tokens` definition. Inspect the repository’s Ruff configuration and add
the narrowest appropriate suppression or per-file ignore for RUF003 and RUF001
affecting `deepseekv32_parser.py`, without replacing the required Unicode token
characters.

Source: Linters/SAST tools

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

Inline comments:
In `@tensorrt_llm/serve/tool_parser/deepseekv32_parser.py`:
- Around line 190-193: Update the tool-call parsing flow in detect_and_parse()
to emit any ordinary text preceding self.bot_token before consuming the matched
tool segment, preserving that prefix when _buffer is reset after the invoke
match. Ensure the returned streaming normal_text includes the prefix alongside
the parsed tool call, and add a streaming test covering ordinary text and a
complete tool call in the same delta.
- Around line 175-190: Update the tool-call detection in the parser method
containing has_tool_call so it only recognizes a complete invoke header, not any
occurrence of "<|DSML|invoke" within current_text. Keep buffering exclusively
when the trailing text is a valid partial prefix of that header, allowing
ordinary text such as the split "invoke" plus "ality" case to be emitted; add
this scenario to the regression tests.

In `@tests/unittest/llmapi/apps/test_tool_parsers.py`:
- Around line 1796-1799: Update the DeepSeek DSML token in the parser test case
to silence RUF001 without changing its value: represent each fullwidth vertical
line as \uFF5C or add a targeted # noqa: RUF001 to the affected line.

---

Nitpick comments:
In `@tensorrt_llm/serve/tool_parser/deepseekv32_parser.py`:
- Around line 178-181: Preserve the intentional fullwidth `|` delimiters in the
comment and `partial_tokens` definition. Inspect the repository’s Ruff
configuration and add the narrowest appropriate suppression or per-file ignore
for RUF003 and RUF001 affecting `deepseekv32_parser.py`, without replacing the
required Unicode token characters.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 937a9700-c351-4d2a-b39a-9b0638f3f7bd

📥 Commits

Reviewing files that changed from the base of the PR and between a4c8e08 and 7adfc4a.

📒 Files selected for processing (2)
  • tensorrt_llm/serve/tool_parser/deepseekv32_parser.py
  • tests/unittest/llmapi/apps/test_tool_parsers.py

Comment thread tensorrt_llm/serve/tool_parser/deepseekv32_parser.py
Comment on lines +190 to +193
if not has_tool_call and not ends_with_partial_token:
# The guard above withholds the whole buffer, so the buffer is what has
# to be emitted once it clears; returning only the latest delta would
# drop everything withheld by an earlier increment.

@coderabbitai coderabbitai Bot Aug 12, 2026

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.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Preserve text before a complete tool call.

When a delta contains ordinary text before self.bot_token, this branch is skipped. The parse path later resets _buffer after the invoke match at Line 278, so the prefix is discarded.

For example, a delta beginning with "Normal text <|DSML|function_calls> ..." can return tool calls without returning "Normal text". detect_and_parse() returns that prefix at Lines 127-130.

Emit the pre-tool text before consuming the tool segment. Add a streaming test with ordinary text and a complete tool call in the same delta. Otherwise, concatenated streaming normal_text does not match non-streaming parsing.

🤖 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 `@tensorrt_llm/serve/tool_parser/deepseekv32_parser.py` around lines 190 - 193,
Update the tool-call parsing flow in detect_and_parse() to emit any ordinary
text preceding self.bot_token before consuming the matched tool segment,
preserving that prefix when _buffer is reset after the invoke match. Ensure the
returned streaming normal_text includes the prefix alongside the parsed tool
call, and add a streaming test covering ordinary text and a complete tool call
in the same delta.

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.

This one is real, and I reproduced it, but I would rather not fix it here.

delta: 'Normal text <|DSML|function_calls> ... </|DSML|function_calls>'
streaming   -> normal_text='',            calls=2
non-streaming -> normal_text='Normal text', calls=1

It is not specific to V3.2 either. DeepSeekV31Parser drops the prefix the same way on 'Normal text <|tool▁calls▁begin|>...', and the call-count difference above suggests the streaming emission path has its own problem separate from the dropped prefix.

That puts it in the tool-parsing branch rather than the withhold guard this PR is about, it touches all four parsers, and it needs its own tests for the emission shape. Fixing it here would make the change span two unrelated code paths. I will open a separate issue with the reproduction so it does not get lost, and link it from this thread.

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.

@Yigtwxx, agreed. The reproduction confirms the finding.

This pull request changes the partial-token buffer guard. The dropped prefix and the streaming call-emission shape are in a separate tool-parsing path. A fix needs cross-parser coverage and dedicated streaming tests.

Deferring this work to a follow-up issue is appropriate. Please link the issue here when available.


✏️ Learnings added
Learnt from: Yigtwxx
Repo: NVIDIA/TensorRT-LLM PR: 17573
File: tensorrt_llm/serve/tool_parser/deepseekv32_parser.py:195-198
Timestamp: 2026-08-12T19:38:23.109Z
Learning: In NVIDIA/TensorRT-LLM DeepSeek streaming tool parsers, ordinary text before a complete tool-call segment can be dropped by the tool-parsing path: `DeepSeekV32Parser` drops text before `<|DSML|function_calls>`, and `DeepSeekV31Parser` has the equivalent issue before `<|tool▁calls▁begin|>`. This is separate from suffix-based partial-token buffering and requires a cross-parser follow-up with tests for normal-text preservation and streaming tool-call emission shape.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: Yigtwxx
Repo: NVIDIA/TensorRT-LLM PR: 17573
File: tensorrt_llm/serve/tool_parser/deepseekv32_parser.py:0-0
Timestamp: 2026-08-12T19:26:02.216Z
Learning: In `tensorrt_llm/serve/tool_parser/deepseekv32_parser.py`, `DeepSeekV32Parser.parse_streaming_increment()` must use suffix-based partial-token detection on the accumulated buffer for `bot_token`, the invoke start token, `eot_token`, `invoke_end_token`, and `_eos_token`. This prevents loss of ordinary text after a diverged DSML prefix and prevents split closing delimiters from leaking into `normal_text`. `DeepSeekV4Parser` inherits this behavior.

You are interacting with an AI system.

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.

Filed as #17580 with the reproduction and the table for all four parsers.

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.

@Yigtwxx, thank you for filing #17580. The issue captures the deferred cross-parser tool-parsing work and its reproduction coverage.


🧠 Learnings used
Learnt from: Yigtwxx
Repo: NVIDIA/TensorRT-LLM PR: 17573
File: tensorrt_llm/serve/tool_parser/deepseekv32_parser.py:0-0
Timestamp: 2026-08-12T19:26:02.216Z
Learning: In `tensorrt_llm/serve/tool_parser/deepseekv32_parser.py`, `DeepSeekV32Parser.parse_streaming_increment()` must use suffix-based partial-token detection on the accumulated buffer for `bot_token`, the invoke start token, `eot_token`, `invoke_end_token`, and `_eos_token`. This prevents loss of ordinary text after a diverged DSML prefix and prevents split closing delimiters from leaking into `normal_text`. `DeepSeekV4Parser` inherits this behavior.

Learnt from: Yigtwxx
Repo: NVIDIA/TensorRT-LLM PR: 17573
File: tensorrt_llm/serve/tool_parser/deepseekv32_parser.py:195-198
Timestamp: 2026-08-12T19:38:23.109Z
Learning: In NVIDIA TensorRT-LLM DeepSeek streaming tool parsers, preserve ordinary text that appears before a complete tool-call marker such as <|DSML|function_calls> or <|tool▁calls▁begin|>. Do not drop this prefix while handling tool calls; keep this behavior distinct from suffix-based partial-token buffering, and add tests covering normal-text preservation and the emitted streaming tool-call shape.

You are interacting with an AI system.

Comment thread tests/unittest/llmapi/apps/test_tool_parsers.py Outdated
Comment thread tensorrt_llm/serve/tool_parser/deepseekv31_parser.py Outdated
Comment thread tensorrt_llm/serve/tool_parser/deepseekv32_parser.py Outdated
Comment thread tensorrt_llm/serve/tool_parser/deepseekv31_parser.py Outdated
Comment thread tensorrt_llm/serve/tool_parser/deepseekv3_parser.py Outdated
@tongyuantongyu

Copy link
Copy Markdown
Member

If eight parametrizations is more than you want in the CPU stage I can cut it down to the two that fail on every parser.

Would be better if you can attach run time of each case when you are talking about that. They seem lightweight so it's probably fine anyway.

@Yigtwxx

Yigtwxx commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Numbers for the parametrizations, measured locally (Python 3.12, CPU only; best
of 200 runs per case, each run = the full streaming sequence plus the
detect_and_parse assertion):

case V3 V3.1 V3.2 V4
["Use ", "<", "div> for a block element."] 11.4 us 11.5 us 18.5 us 18.0 us
["The condition is a <", " b, so it holds."] 8.7 us 8.7 us 14.5 us 13.6 us
["Use <|DSML|function", "ality and <|DSML|invoke", "ality"] 16.1 us 15.9 us 22.3 us 28.7 us

All twelve together are 0.19 ms of actual work; under pytest the whole
parametrized test reports 12 passed in 0.38s, which is collection and fixture
setup rather than the cases themselves. So I have kept all twelve — let me know
if you would still rather I trim it.

I have also pushed the review fixes: the redundant in guards are gone from all
three parsers and the comments now state invariants only.

@tongyuantongyu

Copy link
Copy Markdown
Member

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #65820 [ run ] triggered by Bot. Commit: 35b42bb Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #65820 [ run ] completed with state FAILURE. Commit: 35b42bb

Link to invocation

@tongyuantongyu

Copy link
Copy Markdown
Member

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #65862 [ run ] triggered by Bot. Commit: 35b42bb Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #65862 [ run ] completed with state FAILURE. Commit: 35b42bb
/LLM/main/L0_MergeRequest_PR pipeline #53555 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

…ol parsers

parse_streaming_increment accumulates deltas into self._buffer and withholds
the buffer while it could still grow into a tool-call start token. Once the
ambiguity cleared, the buffer was cleared but only the current delta was
returned, so everything withheld by an earlier increment was dropped from the
streamed response. Both start tokens begin with "<", so ordinary assistant
text containing "<" lost characters.

Emit the buffer instead of the delta. In the V3 and V3.1 parsers also test the
buffer with the _ends_with_partial_token helper rather than testing the delta
with startswith, which matches BaseToolParser.parse_streaming_increment and the
gemma4, minimax_m3, poolside_v1, qwen3 and kimi_k3 parsers. V3.2 already gates
on the buffer, so only the emitted text changes there, and DeepSeekV4Parser
inherits the fix from DeepSeekV32Parser.

Signed-off-by: Yiğit ERDOĞAN <yigiterdogan023@gmail.com>
… DSML tag

The V3.2 guard withheld the buffer whenever a DSML marker appeared anywhere in
it, so once ordinary text diverged from a delimiter the buffer was never
released. "Use <|DSML|function" followed by "ality" is the smallest case: the
text is neither a tool call nor emittable, and with no end-of-stream flush it is
lost. The same applies to DeepSeekV4Parser, which inherits this path.

Replace the marker-presence and rstrip-endswith heuristics with the
_ends_with_partial_token helper, matching the V3 and V3.1 parsers. The check
covers the closing tokens as well, so a delimiter split across deltas is still
withheld and stripped rather than leaking into content.

Also annotate the new test and add the diverged-prefix case, which fails on the
V3.2 and V4 parsers without this change.

Signed-off-by: Yiğit ERDOĞAN <yigiterdogan023@gmail.com>
… as a tool call

The V3.2 streaming path treated the bare "<|DSML|invoke" prefix appearing
anywhere in the buffer as a tool call, so ordinary text that merely starts like
the token, such as "<|DSML|invoke" followed by "ality", was routed into the
tool-call branch. No invoke matched there, the buffer was kept, and the text was
never emitted. DeepSeekV4Parser inherits this path.

Key off the header up to its opening quote instead. Everything after that point
is the arbitrary function name, so this is the longest fixed prefix ordinary
text cannot reproduce by accident, and using the same string in the
partial-token list keeps a header that arrives split across deltas buffered.

The regression case now diverges from both tokens the parser looks for.

Signed-off-by: Yiğit ERDOĞAN <yigiterdogan023@gmail.com>
… comments

`str.replace` already scans the string, so guarding it with `in` only
repeats the scan. The comments that motivated the change described the
previous behaviour rather than the invariant the code now holds, which
is noise for anyone reading only the current version.

Signed-off-by: Yiğit ERDOĞAN <yigiterdogan023@gmail.com>
@Yigtwxx
Yigtwxx force-pushed the fix/deepseek-tool-parser-buffer-drop branch from 35b42bb to 8b382f6 Compare August 13, 2026 16:35
@Yigtwxx

Yigtwxx commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main (e9dd5a79) — no code change; the four-commit patch is
byte-identical to 35b42bb. The old base (3d3d7c9, Aug 12 17:54Z) predated
eight waive/unwaive commits, including 26 post-merge main failures waived in
#17593, #17616 and #17617, so both red runs ran without them.

Neither run published an L0 test tally, which usually means the pipeline ended
before the test stage rather than in it. The change is confined to three DeepSeek
parsers plus test_tool_parsers.py (cpu_only, listed in l0_cpu.yml:77); all
12 new parametrizations pass locally on the rebased tree.

Could you trigger one more /bot run? And if it is not infra, could you paste
which stage failed — the report links are internal-only from outside.

@tongyuantongyu

Copy link
Copy Markdown
Member

@Yigtwxx please don't attempt rebase / merge main unless otherwise asked to. Our CI can skip tests already passed during retry, but you doing rebase will prevent that from working.

@tongyuantongyu

Copy link
Copy Markdown
Member

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #66129 [ run ] triggered by Bot. Commit: 8b382f6 Link to invocation

@Yigtwxx

Yigtwxx commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

Understood — apologies for the extra CI churn, and thanks for re-triggering. I won't rebase or merge main on open PRs unless asked.

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #66129 [ run ] completed with state FAILURE. Commit: 8b382f6
/LLM/main/L0_MergeRequest_PR pipeline #53804 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

@Yigtwxx

Yigtwxx commented Aug 15, 2026

Copy link
Copy Markdown
Contributor Author

@tongyuantongyu that run is red again on 8b382f6, and I still cannot see why —
the Jenkins, CI-report and failure-analysis links are all internal-only from
outside NVIDIA. Every GitHub-side check on the commit is green (Pre-commit, DCO,
PR Base Freshness, Title Format, Checklist Resolution, LLM API Compatibility
Label).

Could you paste the failing stage name, or the first few lines of its log? That
is the one thing blocking me from acting on it. Three runs have now ended in
FAILURE without publishing an L0 test tally, which is what I would expect if
the pipeline stopped before the test stage rather than in it — but I am guessing,
and I would rather not.

For what it is worth, on the current tree the change is confined to three
DeepSeek parsers plus test_tool_parsers.py, which is cpu_only and registered
in l0_cpu.yml:77. Locally all 12 new parametrizations pass, and so does the
rest of the file apart from a fixed set of cases that need a full
import tensorrt_llm my machine cannot provide — those fail identically with and
without this change. Nothing on my side reproduces a failure.

If it turns out to be infra rather than this PR, just say so and I will sit tight
rather than pushing anything.

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]: DeepSeek streaming tool parsers drop text they withheld while waiting on a tool-call token

3 participants