Field note · vLLM
Muse-Glimmer returns empty reasoning_content while the parser demonstrably extracts it
Every completion reports reasoning_content: "" and reasoning_tokens: 0, even when a hundred tokens of reasoning were generated. The parser returns the reasoning when called directly. The defect is somewhere between the two, and I have not found it.
- Hardware
- NVIDIA DGX Spark · GB10 (SM121) · aarch64 · 128 GB unified
- Software
- vLLM 0.27.2rc1.dev384+g1baf372bf (stock main) · RedHatAI/Muse-Glimmer-30B-NVFP4
Symptom
Serving RedHatAI/Muse-Glimmer-30B-NVFP4 via ~/vllm-server/muse/dflash2-start-local.sh — DFlash2 speculative decoding (z-lab/Muse-Glimmer-30B-DFlash2, num_speculative_tokens=15), --reasoning-parser muse_glimmer --tool-call-parser muse_glimmer --enable-auto-tool-choice — every chat completion comes back with reasoning_content: "" and completion_tokens_details.reasoning_tokens: 0, even on turns where the overwhelming majority of generated tokens were reasoning.
$ curl -s localhost:8000/v1/chat/completions -d '{
"model":"Muse-Glimmer-30B-NVFP4-DFlash2",
"messages":[{"role":"user","content":"Say the single word: hello"}],
"max_tokens":800,"temperature":0}'
finish : stop
completion : 105 tokens reasoning_tokens: 0
content len : 5 'hello'
reasoning len : 0 ''
105 completion tokens for a 5-character answer. Around a hundred tokens of reasoning were generated and silently discarded. content itself is correct throughout — this is a lost-field bug, not a correctness bug in what the model actually says.
The output on the wire is well-formed
Bypassing the parser entirely — /v1/completions directly, skip_special_tokens: false, on the exact prompt /v1/chat/completions/render produces — shows the ATEM channel framing MuseGlimmerReasoningParser is built to read, intact:
' to=self<|message|>Say the single word: hello\n\nWe need to say the single word:
hello\n\n...We should comply.<|eom|><|start|>assistant to=user<|message|>hello'
'to=self' : 1
'to=user' : 1
'<|message|>' : 2
'<|eom|>' : 1
Nothing malformed. One reasoning channel (to=self), one content channel (to=user), each opened and closed correctly.
The parser reads it correctly — three ways
Called directly against that exact string, inside the running container, the raw regexes read the reasoning out cleanly:
from vllm.reasoning import muse_glimmer_reasoning_parser as M
raw = ' to=self<|message|>Say the single word: hello\n\nWe should comply.<|eom|>' \
'<|start|>assistant to=user<|message|>hello'
M._REASONING_RE.findall(raw) # ['Say the single word: hello\n\nWe should comply.']
M._CONTENT_RE.search(raw).group(1) # 'hello'
M.MuseGlimmerReasoningParser._classify_bodies(raw)
# ('Say the single word...', 'hello')
So does the fully composed parser the server actually builds at request time:
from vllm.parser.parser_manager import ParserManager
cls = ParserManager.get_parser(tool_parser_name="muse_glimmer",
reasoning_parser_name="muse_glimmer",
enable_auto_tools=True,
model_name="RedHatAI/Muse-Glimmer-30B-NVFP4")
# -> _Parser(DelegatingParser), reasoning_parser_cls=MuseGlimmerReasoningParser
p = cls(tokenizer)
p.extract_reasoning(raw, request)
# -> ('Say the single word: hello\n\nWe should comply.', 'hello')
Three independent call sites — the bare regexes, the parser's own classification helper, and the composed DelegatingParser the server actually instantiates — all return the reasoning text correctly. The parser returns the reasoning. The API returns an empty string. Whatever is wrong, it isn't the parser.
What's ruled out
| candidate | how it was excluded |
|---|---|
| the regexes | extract correctly on the real captured output (above) |
_classify_bodies vs extract_reasoning divergence | both return reasoning; streaming and non-streaming are equally empty |
adjust_request not applied | it is implemented and sets request.skip_special_tokens = False |
| markers stripped before the parser | disproved: content is a clean 'hello', which requires _CONTENT_RE (to=user<|message|>…) to have matched. If markers were absent, extract_reasoning's final else branch would have returned the entire raw text as content. |
the reasoning-phase machine (is_reasoning_end only flips on a tool channel, so reasoning_ended stays False and get_streaming_fallback_content returns content only) | plausible, but disproved — a turn that does open a tool channel returns empty reasoning too |
| parser composition | _Parser(DelegatingParser) with both sub-parsers correctly bound |
| tool parsing interfering | tool calls work end-to-end: get_weather{"city": "Paris"} |
skip_special_tokens request override | identical result with true and false |
Empty in all of: streaming, non-streaming, a plain answer, a tool-call turn, and both values of skip_special_tokens.
What's left
The defect is somewhere in vLLM's response assembly, between a parser that demonstrably returns the reasoning and a response that demonstrably lacks it. I have not located it.
DelegatingParser.extract_reasoning (vllm/parser/abstract_parser.py:389) is a straight pass-through, so whatever drops the field happens downstream of it, in code this investigation didn't reach.
Why this matters more than a missing field
content is correct, so pipeline output looks fine at a glance. What's actually lost:
- Observability into how much of a token budget went to reasoning versus the answer.
- Any caller that gates behavior on
reasoning_tokensis reading a hard-coded0, always.
The second one is the sharp edge. A short-budget call can spend its entire max_tokens allowance reasoning and return empty content, under HTTP 200, with reasoning_tokens pinned at 0 — nothing in the response tells a caller that happened. This host's own register-model.sh documents exactly that failure, reproduced by hand on 2026-08-18. It recurred incidentally during this investigation too: a max_tokens=120 request came back with 120 completion tokens, empty content, and empty reasoning. No error, no signal, no partial output. Just nothing, successfully.
Reproduction (minimal)
- Serve
RedHatAI/Muse-Glimmer-30B-NVFP4with--reasoning-parser muse_glimmer. POST /v1/chat/completions, any prompt,max_tokens >= 600.- Observe
reasoning_content == ""whileusage.completion_tokensfar exceedslen(content). - Confirm the parser is not at fault by calling
ParserManager.get_parser(...)(tokenizer).extract_reasoning(raw, request)directly on the raw generation — it returns the reasoning.
This is not the only defect in this parser. A second one means the reasoning-budget cap that should limit how much a call can spend thinking is silently off entirely — independent of this bug, but compounding it.