System Info
- CPU architecture: N/A (reproducible with a pure-Python snippet, no GPU required)
- GPU: N/A
- TensorRT-LLM branch:
main
- TensorRT-LLM commit:
f7f596b94e
- OS: N/A
The report concerns NemotronV3ReasoningParser in tensorrt_llm/llmapi/reasoning_parser.py
and affects streaming /v1/chat/completions responses for reasoning_parser: nano-v3.
Who can help?
No response
Information
Tasks
Reproduction
DeepSeekR1Parser.parse_delta holds back a delta that is exactly a <think> or
</think> tag (reasoning_end.startswith(delta_text) is true for equality) and waits for
the next delta to decide what it is. When the stream ends on that delta, finish() is
responsible for the withheld buffer. DeepSeekR1Parser.finish() discards a buffer that
holds exactly a tag (reasoning_parser.py:316-320, added in #17157).
NemotronV3ReasoningParser.finish() (reasoning_parser.py:627-655) reimplements the
method without that rule, so the tag is surfaced as model output:
from tensorrt_llm.llmapi.reasoning_parser import ReasoningParserFactory
for kwargs in (None, {"force_nonempty_content": True}):
parser = ReasoningParserFactory.create_reasoning_parser("nano-v3", kwargs)
for delta in ["a", "</think>"]:
parser.parse_delta(delta)
print(kwargs, parser.finish())
parser = ReasoningParserFactory.create_reasoning_parser("nano-v3", {"enable_thinking": False})
parser.parse_delta("<think>")
print(parser.finish())
Output on main:
None ReasoningParserResult(content='', reasoning_content='</think>')
{'force_nonempty_content': True} ReasoningParserResult(content='a</think>', reasoning_content='')
ReasoningParserResult(content='<think>', reasoning_content='')
</think> is a single token for this model, so any generation that closes its reasoning
and then stops (an empty answer, or a stop right after the tag) ends the stream on exactly
that delta and hits the first two cases on every such request.
Expected behavior
A stream that ends on a complete tag yields the same split as parse() on the joined
text: parse("a</think>") gives reasoning_content="a", content="" (or content="a"
with force_nonempty_content), and parse("<think>") with thinking disabled gives empty
content. The tag is a delimiter and is never part of reasoning_content or content.
actual behavior
The literal </think> is appended to reasoning_content (or, with
force_nonempty_content, to content), and a literal <think> is returned as content
when thinking is disabled. Streaming and non-streaming responses differ for the same
generation.
additional notes
DeepSeekR1Parser.finish() is the in-package precedent for the rule; the Nemotron
override predates it and reimplements the parent's flush without it.
- The existing
test_nano_v3_reasoning_parser_finish covers </think> first and in the
middle of the stream, but never as the last delta.
- I have a fix ready (two lines in
NemotronV3ReasoningParser.finish(), plus three
parametrizations in that test) and will open a PR referencing this issue.
System Info
mainf7f596b94eThe report concerns
NemotronV3ReasoningParserintensorrt_llm/llmapi/reasoning_parser.pyand affects streaming
/v1/chat/completionsresponses forreasoning_parser: nano-v3.Who can help?
No response
Information
Tasks
examplesfolder (such as GLUE/SQuAD, ...)Reproduction
DeepSeekR1Parser.parse_deltaholds back a delta that is exactly a<think>or</think>tag (reasoning_end.startswith(delta_text)is true for equality) and waits forthe next delta to decide what it is. When the stream ends on that delta,
finish()isresponsible for the withheld buffer.
DeepSeekR1Parser.finish()discards a buffer thatholds exactly a tag (
reasoning_parser.py:316-320, added in #17157).NemotronV3ReasoningParser.finish()(reasoning_parser.py:627-655) reimplements themethod without that rule, so the tag is surfaced as model output:
Output on
main:</think>is a single token for this model, so any generation that closes its reasoningand then stops (an empty answer, or a stop right after the tag) ends the stream on exactly
that delta and hits the first two cases on every such request.
Expected behavior
A stream that ends on a complete tag yields the same split as
parse()on the joinedtext:
parse("a</think>")givesreasoning_content="a",content=""(orcontent="a"with
force_nonempty_content), andparse("<think>")with thinking disabled gives emptycontent. The tag is a delimiter and is never part of
reasoning_contentorcontent.actual behavior
The literal
</think>is appended toreasoning_content(or, withforce_nonempty_content, tocontent), and a literal<think>is returned ascontentwhen thinking is disabled. Streaming and non-streaming responses differ for the same
generation.
additional notes
DeepSeekR1Parser.finish()is the in-package precedent for the rule; the Nemotronoverride predates it and reimplements the parent's flush without it.
test_nano_v3_reasoning_parser_finishcovers</think>first and in themiddle of the stream, but never as the last delta.
NemotronV3ReasoningParser.finish(), plus threeparametrizations in that test) and will open a PR referencing this issue.