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

candidatehow it was excluded
the regexesextract correctly on the real captured output (above)
_classify_bodies vs extract_reasoning divergenceboth return reasoning; streaming and non-streaming are equally empty
adjust_request not appliedit is implemented and sets request.skip_special_tokens = False
markers stripped before the parserdisproved: 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 interferingtool calls work end-to-end: get_weather{"city": "Paris"}
skip_special_tokens request overrideidentical 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:

  1. Observability into how much of a token budget went to reasoning versus the answer.
  2. Any caller that gates behavior on reasoning_tokens is reading a hard-coded 0, 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)

  1. Serve RedHatAI/Muse-Glimmer-30B-NVFP4 with --reasoning-parser muse_glimmer.
  2. POST /v1/chat/completions, any prompt, max_tokens >= 600.
  3. Observe reasoning_content == "" while usage.completion_tokens far exceeds len(content).
  4. 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.

Status
investigating
First seen
August 21, 2026