.. _tutorial_dasLLAMA_thinking:
=============================
dasLLAMA-10 — Thinking Models
=============================
.. index::
single: Tutorial; dasLLAMA
single: Tutorial; Thinking
single: Tutorial; Reasoning
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
``...``, 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
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:
.. das-doc: given var m = Model()
.. code-block:: das
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 " ..." — the raw wire; history stores only the answer
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.
.. code-block:: das
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.
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:
.. code-block:: das
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.
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:
.. code-block:: das
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 span on the wire
.. seealso::
Full source: :download:`tutorials/dasLLAMA/10_thinking.das <../../../../tutorials/dasLLAMA/10_thinking.das>`
Next tutorial: :ref:`tutorial_dasLLAMA_tool_calling`
Chat basics and the transcript: :ref:`tutorial_dasLLAMA_chat`