Repository navigation
Hold a thought a handler closed for thinking off until a thinking tag arrives (#1532) - #1555
Merged
Merged
Conversation
With thinking off, handlers close a thought the template opened, but the parse still ends a leading thought at the first end tag, as llama.cpp parses the thought it keeps open. Small-chunk streams emitted that thought as content before the end tag arrived. Handlers now mark such results and the stream holds the reply until a thinking tag or the end of the output. Closes #1532.
Owner
Author
|
Design decision (maintainer, 2026-10-28): accept the hold. For results where the handler closed a template-opened thought and the parse takes a leading thought, the stream holds content until |
Contributor
|
Chat app preview removed for |
This was referenced Oct 28, 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
<think>\nbecomes<think></think>\n), but the parse still ends a reply's leading thought at the first end tag, as llama.cpp620d2225parses the thought it keeps open. A stream in small chunks emitted that thought (Plan\n</think>\n\n) as content before the end tag arrived, so streamed content and the final message disagreed.withThoughtClosedForThinkingOff/thoughtClosedForThinkingOffOfinthinking_utils.dart, anExpandolike the closed-thought mark from #1525). Template-result copies keep both marks throughwithThoughtMarksOf. The stream holds a marked reply until it contains a start or end thinking tag, or ends, then runs the held text through the existing parser state machine. Text after the tag streams as before.Production-readiness scope
enableThinking: false(or a thinking-off kwarg), replies on templates that open the thought anyway (StepFun3.5-Flash, DeepSeek R1 distills with the current upstream template, MiniMax M2, Nemotron Nano v2, and any template the marking handlers render that way) never stream a thought aschunk.text. Content waits for</think>,<think>or the end of the reply. Once the model writes</think>, content after it streams live. If the model never writes a thinking tag, the whole reply arrives at the end. That is the cost of matching the final parse at every chunking. With thinking off, reasoning deltas are not emitted, so streaming the held text as reasoning would look the same to the caller.Plan.</think>A</think>B<think>X</think>C, 10 rows per affected template), and non-forced prompts where llamadart's parse takes a leading thought that llama.cpp keeps as content. In those cases the bytewise stream already matches upstream, so changing it would regress upstream parity.Completeness checklist
template-engine-internals.md,chat-template-and-parsing.md,skills/llamadart-chat-streaming/SKILL.md, both changelogs. README/examples/support matrix: N/A, no capability change.)PR type guidance
thinkingForcedOpenwas false, so the stream started outside a thought and released text as content right away. The parse's pre-opened rule (extractThinking: an end tag before any start tag makes the text before it reasoning) only applied once the whole text was available, so a one-chunk stream matched and smaller chunks did not. Tests:</think>or<think>streams as it arrives, an unmarked result is not held, and a forced-open thought streams reasoning.Test Plan
dart run tool/prepare_workspace.dartdart format --output=none --set-exit-if-changed .dart analyze lib test tooldart test -p vm -j 1 --exclude-tags local-only(isolatedTMPDIR): +9482 ~123, all passeddart test -p chrome --exclude-tags local-only(CI). Locally:dart test -p chrome test/unit/core/template test/unit/core/engine/chat_completion_stream_parser_test.dart(+1127, all passed)dart run tool/testing/check_platform_boundaries.dart(OK);./tool/testing/run_template_parity_suites.sh(detection +74, diagnostics +93, unit template +1611, upstream chat suites +10, GBNF +58; all passed, llama.cppb10549); oracle, mutation and real-model runs below.Evidence. The oracle is the #1525 auditor harness built from llama.cpp
620d2225(common_chat_sessionwhole and bytewise): 127,332 cases over 94 templates, thinking on/off, kwarg on/off, notools/none/auto/required/auto-parallel. The stream is fed whole, bytewise, in 3-char chunks and split in two at every point. The 2-split runs on every row where the parse has reasoning or any stream mode differs from the parse.ae5c9abe2)Mutants (37): stream ignores the mark; ignores forced-open precedence; releases only at the end tag; only at the start tag; drops held text at EOF; misses a tag split across chunks; holds until EOF; copies drop the new mark; copies drop the closed-thought mark; the mark ignores
closed; DirectJinja ignoresparsesThoughtBeforeEndTag; MuseGlimmer or MiniMax M1 claim it; each of 18 handlers does not mark; the renderer and each of 5 engine copies drop marks. 34 of 37 killed. The 3 survivors are the Ministral tool-choice-none copy, the generic/content-only grammar copy and the content-only_withFormatcopy. No marked result reaches them: Ministral withnonehas no grammar, and a response schema re-renders Hermes-style templates through the generic handler, which never closes a thought. The same copies were equivalent for the #1525 mark.Real-model check (affected family): DeepSeek-R1-Distill-Llama-8B Q4_K_M, native Metal, temp 0,
enableThinking: false, current upstream template (<think>\n, closed by llamadart). In 4 of 4 prompts the model still wrote a thought ending in</think>. Replaying the recorded emissions: on base, streamed content != final content in 8 of 8 replays (native chunks and bytewise), and</think>leaks bytewise. On head, 8 of 8 match with no leak.engine.createon head streams**Solution:** ...with no thought. StepFun3.5-Flash, MiniMax M2 and Nemotron Nano v2 weights are unavailable locally: the evidence for them is the upstream emissions in the oracle plus the durable StepFun fixture.Matrix Evidence
static-format-analyzeroot-chrometemplate-parityb10549run_template_parity_suites.shstructured-output-adversarial620d2225oraclehigh-risk-exact-head-independent-qa/opt/UnitySrc/personal/llama/llamadart-evidence/pr-1555/audit/evidence-1555-3df1355.jsongguf-chat-features-smokeReview Notes
a32e1eca1, CI pending.High-risk regression review
620d2225emissions (oracle) plus the durable StepFun fixture