Skip to content

[Fix] Make DeepSeek-V4 reasoning and tool-call streaming parsing chunk-invariant - #34458

Merged
hnyls2002 merged 10 commits into
mainfrom
lsyin/dsv4-parser-fix
Aug 12, 2026
Merged

[Fix] Make DeepSeek-V4 reasoning and tool-call streaming parsing chunk-invariant#34458
hnyls2002 merged 10 commits into
mainfrom
lsyin/dsv4-parser-fix

Conversation

@hnyls2002

@hnyls2002 hnyls2002 commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Background

  • Speculative decoding and stream_interval > 1 deliver several tokens per step, splitting multi-character markers across chunk boundaries
  • _parse_streaming_increment_impl cleared _buffer after every emission and destroyed the fragment, so the end token was never recognised and the rest of the turn leaked into reasoning_content
  • On main, <think>abc</think>normal at chunk size 3 already yields reasoning='abc</think>normal', normal=''
  • This affects every detector sharing the base implementation, not only DSV4

Changes

Reasoning parser

  • Hold back the longest trailing slice that is a proper prefix of think_end_token / tool_start_token / think_start_token, and flush it in finish() when the stream ends first
  • Move _ends_with_partial_token to BaseReasoningFormatDetector, replacing the copy in Apertus2509Detector
  • DeepSeekV4Detector passes tool_start_token, so a DSML block that follows reasoning without a </think> reaches the tool-call detector instead of staying in reasoning_content (supersedes fix(parsers): tools are leaking in the reasoning content (deepseek) #32539)
  • Write the stripped buffer back after removing think_start_token, so stream_reasoning=False no longer carries the token into reasoning_content

Tool-call detector

Known divergences

  • Streaming cannot see whether a </think> still follows, so two inputs split differently from detect_and_parse: a model quoting the DSML marker inside its reasoning, and (pre-existing on main) text emitted before <think> when reasoning is not forced
  • Both are pinned by *_is_chunk_dependent tests so a future fix has to update them rather than change behaviour silently

Test plan

  • test_reasoning_parser.py: TestStreamingChunkSizeInvariance feeds each case at chunk sizes [1, 2, 3, 5, 7, 11, 23, 1000] and compares against detect_and_parse (supersedes [Tests] Add regression tests for reasoning parser edge cases #34157)
  • test_deepseekv4_detector.py: preamble across delta splits, MalformedJSON fallback, error-path recovery, multi-section parsing
  • Two existing assertions are intentionally flipped, both because they pinned behaviour this PR fixes:
    • test_finish_drops_partial_end_tag_when_streaming_reasoning becomes ..._flushes_.... Its premise was that anything left in _buffer had already been emitted chunk by chunk; with holdback that text has never been emitted, and dropping it loses content whose only fault is resembling the start of a token
    • TestGlm45Detector.test_streaming_no_stream_reasoning no longer expects the raw <think> tag inside reasoning_content. Its comment described the leak it was pinning ("stripped from the local current_text variable but NOT from self._buffer"); stream_reasoning=False now matches detect_and_parse
  • Supersedes fix(parser): DSV4 reasoning + tool call streaming parser bugs #34178, which reported the buffer-clearing bug this PR fixes

CI States

Latest PR Test (Base): 🚫 Run #31546554111
Latest PR Test (Extra): ❌ Run #31546553893

Co-Authored-By: hao-cyber <89575785+hao-cyber@users.noreply.github.com>
Co-Authored-By: Enrico Falco <enrico9034@gmail.com>
Co-Authored-By: Svyatoslav <85786374+slivanovich@users.noreply.github.com>
Co-Authored-By: Andreas Hassellof <andreas@ombori.com>
Co-Authored-By: Leoyzen <leoyzen@gmail.com>
@hnyls2002

hnyls2002 commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

/tag-and-rerun-ci

hnyls2002 and others added 6 commits August 11, 2026 15:29
Co-Authored-By: Chenglun Hu <chenglunhu@gmail.com>
Co-Authored-By: robellliu-dev <robell.liu@huawei.com>
Co-Authored-By: Gavin.Zhu <gavin.z@gmicloud.ai>
Co-Authored-By: Xinyuan Tong <xinyuantong.cs@gmail.com>
Co-Authored-By: tancheng33 <garrytancheng@gmail.com>
Co-Authored-By: dineshx29 <dinesh.b.offl@gmail.com>
Co-Authored-By: Kangyan Zhou <zky314343421@gmail.com>
Co-Authored-By: hao-cyber <89575785+hao-cyber@users.noreply.github.com>
Co-Authored-By: Enrico Falco <enrico9034@gmail.com>
Co-Authored-By: Svyatoslav <85786374+slivanovich@users.noreply.github.com>
Co-Authored-By: Andreas Hassellof <andreas@ombori.com>
Co-Authored-By: Leoyzen <leoyzen@gmail.com>
@hnyls2002

hnyls2002 commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

/rerun-test test_reasoning_parser.py test_deepseekv4_detector.py test_kimik3_reasoning_parser.py test_reasoning_content_without_parser.py test_reasoner_grammar_backend.py test_function_call_parser.py test_serving_chat.py test_reasoning.py test_openai_function_calling.py test_anthropic_tool_use.py test_e2e_constrained_reasoning.py test_constrained_decoding_spec_reasoning.py
(2 tries)

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Results for /rerun-test test_reasoning_parser.py test_deepseekv4_detector.py test_kimik3_reasoning_parser.py test_reasoning_content_without_parser.py test_reasoner_grammar_backend.py test_function_call_parser.py test_serving_chat.py:

🚀 ubuntu-latest (7 tests): ❌ View workflow run

cd test/ && python3 registered/unit/parser/test_reasoning_parser.py
cd test/ && python3 registered/unit/function_call/test_deepseekv4_detector.py
cd test/ && python3 registered/unit/parser/test_kimik3_reasoning_parser.py
cd test/ && python3 registered/unit/parser/test_reasoning_content_without_parser.py
cd test/ && python3 registered/unit/constrained/test_reasoner_grammar_backend.py
cd test/ && python3 registered/unit/function_call/test_function_call_parser.py
cd test/ && python3 registered/unit/entrypoints/openai/test_serving_chat.py

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Results for /rerun-test test_reasoning_parser.py test_deepseekv4_detector.py test_kimik3_reasoning_parser.py test_reasoning_content_without_parser.py test_reasoner_grammar_backend.py test_function_call_parser.py test_serving_chat.py:

🚀 ubuntu-latest (7 tests): ✅ View workflow run

cd test/ && python3 registered/unit/parser/test_reasoning_parser.py
cd test/ && python3 registered/unit/function_call/test_deepseekv4_detector.py
cd test/ && python3 registered/unit/parser/test_kimik3_reasoning_parser.py
cd test/ && python3 registered/unit/parser/test_reasoning_content_without_parser.py
cd test/ && python3 registered/unit/constrained/test_reasoner_grammar_backend.py
cd test/ && python3 registered/unit/function_call/test_function_call_parser.py
cd test/ && python3 registered/unit/entrypoints/openai/test_serving_chat.py

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Results for /rerun-test test_reasoning_parser.py test_deepseekv4_detector.py test_kimik3_reasoning_parser.py test_reasoning_content_without_parser.py test_reasoner_grammar_backend.py test_function_call_parser.py test_serving_chat.py test_reasoning.py test_openai_function_calling.py test_anthropic_tool_use.py test_e2e_constrained_reasoning.py test_constrained_decoding_spec_reasoning.py:

🚀 ubuntu-latest (7 tests): ✅ View workflow run

cd test/ && python3 registered/unit/parser/test_reasoning_parser.py
cd test/ && python3 registered/unit/function_call/test_deepseekv4_detector.py
cd test/ && python3 registered/unit/parser/test_kimik3_reasoning_parser.py
cd test/ && python3 registered/unit/parser/test_reasoning_content_without_parser.py
cd test/ && python3 registered/unit/constrained/test_reasoner_grammar_backend.py
cd test/ && python3 registered/unit/function_call/test_function_call_parser.py
cd test/ && python3 registered/unit/entrypoints/openai/test_serving_chat.py

🚀 1-gpu-h100 (3 tests): ✅ View workflow run

cd test/ && python3 registered/reasoning/test_reasoning.py
cd test/ && python3 registered/openai_server/function_call/test_openai_function_calling.py
cd test/ && python3 registered/openai_server/function_call/test_anthropic_tool_use.py

🚀 1-gpu-5090 (1 test): ✅ View workflow run

cd test/ && python3 registered/unit/constrained/test_e2e_constrained_reasoning.py

🚀 2-gpu-h100 (1 test): ✅ View workflow run

cd test/ && python3 registered/spec/test_constrained_decoding_spec_reasoning.py

Leoyzen added a commit to Leoyzen/sglang that referenced this pull request Aug 12, 2026
…otentially_dsml trap

Three bugs caused agent to stop when it should be calling tools:

1. Exception handler cleared self._buffer (""), permanently losing tool
   call text. Fix: retain buffer, reset transient tool state instead.

2. str.rstrip(token) treated argument as character set, not substring.
   rstrip("parameter") truncated streaming values ending with chars in
   {p,a,r,m,e,t} (e.g. "find /tmp" → "find /"). Fix: use removesuffix.

3. Broad potentially_dsml check (any "|DSML|" in buffer) trapped all
   subsequent text in buffer when a DSML sub-tag appeared without an
   invoke. No finish() method existed to flush at stream end. Fix:
   narrow potentially_dsml to tool-call tags only; add finish() method;
   wire up parse_stream_end() in FunctionCallParser and serving_chat.

4. Reasoning parser used broad tool_start_token="<|DSML|" which
   matched any DSML sub-tag in reasoning, causing false positives.
   Fix: narrow to "<|DSML|tool_calls>".

Related: sgl-project#34458 (supersedes sgl-project#34178, which reported the buffer-clearing bug)
Tests: 8 new tests + 1 updated test, all 26 V32+V4 tests pass.
Leoyzen added a commit to Leoyzen/sglang that referenced this pull request Aug 12, 2026
…p fix, finish() flush, tool_start_token narrowing

Four bugs caused tool call loss, value truncation, text trapping, and
DSML tag leakage in DeepSeek-V4 streaming parsing. All four are present
in the current main (post sgl-project#34458) and are fixed here:

Bug 1 - Exception handler cleared self._buffer, permanently losing tool
call text. Fix: retain buffer, reset transient tool state instead.

Bug 2 - str.rstrip(token) treated the argument as a character set, not a
substring. rstrip("parameter") truncated streaming values ending with
chars in {p,a,r,m,e,t}. Fix: use removesuffix.

Bug 3 - Broad potentially_dsml check trapped all subsequent text when a
DSML sub-tag appeared without an invoke. Fix: narrow to tool-call tags
only; add finish() override to DeepSeekV32Detector.

Bug 4 - Broad tool_start_token matched any DSML sub-tag in reasoning.
Fix: narrow to tool_calls; add holdback at think_end and non-reasoning
boundaries using _ends_with_partial_token.

Tests: 12 new regression tests + 2 existing sgl-project#34458 tests updated.
All 39 tests pass.

Co-authored-by: Leoyzen <leoyzen@gmail.com>
Leoyzen added a commit to Leoyzen/sglang that referenced this pull request Aug 12, 2026
…p fix, finish() flush, tool_start_token narrowing

Four bugs caused tool call loss, value truncation, text trapping, and
DSML tag leakage in DeepSeek-V4 streaming parsing. All four are present
in the current main (post sgl-project#34458) and are fixed here:

Bug 1 - Exception handler cleared self._buffer, permanently losing tool
call text. Fix: retain buffer, reset transient tool state instead.

Bug 2 - str.rstrip(token) treated the argument as a character set, not a
substring. rstrip("parameter") truncated streaming values ending with
chars in {p,a,r,m,e,t}. Fix: use removesuffix.

Bug 3 - Broad potentially_dsml check trapped all subsequent text when a
DSML sub-tag appeared without an invoke. Fix: narrow to tool-call tags
only; add finish() override to DeepSeekV32Detector.

Bug 4 - Broad tool_start_token matched any DSML sub-tag in reasoning.
Fix: narrow to tool_calls; add holdback at think_end and non-reasoning
boundaries using _ends_with_partial_token.

Tests: 12 new regression tests + 2 existing sgl-project#34458 tests updated.
All 39 tests pass.

Co-authored-by: Leoyzen <leoyzen@gmail.com>
Leoyzen added a commit to Leoyzen/sglang that referenced this pull request Aug 14, 2026
…rdening

Cherry-pick and squash of three PRs:
- sgl-project#34262 infra: BaseFormatDetector.finish(), FunctionCallParser.parse_stream_end(),
  flush param in serving_chat._process_tool_call_stream
- sgl-project#34458: chunk-invariance — partial token holdback, _ends_with_partial_token,
  tool_start_token for DeepSeekV4Detector, MalformedJSON catch, preamble preservation
- sgl-project#34600: hardening — buffer retention on error, rstrip→removesuffix, narrow
  potentially_dsml to tool-call tags, finish() override on DeepSeekV32Detector,
  narrow tool_start_token to <|DSML|tool_calls, holdback at think_end boundary

Co-authored-by: hnyls2002 <hnyls2002@gmail.com>
Co-authored-by: Leoyzen <leoyzen@gmail.com>
Leoyzen added a commit to Leoyzen/sglang that referenced this pull request Aug 14, 2026
…rdening

Cherry-pick and squash of three PRs:
- sgl-project#34262 infra: BaseFormatDetector.finish(), FunctionCallParser.parse_stream_end(),
  flush param in serving_chat._process_tool_call_stream
- sgl-project#34458: chunk-invariance — partial token holdback, _ends_with_partial_token,
  tool_start_token for DeepSeekV4Detector, MalformedJSON catch, preamble preservation
- sgl-project#34600: hardening — buffer retention on error, rstrip→removesuffix, narrow
  potentially_dsml to tool-call tags, finish() override on DeepSeekV32Detector,
  narrow tool_start_token to <|DSML|tool_calls, holdback at think_end boundary

Co-authored-by: hnyls2002 <hnyls2002@gmail.com>
Co-authored-by: Leoyzen <leoyzen@gmail.com>
Leoyzen added a commit to Leoyzen/sglang that referenced this pull request Aug 14, 2026
…rdening

Cherry-pick and squash of three PRs:
- sgl-project#34262 infra: BaseFormatDetector.finish(), FunctionCallParser.parse_stream_end(),
  flush param in serving_chat._process_tool_call_stream
- sgl-project#34458: chunk-invariance — partial token holdback, _ends_with_partial_token,
  tool_start_token for DeepSeekV4Detector, MalformedJSON catch, preamble preservation
- sgl-project#34600: hardening — buffer retention on error, rstrip→removesuffix, narrow
  potentially_dsml to tool-call tags, finish() override on DeepSeekV32Detector,
  narrow tool_start_token to <|DSML|tool_calls, holdback at think_end boundary

Co-authored-by: hnyls2002 <hnyls2002@gmail.com>
Co-authored-by: Leoyzen <leoyzen@gmail.com>
saturn-acc pushed a commit to saturn-acc/sglang that referenced this pull request Aug 16, 2026
…k-invariant (sgl-project#34458)

Co-authored-by: hao-cyber <89575785+hao-cyber@users.noreply.github.com>
Co-authored-by: Enrico Falco <enrico9034@gmail.com>
Co-authored-by: Svyatoslav <85786374+slivanovich@users.noreply.github.com>
Co-authored-by: Andreas Hassellof <andreas@ombori.com>
Co-authored-by: Leoyzen <leoyzen@gmail.com>
Co-authored-by: Chenglun Hu <chenglunhu@gmail.com>
Co-authored-by: robellliu-dev <robell.liu@huawei.com>
Co-authored-by: Gavin.Zhu <gavin.z@gmicloud.ai>
Co-authored-by: Xinyuan Tong <xinyuantong.cs@gmail.com>
Co-authored-by: tancheng33 <garrytancheng@gmail.com>
Co-authored-by: dineshx29 <dinesh.b.offl@gmail.com>
Co-authored-by: Kangyan Zhou <zky314343421@gmail.com>
dinesh20073 added a commit to dinesh20073/sglang that referenced this pull request Aug 17, 2026
…grapheme streaming

Adds regression coverage for malformed nested reasoning tags and Unicode
grapheme clusters split across streaming chunks.

Chunk-invariance and streaming/full-input consistency tests are omitted because
they are already covered by sgl-project#34458.
legekka pushed a commit to legekka/sglang that referenced this pull request Aug 21, 2026
…rsing onto v0.5.16

Merged upstream fix (2026-08-12) for the streaming tool-call parser
dropping text that precedes the DSML tag when it arrives in the same
detokenizer increment as the tag (batched detok under speculative
decoding). Previously the invoke loop returned normal_text="" once the
buffer looked DSML-ish, silently discarding the preamble; non-streaming
parsing kept it, so stream and non-stream disagreed.

- deepseekv32_detector.py: recover preamble for the first tool call
  (back to the bot_token, same trailing-newline trim as detect_and_parse);
  multi-section non-streaming parse via findall; MalformedJSON tolerance;
  exception path re-emits verbatim instead of swallowing the turn.
- Known upstream limitation (tracked by open sgl-project#34518/sgl-project#34600): prose between
  or after multiple tool-call sections stays buffered.
- reasoning_parser.py hunks of sgl-project#34458 NOT ported: 0.5.16 maps deepseek-v4
  to _DeepSeekV3Detector (no dsv4 subclass); different base structure.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant