bots-garden/mini-mepublic Fork
⑂ fix/plan-mode
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. 13af864Unverified · on fix/plan-mode · k33g · 16d ago
thinking-models.md · 53 lines · 5.6 KBmarkdown
Blame HistoryOpen raw

Modèles à raisonnement — explication

Pourquoi mm affiche le raisonnement d'un modèle, d'où il vient, et pourquoi le lire n'était pas optionnel.

Ce qu'envoie un modèle à raisonnement

Un modèle à raisonnement répond en deux temps. D'abord il réfléchit — parfois longuement — puis il répond. Sur un serveur compatible OpenAI, les deux ne partagent pas le même champ :

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 est l'orthographe de Docker Model Runner, llama.cpp, vLLM et DeepSeek ; quelques serveurs disent reasoning. mm lit les deux.

Les séparer, c'est le serveur qui rend service : un client peut afficher le raisonnement autrement, ou pas du tout, sans rien extraire de la réponse. Rien à activer — les modèles qui réfléchissent réfléchissent.

Pourquoi mm le lit lui-même sur le fil

Le greffon compatible OpenAI de Genkit ne transmet pas le raisonnement. Sa boucle de streaming construit un chunk à partir de Delta.Content et Delta.ToolCalls, puis n'appelle le gestionnaire que si ce chunk contient quelque chose. Un chunk qui ne porte que du raisonnement est jeté.

Deux choses tombaient avec lui.

Le raisonnement lui-même, évidemment — c'est pourquoi mm n'affichait rien du tout pendant qu'un modèle à raisonnement travaillait.

Et le battement du chien de garde, qui est la partie sérieuse. mm annule une génération quand aucun token n'est arrivé depuis watchdogTimeout (20 s par défaut) : une connexion pendue ne doit pas pendre l'agent. Ce battement vit dans le gestionnaire de streaming que le greffon n'appelait pas. Un modèle qui raisonnait plus longtemps que le délai était donc tué pour « stalled » alors que son serveur diffusait des tokens sans le moindre problème — mesuré sur qwen3.5-2b : 38 secondes de raisonnement ininterrompu, pas un octet de contenu.

mm lit donc le raisonnement une couche sous Genkit : un intergiciel sur le client HTTP enveloppe le corps de la réponse et analyse les lignes SSE à mesure qu'elles sont lues, en laissant passer chaque octet intact. Le greffon voit exactement le flux qu'il aurait vu.

D'où le fait que showThinking: false coupe l'affichage et rien d'autre. Le raisonnement est toujours lu, parce que le lire est ce qui garde le tour en vie.

Depuis Genkit 1.13, le greffon transmet bien le raisonnement : sous forme de chunks qui ne contiennent que du raisonnement, passés au gestionnaire de streaming, et sous forme d'une partie de raisonnement dans le message final. mm garde malgré tout sa propre lecture — une seule voie pour l'affichage et le battement, quels que soient la version de Genkit et le serveur — et neutralise ces deux nouvelles livraisons. Un chunk qui ne contient que du raisonnement est ignoré par le gestionnaire de mm : traité comme un morceau de réponse vide, chacun fermait le bloc de pensée, et le mode ligne affichait un bloc 💭 par token. La partie de raisonnement du message final est retirée avant que Genkit ou mm ne conserve le message : laissée en place, elle repartirait vers le serveur comme reasoning_content du message de l'assistant au prochain appel du modèle.

Où il va

Façade Ce que vous voyez
Mode ligne Un bloc 💭, grisé et en italique, avant la réponse 🤖. Texte brut quand la sortie n'est pas un terminal.
Fenêtre (mm -tui) Son entrée ‣ thinking, grisée, en italique et d'une autre couleur — un aparté, visiblement pas la réponse.
Éditeur (ACP) Une mise à jour agent_thought_chunk. Le protocole connaît la notion, ce n'est donc pas une astuce d'affichage : Zed le replie sous son propre volet « Thinking ».

Trois propriétés valent partout :

  • Le raisonnement n'entre jamais dans l'historique. Le modèle ne relit pas ses propres pensées au tour suivant, une session reprise ne les rejoue pas, et elles ne pèsent pas sur le contexte comme le fait la réponse.
  • Un résumé n'affiche jamais son raisonnement. La compression se fait dans le dos de l'utilisateur ; un modèle qui réfléchit à un résumé ne doit pas déverser ça au milieu d'une conversation. Le battement, lui, continue.
  • L'écouteur voyage sur le contexte de la requête, pas dans une variable de paquet : deux tours simultanés ne peuvent pas croiser leurs raisonnements.

Ce que ce n'est pas

Ce n'est pas un réglage d'« effort de raisonnement » : mm n'a pas de clé à lui pour cela, et affiche ce qui arrive. On peut tout de même demander au serveur de ne pas raisonner, via sampling, dont les clés inconnues de l'API OpenAI sont transmises telles quelles (depuis Genkit 1.12) : chat_template_kwargs: {enable_thinking: false} pour un serveur llama.cpp (dmr, llamacpp, llmman), reasoning_effort: none pour Ollama. Mesuré avec Qwen3.5 4B sur Docker Model Runner : la même réponse en moins d'une seconde, sans les quatre secondes de raisonnement. mm le fait de lui-même, serveur par serveur, pour le seul résumé d'une compaction. Voir Couper le raisonnement d'un modèle.

Ce n'est pas un analyseur. Certains modèles encadrent plutôt leur raisonnement de balises <think> à l'intérieur de content. Celles-là arrivent comme du texte de réponse ordinaire et mm les affiche comme tel — les deux formes demanderaient des traitements différents, et seul le champ est géré aujourd'hui.

 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
52
53
# Modèles à raisonnement — explication

> Pourquoi `mm` affiche le raisonnement d'un modèle, d'où il vient, et pourquoi le lire n'était pas optionnel.

## Ce qu'envoie un modèle à raisonnement

Un modèle à raisonnement répond en deux temps. D'abord il réfléchit — parfois longuement — puis il répond. Sur un serveur compatible OpenAI, les deux ne partagent pas le même champ :

```
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` est l'orthographe de Docker Model Runner, llama.cpp, vLLM et DeepSeek ; quelques serveurs disent `reasoning`. `mm` lit les deux.

Les séparer, c'est le serveur qui rend service : un client peut afficher le raisonnement autrement, ou pas du tout, sans rien extraire de la réponse. Rien à activer — les modèles qui réfléchissent réfléchissent.

## Pourquoi `mm` le lit lui-même sur le fil

Le greffon compatible OpenAI de Genkit ne transmet pas le raisonnement. Sa boucle de streaming construit un chunk à partir de `Delta.Content` et `Delta.ToolCalls`, puis n'appelle le gestionnaire que si ce chunk contient quelque chose. Un chunk qui ne porte que du raisonnement est jeté.

Deux choses tombaient avec lui.

Le raisonnement lui-même, évidemment — c'est pourquoi `mm` n'affichait rien du tout pendant qu'un modèle à raisonnement travaillait.

Et **le battement du chien de garde**, qui est la partie sérieuse. `mm` annule une génération quand aucun token n'est arrivé depuis `watchdogTimeout` (20 s par défaut) : une connexion pendue ne doit pas pendre l'agent. Ce battement vit dans le gestionnaire de streaming que le greffon n'appelait pas. Un modèle qui raisonnait plus longtemps que le délai était donc tué pour « stalled » alors que son serveur diffusait des tokens sans le moindre problème — mesuré sur `qwen3.5-2b` : 38 secondes de raisonnement ininterrompu, pas un octet de contenu.

`mm` lit donc le raisonnement une couche sous Genkit : un intergiciel sur le client HTTP enveloppe le corps de la réponse et analyse les lignes SSE à mesure qu'elles sont lues, en laissant passer chaque octet intact. Le greffon voit exactement le flux qu'il aurait vu.

D'où le fait que `showThinking: false` coupe l'*affichage* et rien d'autre. Le raisonnement est toujours lu, parce que le lire est ce qui garde le tour en vie.

Depuis Genkit 1.13, le greffon transmet bien le raisonnement : sous forme de chunks qui ne contiennent que du raisonnement, passés au gestionnaire de streaming, et sous forme d'une partie de raisonnement dans le message final. `mm` garde malgré tout sa propre lecture — une seule voie pour l'affichage et le battement, quels que soient la version de Genkit et le serveur — et neutralise ces deux nouvelles livraisons. Un chunk qui ne contient que du raisonnement est ignoré par le gestionnaire de `mm` : traité comme un morceau de réponse vide, chacun fermait le bloc de pensée, et le mode ligne affichait un bloc `💭` par token. La partie de raisonnement du message final est retirée avant que Genkit ou `mm` ne conserve le message : laissée en place, elle repartirait vers le serveur comme `reasoning_content` du message de l'assistant au prochain appel du modèle.

## Où il va

| Façade | Ce que vous voyez |
|---|---|
| Mode ligne | Un bloc `💭`, grisé et en italique, avant la réponse `🤖`. Texte brut quand la sortie n'est pas un terminal. |
| Fenêtre (`mm -tui`) | Son entrée `‣ thinking`, grisée, en italique et d'une autre couleur — un aparté, visiblement pas la réponse. |
| Éditeur (ACP) | Une mise à jour `agent_thought_chunk`. Le protocole connaît la notion, ce n'est donc pas une astuce d'affichage : Zed le replie sous son propre volet « Thinking ». |

Trois propriétés valent partout :

- **Le raisonnement n'entre jamais dans l'historique.** Le modèle ne relit pas ses propres pensées au tour suivant, une session reprise ne les rejoue pas, et elles ne pèsent pas sur le contexte comme le fait la réponse.
- **Un résumé n'affiche jamais son raisonnement.** La compression se fait dans le dos de l'utilisateur ; un modèle qui réfléchit à un résumé ne doit pas déverser ça au milieu d'une conversation. Le battement, lui, continue.
- **L'écouteur voyage sur le contexte de la requête**, pas dans une variable de paquet : deux tours simultanés ne peuvent pas croiser leurs raisonnements.

## Ce que ce n'est pas

Ce n'est pas un réglage d'« effort de raisonnement » : `mm` n'a pas de clé à lui pour cela, et affiche ce qui arrive. On peut tout de même demander au serveur de ne pas raisonner, via `sampling`, dont les clés inconnues de l'API OpenAI sont transmises telles quelles (depuis Genkit 1.12) : `chat_template_kwargs: {enable_thinking: false}` pour un serveur llama.cpp (`dmr`, `llamacpp`, `llmman`), `reasoning_effort: none` pour Ollama. Mesuré avec Qwen3.5 4B sur Docker Model Runner : la même réponse en moins d'une seconde, sans les quatre secondes de raisonnement. `mm` le fait de lui-même, serveur par serveur, pour le seul résumé d'une compaction. Voir [Couper le raisonnement d'un modèle](../how-to/turn-reasoning-off.md).

Ce n'est pas un analyseur. Certains modèles encadrent plutôt leur raisonnement de balises `<think>` à l'intérieur de `content`. Celles-là arrivent comme du texte de réponse ordinaire et `mm` les affiche comme tel — les deux formes demanderaient des traitements différents, et seul le champ est géré aujourd'hui.