Repository navigation
fix(glimmer): a format with thinking off waits for the message header, in single pass and two-pass alike - #464
Conversation
…, in single pass and two-pass alike
A glimmer response never starts in content: the prompt ends at "<|start|>assistant" (the publisher's template), and
the model writes a recipient and <|message|> first, often after a to=self message it writes even with thinking off.
With thinking off and a format, both modes bound the format from the first token, so the header could not be
written: muse-glimmer:30b (Q4_K_M, llama.cpp, production's OLLAMA_FORMAT_TWO_PASS=1) returned `{...}<|eot|>`, and
upstream measured a lost first field in single pass.
- parsers: with thinking off, GlimmerParser.ThinkingClose returns " to=user<|message|>" and
"<|start|>assistant<|message|>", so a single-pass format waits for the header. Upstream ollama#18687
(open, a4380a5), taken as written with its test.
- server: two-pass sends the same closings when thinking is off (thinkingCloseForRequest). With thinking off it
has no pass one to defer and already ran the single pass's one call, but without the closings; upstream's
parser change alone leaves OLLAMA_FORMAT_TWO_PASS=1 bound from the first token. Every other parser returns no
closings with thinking off, and both runners read closings only alongside a format, so nothing else changes.
- parsers: a response that never writes a header (a format still bound from the first token, e.g. one that
reaches the runner without closings) no longer returns its closing <|eot|> or <|eom|> as content.
TestThinkFormatGlimmerThinkingOff covers chat and generate in both modes over five output shapes; without the
server change its ten two-pass cases fail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…duced, and glimmer against gemma4:31b think-off Phases 0-2 done (inventory, smoke on all eight tags, preflight ladders); phase 3 in progress. The findings and their draft PRs (#464-#468), and summarize_head_to_head.py's table of glimmer against gemma4:31b, thinking off, pasted as printed with its MIXED-build provenance. Several glimmer cells sit on the format + think-off path #464 fixes; the rglfix_ re-run is the comparison to read. Also corrects the record's unknown-think-level line (it falls back to the default; upstream ollama#18473) and the probe check that follows from it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ormat with thinking on
Pass two of OLLAMA_FORMAT_TWO_PASS re-renders pass one's thinking as a to=self message, and that prompt ends at
"<|start|>assistant" like every glimmer prompt. Pass two binds the format from its first token, so the model could
not write " to=user<|message|>" and wrote JSON where the recipient goes: muse-glimmer:30b-mxfp8 (MLX, release
0.40.2, OLLAMA_FORMAT_TWO_PASS=1, think on) echoed the prompt as {"user": "You are a ..."} in 6 of 14 vision-suite
cases, 3 of them looping. The same header bug as the thinking-off fix, on the thinking-on path.
formatContinuationHeader names the prefill per model for both handlers: harmony's existing final-channel header,
and glimmer's " to=user<|message|>". Every other model's re-rendered prompt already ends where the answer starts.
Single pass is unaffected: it sends glimmer's thinking-on closings, which include the header.
TestThinkFormatGlimmerTwoPassThinkingOn covers chat and generate; it fails without the change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
CUDA host review, at Tests. In golang:1.26,
Models this host serves: gemma4, qwen3.5, qwen3.6, qwen3.8 and nemotron-3-nano send no closings with thinking off, in either mode, and their content is unchanged. One correction to "No other model changes":
Not measured here: no CUDA real-model run, and glimmer is not served on this host yet. From |
…thinking off, in two-pass too Both parsers begin inside thinking whatever the request says, so a think:false + format request now waits for their closing in two-pass as single pass already did. The two-pass rows fail against main's routes.go (no closings sent). Raised in the CUDA host's review of #464. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks: the qwen3-vl-thinking and harmony change is now tested and described.
From |
|
Confirmed at
The description now names both parsers, which settles the point for the CUDA host. From |
…s 1-5, the fixes it produced, and the follow-ups Phases 0-5 ran on all eight tags. The record's outcome section lists the five fixes now in v0.40.2-dynres.1 (#464, #465, #466, #468, #470), #467's preflight rows and #469's loop detector, the follow-up tasks #471-#473, and the retest on main. Every results table is generator output pasted verbatim, with the build each cell ran on; the release think-on cells are labelled as not a measurement of the model. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A format request with thinking off on muse-glimmer now waits for the message header, in single pass and in the two-pass rollback alike. Before, both modes bound the grammar from the first token. On GGUF the reply came back as
{...}<|eot|>, and no client parses that as JSON.What was measured
Measured 2026-10-11 on Metal (
macbook-pro-m5-max-128GB/mlx-metal, #463), in production's environment (OLLAMA_FORMAT_TWO_PASS=1):30b(Q4_K_M)30b-q4_K_M-dflash30b-q8_030b-q8_0-dflash0.40.2-dynres-0-g5623192{…}<|eot|>{…}<|eot|>{…}<|eot|>{…}<|eot|>{ "city": "Paris", "population_millions": 2.1 }think: falseplus a JSON schema, on/api/chat.30b-nvfp4,30b-mxfp8,30b-mlx) the reply came back clean. The MLX runner doesn't print the end-of-turn token.multi_3imgcase: the same cause, at its worst. Measured on30b-nvfp4, thinking off,format: "json"(the vision suite's default), on the release build.Answer: it begins with the prompt's own words (
Analyze each image independently and then answer cross-image questions…) instead of{.Then: it degenerates into
, " 1, " :"} 1, "repeated untilnum_predict(2200,done_reason: length,json_valid: false). It did not converge at any context rung up to 131072.Mechanism: the format bound from the first token, so the model wrote its message header inside the JSON. The parser took everything up to
<|message|>as the header and returned the rest. This is the mechanism behind upstream's lost-first-field report, on a three-image task.This PR: the closings make the format wait for the header on MLX as well (
mlxrunner/client.go:233builds them into the grammar).Before/after, thinking off, all eight tags: the suite on the release build (
rgl_) against this branch's first commit 4cfad4a (rglfix_). Each tag ran 27 cases, atnum_ctx16384, under production's idle gate (2026-10-11/12).30b-nvfp430b-mlx30b-mxfp830b-mxfp8-dflash30b30b-q4_K_M-dflash30b-q8_030b-q8_0-dflash{" }and other whitespace-only objects, at 266-329 tokens;I see a light gray background…);multi_3img, the prompt's words followed by degeneration to the 2200 cap;bboxm_pin_noanc_named, IoU 0.0 → 0.839 here);-dflashtwin fail and recover on the same cases, so the drafter plays no part.30b-mxfp8,bboxm_pin_noanc_named, think off + format, release build,stream: true.<|message|>header, the parser holds everything in its header state untildone.muse-glimmer:30b): two of the 27 suite cases (bboxm_free_noanc_pos,bbox_contract_box2d_1img) came back as HTTP 500prediction aborted, token repeat limit reached, with no output.llm/llama_server.go:2483-2493aborts once the same trimmed token arrives 101 times in a row.30b-mxfp8):bboxm_pin_noanc_namedbegan cleanly ({"x1": 70, "y1": 147, …) and then repeated the same object's keys (duplicates are legal underformat: "json") untilnum_predict. The suite salvaged one box from it.Cause
A glimmer response never starts in content. The prompt ends at
<|start|>assistant, as the publisher's template does, and the model first writesto=user<|message|>. With thinking off it often writes ato=selfmessage first: about 117–140 hidden tokens on every tag measured.GlimmerParser.ThinkingClose()returned nothing with thinking off.So the format bound from the first token, the header was never written, and the parser's headerless fallback returned the raw text, end tag included.
Thinking on: pass two had the same header bug (second commit, 4b7c3b7)
Measured 2026-10-11 on
30b-mxfp8, think on,format: "json", release build,OLLAMA_FORMAT_TWO_PASS=1,num_ctx16384, with the loop detector on (#469).13 of the 27 suite answers echo the prompt as JSON:
{"user": "You are a localization service. Find every distinct coloured shape….loop detected:content):bboxm_pin_noanc_named,scene_single_anchoredanddocument_single.bboxm_free_anc_named,bboxm_free_noanc_named,multi_3img, and 7 of the 12bbox_contract*cases (_perobject,_anchored_1img,_box2d_1img,_positional_1img,_real_1img,_adv_real,_adv_norm1).GGUF has it too:
muse-glimmer:30b(Q4_K_M, llama.cpp) echoes in 13 of 27 answers. 26 of the 27 are still valid JSON, and 1 loops (document_single).muse-glimmer:30b-q8_0, the one tag that was clean with thinking off, echoes in 15 of 27, all 27 valid JSON, and 2 end asloop detected:token-run(multi_3img,multi_3img_anchored). Both runners fail the same way, because the bug is in the server's pass two.30b-nvfp4answered all 27 cleanly, and no think-off cell echoes, so the echo is not in every reply. JSON validity hides it: an echoed answer is usually valid JSON.The cause: pass two re-renders pass one's thinking as a
to=selfmessage. That prompt ends at<|start|>assistant, like every glimmer prompt, and pass two binds the format from its first token. The model cannot writeto=user<|message|>, so it writes JSON where the recipient goes. Harmony already gets a header prefilled here for the same reason; glimmer got none.Single pass is unaffected: it sends glimmer's thinking-on closings, which include the header.
The fix:
formatContinuationHeadernames the prefill per model, for chat and generate: harmony's existing final-channel header, and glimmer'sto=user<|message|>. Every other model's re-rendered prompt already ends where the answer starts.Test:
TestThinkFormatGlimmerTwoPassThinkingOncovers chat and generate, and fails without the change.Real-model check, think on: the suite on the release build (
rgl_) against this commit 4b7c3b7 (rglfix_). Each tag ran 27 cases atnum_ctx16384, with the loop detector on (vision-suite: a verbatim loop ends the cell as a verdict -- loop detected:<thinking|content|token-run> (ADR 0049) #469), under production's idle gate (2026-10-12).30b-nvfp430b-mxfp830b30b-q8_030b-nvfp4is the control. It never echoed, and it is unchanged.Against stock 0.40.2. Upstream's release ran the same suite on the same store, single pass only (its only mode):
30b-nvfp4/30b30b-nvfp4/30b30b-nvfp4/30b30bthis branch's median think-on length is 1219 tokens against stock's 1218: two-pass now answers as single pass does.A side effect in single pass on MLX (
OLLAMA_FORMAT_TWO_PASS=0, not production's mode). The smoke's schema check (format_schema), 2026-10-12:{ "city": …compact{+ 33 spaces, then valid JSON (341 tokens){+ 32 spaces, then valid JSON{ "city": …compact{then whitespace tonum_predict(16384 tokens, FAIL)Changes
model/parsers: upstream ollama/ollama#18687, open and unreviewed, taken as written with its test. With thinking off,ThinkingClose()returnsto=user<|message|>and<|start|>assistant<|message|>, so a single-pass format waits for the header.server: two-pass sends the same closings when thinking is off (thinkingCloseForRequest). This is what fixes production; parsers: end glimmer's free text at the message header with thinking off ollama/ollama#18687 alone leavesOLLAMA_FORMAT_TWO_PASS=1bound from the first token.qwen3-vl-thinking(Qwen3VLParser.setInitialStateignores the think value) and harmony return their closings even with thinking off. Athink:false+ format request now sends them in two-pass, as single pass already did.thinkingwith an emptycontent.TestThinkFormatThinkingOffParsersThatThinkAnyway(23bf8ed79). Its two-pass rows fail againstmain'sroutes.go.qwen3-vl:2b/4b-thinking).llm/llama_server.go:2381,mlxrunner/client.go:233).server: two-pass prefills glimmer'sto=user<|message|>before pass two binds the format with thinking on (formatContinuationHeader, which also holds harmony's existing prefill).model/parsers: a response that never writes a header no longer returns its closing<|eot|>/<|eom|>as content. This is a backstop for any format that still reaches the runner without closings.Tests
TestThinkFormatGlimmerThinkingOffcovers chat and generate in both modes, over five output shapes: header, thinks-anyway, thinks with no recipient, and headerless with<|eot|>or<|eom|>.TestGlimmerParserHeaderlessEndTagsAreNotContentstreams the headerless reply split at every byte.TestThinkFormatGlimmerTwoPassThinkingOncovers pass two with thinking on, for chat and generate.go test ./server ./model/parsers ./model/renderers ./llm ./mlxrunnerall pass. The second commit was re-run on./server ./model/parsers ./model/renderers.Upstream: if ollama#18687 merges, a fold will meet a clean overlap in
model/parsers. The server half is fork-only, becauseOLLAMA_FORMAT_TWO_PASSis (ADR 0045).macbook-pro-m5-max-128GB/mlx-metal🤖 Generated with Claude Code