bots-garden/mini-mepublic Fork
⑂ feature/prompt-flag
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP
🛟 Updated. 0394624 · on feature/prompt-flag · k33g · Sep 21, 2026
thinking-models.md · 51 lines · 3.7 KBmarkdown
Blame HistoryOpen raw

Thinking models — explanation

Why mm shows a model's reasoning, where that reasoning comes from, and why reading it was not optional.

What a thinking model sends

A thinking model answers in two phases. First it reasons — sometimes at length — then it answers. On an OpenAI-compatible server the two do not share a field:

data: {"choices":[{"delta":{"reasoning_content":"Here's a thinking process: ..."}}]}
data: {"choices":[{"delta":{"reasoning_content":" 1. Analyze the request ..."}}]}
data: {"choices":[{"delta":{"content":"Quicksort is usually faster because ..."}}]}

reasoning_content is the spelling used by Docker Model Runner, llama.cpp, vLLM and DeepSeek; a few servers say reasoning. mm reads both.

Keeping them apart is the server being helpful: a client can show the reasoning differently, or not at all, without parsing anything out of the answer. Nothing has to be enabled — the models that think, think.

Why mm reads it off the wire itself

Genkit's OpenAI-compatible plugin does not forward reasoning. Its streaming loop builds a chunk from Delta.Content and Delta.ToolCalls, and then calls the handler only if that chunk has something in it. A chunk carrying nothing but reasoning is dropped.

Two things went down with it.

The reasoning itself, obviously — which is why mm used to show nothing at all while a thinking model worked.

And the watchdog's heartbeat, which is the part that mattered. mm cancels a generation when no token has arrived for watchdogTimeout (20 s by default): a hung connection must not hang the agent. That heartbeat lives in the same streaming handler the plugin was not calling. So a model that reasoned for longer than the timeout was killed as "stalled" while its server was streaming tokens perfectly happily — measured on qwen3.5-2b: 38 seconds of uninterrupted reasoning, not one byte of content.

So mm reads the reasoning one layer below Genkit: a middleware on the HTTP client wraps the response body and scans the SSE lines as they are read, passing every byte through untouched. The plugin sees exactly the stream it would have seen.

This is why showThinking: false turns off the display and nothing else. The reasoning is always read, because reading it is what keeps the turn alive.

Where it goes

Front end What you see
Line mode A 💭 block, dim and italic, before the 🤖 answer. Plain text when the output is not a terminal.
Window (mm -tui) Its own ‣ thinking entry, dim italic in a different colour — an aside, visibly not the answer.
Editor (ACP) An agent_thought_chunk update. The protocol has the concept, so this is not a rendering trick: Zed folds it under its own "Thinking" disclosure.

Three properties hold everywhere:

  • Reasoning is never added to the history. The model does not read its own thoughts back on the next turn, a resumed session does not replay them, and they do not count against the context the way the answer does.
  • A summary never shows its reasoning. Compression happens behind the user's back; a model that thinks about a summary must not spill that into the middle of a conversation. The heartbeat still beats there.
  • The listener rides on the request context, not on a package variable, so two turns running at once cannot cross their reasoning.

What this is not

It is not a "reasoning effort" setting: mm asks for nothing and configures nothing on the model side. A model thinks or it does not, and mm shows what arrives.

It is not a parser. Some models instead wrap their reasoning in <think> tags inside content. Those arrive as ordinary answer text and mm shows them as such — the two shapes would need different treatment, and only the field is handled today.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# Thinking models — explanation

> Why `mm` shows a model's reasoning, where that reasoning comes from, and why reading it was not optional.

## What a thinking model sends

A thinking model answers in two phases. First it reasons — sometimes at length — then it answers. On an OpenAI-compatible server the two do not share a field:

```
data: {"choices":[{"delta":{"reasoning_content":"Here's a thinking process: ..."}}]}
data: {"choices":[{"delta":{"reasoning_content":" 1. Analyze the request ..."}}]}
data: {"choices":[{"delta":{"content":"Quicksort is usually faster because ..."}}]}
```

`reasoning_content` is the spelling used by Docker Model Runner, llama.cpp, vLLM and DeepSeek; a few servers say `reasoning`. `mm` reads both.

Keeping them apart is the server being helpful: a client can show the reasoning differently, or not at all, without parsing anything out of the answer. Nothing has to be enabled — the models that think, think.

## Why `mm` reads it off the wire itself

Genkit's OpenAI-compatible plugin does not forward reasoning. Its streaming loop builds a chunk from `Delta.Content` and `Delta.ToolCalls`, and then calls the handler only if that chunk has something in it. A chunk carrying nothing but reasoning is dropped.

Two things went down with it.

The reasoning itself, obviously — which is why `mm` used to show nothing at all while a thinking model worked.

And **the watchdog's heartbeat**, which is the part that mattered. `mm` cancels a generation when no token has arrived for `watchdogTimeout` (20 s by default): a hung connection must not hang the agent. That heartbeat lives in the same streaming handler the plugin was not calling. So a model that reasoned for longer than the timeout was killed as "stalled" while its server was streaming tokens perfectly happily — measured on `qwen3.5-2b`: 38 seconds of uninterrupted reasoning, not one byte of content.

So `mm` reads the reasoning one layer below Genkit: a middleware on the HTTP client wraps the response body and scans the SSE lines as they are read, passing every byte through untouched. The plugin sees exactly the stream it would have seen.

This is why `showThinking: false` turns off the *display* and nothing else. The reasoning is always read, because reading it is what keeps the turn alive.

## Where it goes

| Front end | What you see |
|---|---|
| Line mode | A `💭` block, dim and italic, before the `🤖` answer. Plain text when the output is not a terminal. |
| Window (`mm -tui`) | Its own `‣ thinking` entry, dim italic in a different colour — an aside, visibly not the answer. |
| Editor (ACP) | An `agent_thought_chunk` update. The protocol has the concept, so this is not a rendering trick: Zed folds it under its own "Thinking" disclosure. |

Three properties hold everywhere:

- **Reasoning is never added to the history.** The model does not read its own thoughts back on the next turn, a resumed session does not replay them, and they do not count against the context the way the answer does.
- **A summary never shows its reasoning.** Compression happens behind the user's back; a model that thinks about a summary must not spill that into the middle of a conversation. The heartbeat still beats there.
- **The listener rides on the request context**, not on a package variable, so two turns running at once cannot cross their reasoning.

## What this is not

It is not a "reasoning effort" setting: `mm` asks for nothing and configures nothing on the model side. A model thinks or it does not, and `mm` shows what arrives.

It is not a parser. Some models instead wrap their reasoning in `<think>` tags inside `content`. Those arrive as ordinary answer text and `mm` shows them as such — the two shapes would need different treatment, and only the field is handled today.