[llmapi] NemotronV3ReasoningParser returns empty content when enable_thinking=False
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 14.7k
- Forks
- 2.8k
- Avg merge
- 2d 23h
- Merged PRs (30d)
- 489
Description
System Info
- TensorRT-LLM version: 1.3.0rc9
- Model:
nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4(revision4f0cf9d) - Reasoning parser:
--reasoning_parser nano-v3 - Tool parser:
--tool_parser qwen3_coder - Backend: pytorch
- GPU: NVIDIA RTX PRO 6000 Blackwell (96 GB, SM120)
- Inference cmd:
trtllm-serve serve <model> --backend pytorch --reasoning_parser nano-v3 --tool_parser qwen3_coder --enable_chunked_prefill --trust_remote_code ...
Description
NemotronV3ReasoningParser (registered as nano-v3 in tensorrt_llm/llmapi/reasoning_parser.py) returns empty content when a request is sent with chat_template_kwargs: {"enable_thinking": false} AND the model leaks tokens into the reasoning stream OR fails to emit a closing </think> tag.
The parser inherits DeepSeekR1Parser's default behavior of routing tokens to reasoning_content until a </think> is seen. In thinking-disabled mode the model shouldn't be producing reasoning at all — but in practice it occasionally does (model behavior, not parser behavior). The current parser then returns:
content: empty stringreasoning_content: the actual answer
OpenAI-compatible chat clients render only content, so the user sees a blank response.
Why this is a bug, not a feature
NemotronV3ReasoningParser already has a force_nonempty_content mechanism that does exactly the right thing — but it requires the caller to opt in via chat_template_kwargs.force_nonempty_content=True. That's a fine power-user toggle, but it doesn't match the documented intent of enable_thinking=False.
For comparison, the HuggingFace model card for nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4 ships a vLLM reasoning parser (super_v3_reasoning_parser.py, revision 4f0cf9d) that triggers the same swap when EITHER force_nonempty_content=True OR enable_thinking=False. So the TRT-LLM parser is out of sync with the model's documented inference behavior.
Reproduction (occurs ~5-15% of the time in our environment)
# Spin up trtllm-serve with Nemotron-3-Super-120B-NVFP4 + --reasoning_parser nano-v3
for i in $(seq 1 20); do
curl -ksf https://localhost:8443/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"nemotron-super-120b-nvfp4",
"messages":[{"role":"user","content":"What is 17 * 23?"}],
"max_tokens":80,
"chat_template_kwargs":{"enable_thinking":false}}' \
| jq -r '.choices[0].message.content' &
done
wait
# Count empty responses
Without the fix, 1-3 of 20 responses come back with empty content while reasoning_content has the answer. With the proposed fix, 20/20 return content.
Proposed fix
Extend the swap-gate in NemotronV3ReasoningParser so it ALSO fires when chat_template_kwargs.enable_thinking is False. Same semantics as force_nonempty_content=True, just gated on an additional condition.
Four touch points, all inside the same class in tensorrt_llm/llmapi/reasoning_parser.py:
__init__: readenable_thinkingflag, store asself._enable_thinking_is_false_maybe_swap_content: extend gate to(self._force_nonempty_content or self._enable_thinking_is_false)finish: same gate extension on the missing-closing-tag branchparse_delta: same gate extension on the accumulator
Behavior when thinking is ENABLED — unchanged.
Behavior when force_nonempty_content=True — unchanged.
The new branch only fires when enable_thinking=False AND the parser would otherwise return empty content.
47-line unified diff (no incidental edits — strictly within NemotronV3ReasoningParser) ready to submit. Happy to open a PR with unit tests as soon as this issue is approved.
Worth considering during review
- Should the fix also extend to
KimiK2ReasoningParser(also inheritsDeepSeekR1Parser)? Kimi K2 doesn't expose anenable_thinkingchat-template flag today, so unclear if it has the equivalent problem — happy to investigate as a follow-up. - Alternative API: instead of adding a new instance var, the constructor could just default
self._force_nonempty_content = Truewhenenable_thinking is False. Less code, same effect, butforce_nonempty_contentthen becomes overloaded with two meanings. Open to either direction.
Environment evidence
Patched our local install of tensorrt_llm/llmapi/reasoning_parser.py with the proposed fix on 2026-05-21. Smoke verified: PING/PONG and a 17×23 prompt both return non-empty content with enable_thinking=False. Behavior with enable_thinking=True unchanged (clean <think>...</think> framing, correct math answer). No regression in force_nonempty_content=True path.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in tensorrt_llm/llmapi/reasoning_parser.py at NemotronV3ReasoningParser and inspect the existing force_nonempty_content handling in init, _maybe_swap_content, finish, and parse_delta. Reproduce with the provided trtllm-serve curl loop using enable_thinking=false, then verify that leaked reasoning produces non-empty content while enabled thinking and force_nonempty_content behavior remain unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python, pytorch
- Domain
- ai, api
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100