Field note · vLLM

A reasoning parser with no start/end strings silently disables thinking-budget enforcement

Muse-Glimmer delimits reasoning by channel markers rather than a token pair, so vLLM cannot resolve reasoning token ids, so reasoning_config.enabled stays false, so the budget cap never engages — on a model whose chat template reasons by default.

Hardware
NVIDIA DGX Spark · GB10 (SM121) · aarch64
Software
vLLM 0.27.2rc1.dev384+g1baf372bf (stock main) · RedHatAI/Muse-Glimmer-30B-NVFP4
WARNING [vllm.py:1775] Auto-initialization of reasoning token IDs failed. Please check whether your reasoning parser has implemented the `reasoning_start_str` and `reasoning_end_str`.

Symptom

Same setup as the empty-reasoning_content investigation: DGX Spark GB10/SM121, vLLM 0.27.2rc1.dev384+g1baf372bf (stock main), RedHatAI/Muse-Glimmer-30B-NVFP4, --reasoning-parser muse_glimmer. The warning above is logged twice at every boot — once from the API server process, once again from the engine core. Not a one-off: every cold start of this model with this parser reproduces it, twice.

Where it comes from

vllm/config/vllm.py:1771:

if self.reasoning_config is not None and self.model_config is not None:
    self.reasoning_config.initialize_token_ids(self.model_config)
    if not self.reasoning_config.enabled:
        logger.warning_once("Auto-initialization of reasoning token IDs failed. ...")

ReasoningConfig.initialize_token_ids (vllm/config/reasoning.py:69) asks the parser class for reasoning_start_str and reasoning_end_str, tokenizes them, and uses the resulting ids to bound where reasoning begins and ends in the token stream. MuseGlimmerReasoningParser implements neither attribute. No token ids get resolved, and reasoning_config.enabled stays False.

The parser isn't wrong to omit them

This is a reasonable design choice on its own terms, not an oversight. MuseGlimmer's reasoning isn't delimited by a single start/end token pair — it's channel-structured: to=<recipient><|message|> … <|eom|>. MuseGlimmerReasoningParser deliberately works on decoded text with regexes instead of the single-start/end-token base class other reasoning parsers use, and its own module docstring says so. The shape genuinely doesn't fit the two-string contract ReasoningConfig expects.

What's missing is a fallback. The config layer has no accommodation for a channel-structured parser, so the capability is silently dropped rather than adapted. The parser did the right thing for its own architecture; the config layer just has nowhere to put that.

Consequence

reasoning_config.enabled is read at vllm/v1/engine/input_processor.py:117 — the reasoning-budget enforcement path, the mechanism meant to cap how many tokens a request can spend thinking before being forced to stop. With the flag False, there are no token ids to bound against, so that cap cannot operate at all. Every request reasons for as long as the model wants to, with no ceiling vLLM enforces.

This is independent of the empty-reasoning_content defect — same parser, different bug. input_processor.py:117 gates budget enforcement, not response-field population; disabling one does not explain or cause the other. Verified, not assumed.

Why it matters specifically here

Muse-Glimmer's chat template reasons by default. render_reasoning() defaults reasoning_strength to 'high', and on the no-system-message branch it injects a "Reasoning strength: high." line into a synthesized system block — so even a bare request thinks, with no opt-out at the template level.

Put the three pieces together: a model that always reasons, served by a stack whose reasoning-budget enforcement is silently off, reporting reasoning_tokens: 0 regardless of how much thinking actually happened (that other Muse-Glimmer defect). None of the three explains the others, but together: a short-budget call can spend its entire allowance thinking and return empty content under HTTP 200, with no signal anywhere that it happened.

Fix directions

Two ways to close this, upstream, neither applied yet:

  1. Parser-side. Implement reasoning_start_str = "to=self<|message|>" and reasoning_end_str = "<|eom|>". Both are real strings that appear in the emitted stream. The catch: the parser's own docstring warns these markers "are not guaranteed to be single vocab tokens across every checkpoint's tokenizer" — which may be exactly why they were left unimplemented in the first place. Whoever picks this up needs to confirm they tokenize to single ids, or that the config path tolerates multi-token markers.
  2. Config-side. Let ReasoningConfig accept a parser-supplied predicate instead of requiring a token-id pair, so a channel-structured parser like this one isn't silently downgraded just because its reasoning isn't delimited the way the config layer expects.

Reproduction

  1. Serve any Muse-Glimmer checkpoint with --reasoning-parser muse_glimmer.
  2. Check the container logs — docker logs <container> | grep "Auto-initialization of reasoning token IDs" — present on every boot.
  3. Confirm the parser defines neither attribute — grep -n "reasoning_start_str\|reasoning_end_str" vllm/reasoning/muse_glimmer_reasoning_parser.py returns nothing.
Status
investigating
First seen
August 21, 2026