8.7.11. dasLLAMA-10 — Thinking Models

A thinking model works through the question before answering, and the reply’s wire format carries that reasoning span: Qwen3 and GLM wrap it in <think>...</think>, gpt-oss speaks Harmony channels, gemma-4 a thought channel. This tutorial splits replies into reasoning and answer — buffered and live-streamed — and then turns thinking off.

Run it with a thinking-family GGUF (Qwen3-0.6B-Q8_0 works):

daslang.exe -jit tutorials/dasLLAMA/10_thinking.das -- path/to/Qwen3-0.6B-Q8_0.gguf

8.7.11.1. The reply carries its reasoning

respond streams the raw pieces — reasoning markers included — while chat.history stores the reply already reasoning-stripped (tutorial 02). Thinking burns token budget, so raise max_new past its 256 default:

var chat = create_chat(m, "You are a concise assistant.", 1024l)
add_user(chat, "Which is larger: 17 * 24 or 20 * 20? One sentence.")
let full = respond(m, chat, SamplingParams()) $(_piece) => true   // buffered: collect only
// full starts "<think> ..." — the raw wire; history stores only the answer

8.7.11.2. split_reasoning: the buffered split

split_reasoning splits a complete reply at the family’s reasoning boundary. Both halves come back stripped when a reasoning span was found; a reply with no reasoning passes through untouched — so it is safe to run on every reply, thinking model or not.

let sp = split_reasoning(chat, full)
print("reasoning: {sp.reasoning}\n")   // the model's work: 17*24 = 408, 20*20 = 400 ...
print("content:   {sp.content}\n")     // the answer: 17 * 24 is larger than 20 * 20.

8.7.11.3. Streaming the split: think_feed / think_finish

A live UI wants the reasoning marked while it streams, not after. The ThinkStream splitter turns each streamed piece into (reasoning, content) deltas, holding partial markers across piece boundaries. Create it for the turn, feed every piece, and flush at the end — an unclosed reasoning span (the token budget cut mid-thought) classifies as reasoning:

var chat2 = create_chat(m, "You are a concise assistant.", 1024l)
add_user(chat2, "What is the capital of Australia? One sentence.")
var ts = make_think_stream(chat2)
respond(m, chat2, SamplingParams()) $(piece) {
    var r = ""
    var c = ""
    think_feed(ts, piece, r, c)   // overwritten per piece; either may be empty
    fprint(fstdout(), c)          // show the answer live; dim or hide r as you like
    fflush(fstdout())
    return true
}
var tail_r = ""
var tail_c = ""
think_finish(ts, tail_r, tail_c)

On a model with no reasoning format the stream is a pass-through: every piece comes back as content. And a small model sometimes ends a later turn inside its think block — the flush then returns the tail as reasoning, and the content half stays empty; the tutorial file shows the pattern that handles both endings.

8.7.11.4. Turning thinking off

Hybrid families answer directly when the turn opens with the template’s empty think block. set_thinking(false) renders exactly that (a no-op for models with no think specials in the vocabulary); the default is on. Schedulers that cut streams themselves read effective_stop_ids — the template’s stops plus the thinking-off extras while thinking is off — rather than the template’s raw stop list:

set_thinking(chat, false)
let stops <- effective_stop_ids(chat)
add_user(chat, "And 12 * 12 vs 11 * 13? One sentence.")
// respond(...) now answers directly — no <think> span on the wire

See also

Full source: tutorials/dasLLAMA/10_thinking.das

Next tutorial: dasLLAMA-11 — Tool Calling

Chat basics and the transcript: dasLLAMA-02 — Chat and Templates