Repository navigation
Conversation
This was referenced Nov 10, 2026
…ammar' into fix/1887-minimax-m3-thinking-off
…ammar' into fix/1887-minimax-m3-thinking-off
…before a tool call (#1887)
19 tasks done
leehack
added a commit
that referenced
this pull request
Nov 10, 2026
Owner
Author
|
Landed on main via batch integration PR #1895 (rebase merge, main |
This was referenced Nov 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #1887. Refs #1874 (0.19.4 plan, lane 7).
Stacked on #1890 (base
fix/1531-qwen3-coder-required-grammaratb75aa2e91, on #1885 and #1882). Review commits933e48bee(fix) and302757be6(audit follow-ups);22fe46da5andbf8735ab8merge the moved base (95d4986c9->55c69df04->b75aa2e91). Both merges were clean except that the union-merged changelogs kept #1531's old required-call bullet beside its reworded one; the stale copy was removed each time, so each changelog gains only the #1887 bullet. Retarget once #1890 merges.After a prompt that opens no thought (MiniMax M3 with thinking off, or
thinking_mode: adaptive), llama.cppcd959bae9(common/parsers/minimax-m3.cpp) takes a thought only where<mm:think>starts the reply, with no whitespace before it, drops a</mm:think>there, and keeps every other thinking tag as content. llamadart took a<mm:think>thought from anywhere in the reply, and a</mm:think>after text ended a thought.minimaxM3gets thetakesThoughtOnlyAtReplyStarttrait from Take a thought only at the start of a reply after a prompt that opens no thought, as llama.cpp does (#1865) #1882.thoughtStartsReplyExactly(MiniMax M3 only): the reply-start thought skips no whitespace, and an end tag that starts the reply is dropped.withThoughtOnlyAtReplyStart(parse, exact:)carries it toextractThinking; the stream waits at the reply start for either tag._isToolCallEnvelopeBuffer) applies the same rule, so a<mm:think>after leading whitespace or after a leading</mm:think>, followed by a call before any end tag, streams as content instead of being taken as a thought and suppressed (audit note 1).Why the extra trait. The issue's proposed fix (the #1882 trait alone) fixes the mid-reply thought, but in a wider corpus it breaks 300 rows that matched: a reply starting with a bare
</mm:think>would keep the tag as content, where llama.cpp drops it. The #1882 audit corpus had no such reply.The 16 leading-thought rows from the issue. With thinking off, llama.cpp consumes a leading
<mm:think>…</mm:think>and drops it. llamadart'sparseRenderedstill returns it asreasoningContent(documented: with thinking off the engine drops reasoning chunks, whileparseRenderedstill returns it), so the engine's output matches llama.cpp. The leading-whitespace variants (\n<mm:think>…) were real differences and are fixed.Production-readiness scope
</mm:think>after text, stay content.closed_thought_upstream.md: with thinking off,parseRenderedreturns a leading thought as reasoning that llama.cpp drops (the engine drops it too); a partial parse drops a trailing partial thinking tag after content, which llama.cpp keeps (final parse and stream unaffected). With thinking on and nothinking_modekwarg, llamadart rendersenabled(forced-open<mm:think>) where llama-server rendersadaptive, by design since DeepSeek V3.1 and Qwen thinking parity gaps against llama.cpp (thinking flag, escapes, repeated <think>) #750 and documented in the guide.Completeness checklist
chat-template-and-parsing.md,template-engine-internals.md.skills/llamadart-chat-streaming/SKILL.mdalready states the rule MiniMax M3 now follows; unchanged.)PR type guidance
minimaxM3lacked the reply-start rule, soextractThinkingtook a<mm:think>thought anywhere and ended a thought at a</mm:think>after text; the generic rule also skips leading whitespace and keeps a leading bare end tag, which MiniMax M3's upstream parser does not; and the stream's tool-envelope check stripped any leading thought. Regression tests:closed_thought_upstream.json(thinking off with default kwargs; thinking on withthinking_mode: adaptive), 21 + 26 cases plus 5dropped_thought_cases, recorded from thecd959bae9probe (common_chat_session, whole = byte-by-byte = finish-only for every case; partial cases fromcommon_chat_peg_parse). Each case checksparseRenderedwith and without tools and the stream at whole, per-char, 3-char and every 2-split chunking; dropped-thought cases check the engine stream with thinking off. On base95d4986c947 of the first set failed. The audit follow-up casesunfinished-thought-after-space-then-callandunfinished-thought-after-end-tag-then-callfail (both entries) without the envelope fix;end-tag-onlypins a reply of just</mm:think>.thinking_utils_test.dart:withThoughtOnlyAtReplyStart(exact: true).chat_format_registry_test.dart: the reviewed-dispatch text for the final envelope check now includesexactReplyStart.Test Plan
All at
302757be6unless noted.dart run tool/prepare_workspace.dart(no tracked changes)dart format --output=none --set-exit-if-changed .;dart analyze;dart run tool/testing/check_platform_boundaries.dartdart test -p vm -j 1 --exclude-tags local-only(isolatedTMPDIR): 12,728 passed, 129 skipped, 6 failed, allwindows_registry_qualification_test.dart(needspwsh, absent on this host)dart test -p chrome --exclude-tags local-only test/unit/core test/integration/core: 3,819 passed, 1 skipped (full Chrome run at3cdc8948f: 4,667 passed, 1 skipped)coverage-lib:Test Linux VM (with Coverage)andTest Linux & Web (with Coverage)pass in run 42802975044./tool/testing/run_template_parity_suites.sh: detection 74, diagnostic 103, unit template 1,876, upstream parser 10, GBNF acceptance 81, all passedMatrix Evidence
root-vmpwsh-only failuresroot-chrometemplate-paritystructured-output-adversarialcd959bae9common_chat_session+common_chat_peg_parseoraclecoverage-libhigh-risk-exact-head-independent-qa22fe46da5; re-audit needed at302757be6Upstream comparison (llama.cpp
cd959bae9), baseb75aa2e91vs head302757be6MiniMax M3 corpus: the upstream template x thinking on/off x kwargs (none,
thinking,thinking_modeenabled/disabled,thinking_mode: adaptive) x 5 tool modes x 235 replies (#1858/#1865/#1525 shapes, the #1882 audit shapes, bare</mm:think>, namespaced calls, and unfinished thoughts after whitespace or an end tag before a call). "Engine" is the stream with the request's ownenableThinking.The remaining mismatches are the forced-open render with thinking on and no
thinking_mode(see above), the documented thinking-offparseRenderedreasoning, and partial trailing tag prefixes.All-template sweeps (76 upstream
models/templates+ vendoredtest/fixtures/templates; thinking on/off x kwargs x 5 tool modes):Only MiniMax M3 rows change, apart from 7 Gemma 4 rows whose byte, 2-char or 3-char stream already differs from their parse on base and leaks a different length of call JSON as content from run to run (the stream's time-based partial parse interval); parse results are identical and both runs mismatch, so they are unrelated to this change.
Mutation check
16 mutants over
core/unit,core/engineandcore/templatetests. 12 killed: trait removed, exact trait removed, parse keeps the leading end tag, parse skips leading space, engine passesexact: false, stream exact rule off, stream does not wait on an end-tag prefix, stream keeps the end tag, stream skips leading space, zone rule ignoresexact, B5 (rest.length <= endTag.length, killed byend-tag-only), and envelope check without the exact rule (killed by the two new tool-call cases). 4 give identical results on all 18,800 MiniMax M3 corpus rows: the tool-mode decision path without the exact rule (unreachable for MiniMax M3's stream, so that argument was dropped), the exact strip not dropping a leading end tag or not waiting on a partial tag (the envelope check only suppresses content the parse leaves empty in those shapes). TheleadingThoughtOnlyguard on the exact flag also survives but is not equivalent:_isToolCallEnvelopeBufferdoes reach the exact strip on forced-open prompts, and removing the guard makes 88 forced-open corpus rows chunking-dependent; no test pins it yet (independent audit at302757be, follow-up on #1861).Review Notes
22fe46da5(audit notes 1–3 addressed in302757be6); re-audit of302757be6pending302757be6; dispatchedci.ymlrun 42802975044: 26 passed, 6 skipped, 0 failedHigh-risk regression review
backendRuntime,structuredOutput)22fe46da5; pending at302757be6302757be6/b75aa2e91(fix/1531-qwen3-coder-required-grammar)cd959bae9parser via the oracle, with durable fixtures inclosed_thought_upstream.json.