bots-garden/mini-mepublic Fork
⑂ 30a5de4
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.Unverified

k33g committed 2026-10-06T13:21:28+02:00 Browse files
30a5de4 parent: 5f0c1e0
modified demo-llmman/agent.llmman.yaml +1 -1
@@ -192,7 +192,7 @@ sampling:
192192 top_p: 0.9
193193 max_tokens: 4096
194194 chat_template_kwargs:
195- enable_thinking: true
195+ enable_thinking: false
196196
197197 # Silence allowed between two tokens once the answer has started.
198198 watchdogTimeout: 3m
@@ -192,7 +192,7 @@ sampling:
192 top_p: 0.9192 top_p: 0.9
193 max_tokens: 4096193 max_tokens: 4096
194 chat_template_kwargs:194 chat_template_kwargs:
195- enable_thinking: true195+ enable_thinking: false
196 196
197 # Silence allowed between two tokens once the answer has started.197 # Silence allowed between two tokens once the answer has started.
198 watchdogTimeout: 3m198 watchdogTimeout: 3m
modified docs/en/how-to/turn-reasoning-off.md +17 -1
@@ -44,8 +44,24 @@ A thinking model (Qwen3.5, Qwen3.x, gpt-oss…) reasons before it answers. On a
4444
4545 - **Hide the reasoning but let the model think.** `showThinking: false` removes the `💭` block, the window's `‣ thinking` entry and the editor's thought chunks. The model still reasons — same time, same tokens.
4646 - **The model ignores the switch.** `enable_thinking` is a variable of the model's chat template: a template that does not read it (a model without a thinking mode, or with another variable name) is not affected. Check the model card for the variable its template reads.
47+- **Lower the effort instead of turning it off.** Some templates read a graded variable rather than an on/off one — gpt-oss's reads `reasoning_effort`, with `low`, `medium` or `high`. On `dmr`, `llamacpp` or `llmman`, pass it the same way, under `chat_template_kwargs`:
48+
49+ ```yaml
50+ sampling:
51+ temperature: 0.0
52+ max_tokens: 4096
53+ chat_template_kwargs:
54+ reasoning_effort: low
55+ ```
56+
57+ Use it with care:
58+ - Reported to work by a user with `llmman` and `llamacpp`; not measured in this project (no test, no timing) and not tried on `dmr`.
59+ - The variable's name and its values belong to the model's template, not to `mm`, which forwards them unchecked. A template that does not read the variable, or a value it does not know, is ignored without any error: compare the length of the `💭` block with and without the setting to know whether it took effect.
60+ - Qwen3.x templates read `enable_thinking` only: on/off, no levels.
61+ - On `ollama`, `reasoning_effort` goes at the top level of `sampling`, not under `chat_template_kwargs`; only `none` has been measured there.
62+ - Compaction summaries do not follow it — see below.
4763 - **Turn it off for one run.** Keep two configuration files, or start the server itself without reasoning (for llama-server, see its `--reasoning-budget` option).
48-- **Compaction summaries.** Nothing to do: `mm` already asks the server not to reason for the summary of `/compact` and of the automatic compression, whatever `sampling` says.
64+- **Compaction summaries.** Nothing to do: `mm` turns reasoning off for the summary of `/compact` and of the automatic compression, whatever `sampling` says. On `dmr`, `llamacpp` and `llmman` it sets `enable_thinking: false` in `chat_template_kwargs`, keeps your other template variables, and lowers a `reasoning_effort` you set there to `low` — gpt-oss's template has no "off", so a summary from that model still reasons a little and that counts against `context.summaryMaxTokens`. On `ollama` it sends `reasoning_effort: none`.
4965
5066 ## See also
5167
@@ -44,8 +44,24 @@ A thinking model (Qwen3.5, Qwen3.x, gpt-oss…) reasons before it answers. On a
44 44
45 - **Hide the reasoning but let the model think.** `showThinking: false` removes the `💭` block, the window's `‣ thinking` entry and the editor's thought chunks. The model still reasons — same time, same tokens.45 - **Hide the reasoning but let the model think.** `showThinking: false` removes the `💭` block, the window's `‣ thinking` entry and the editor's thought chunks. The model still reasons — same time, same tokens.
46 - **The model ignores the switch.** `enable_thinking` is a variable of the model's chat template: a template that does not read it (a model without a thinking mode, or with another variable name) is not affected. Check the model card for the variable its template reads.46 - **The model ignores the switch.** `enable_thinking` is a variable of the model's chat template: a template that does not read it (a model without a thinking mode, or with another variable name) is not affected. Check the model card for the variable its template reads.
47+- **Lower the effort instead of turning it off.** Some templates read a graded variable rather than an on/off one — gpt-oss's reads `reasoning_effort`, with `low`, `medium` or `high`. On `dmr`, `llamacpp` or `llmman`, pass it the same way, under `chat_template_kwargs`:
48+
49+ ```yaml
50+ sampling:
51+ temperature: 0.0
52+ max_tokens: 4096
53+ chat_template_kwargs:
54+ reasoning_effort: low
55+ ```
56+
57+ Use it with care:
58+ - Reported to work by a user with `llmman` and `llamacpp`; not measured in this project (no test, no timing) and not tried on `dmr`.
59+ - The variable's name and its values belong to the model's template, not to `mm`, which forwards them unchecked. A template that does not read the variable, or a value it does not know, is ignored without any error: compare the length of the `💭` block with and without the setting to know whether it took effect.
60+ - Qwen3.x templates read `enable_thinking` only: on/off, no levels.
61+ - On `ollama`, `reasoning_effort` goes at the top level of `sampling`, not under `chat_template_kwargs`; only `none` has been measured there.
62+ - Compaction summaries do not follow it — see below.
47 - **Turn it off for one run.** Keep two configuration files, or start the server itself without reasoning (for llama-server, see its `--reasoning-budget` option).63 - **Turn it off for one run.** Keep two configuration files, or start the server itself without reasoning (for llama-server, see its `--reasoning-budget` option).
48-- **Compaction summaries.** Nothing to do: `mm` already asks the server not to reason for the summary of `/compact` and of the automatic compression, whatever `sampling` says.64+- **Compaction summaries.** Nothing to do: `mm` turns reasoning off for the summary of `/compact` and of the automatic compression, whatever `sampling` says. On `dmr`, `llamacpp` and `llmman` it sets `enable_thinking: false` in `chat_template_kwargs`, keeps your other template variables, and lowers a `reasoning_effort` you set there to `low` — gpt-oss's template has no "off", so a summary from that model still reasons a little and that counts against `context.summaryMaxTokens`. On `ollama` it sends `reasoning_effort: none`.
49 65
50 ## See also66 ## See also
51 67
modified docs/en/reference/configuration.md +1 -1
@@ -43,7 +43,7 @@
4343 | `threshold` | int | `75` | Percentage of the context window beyond which the history is compressed before the next question. Between 1 and 100. |
4444 | `maxMessages` | int | `80` | Message count that triggers a compression when the window is unknown or the estimate is low. `0` disables. Must be ≥ 0. |
4545 | `keepLastTurns` | int | `3` | Question turns kept raw at the end of the history. Must be ≥ 1. |
46-| `summaryMaxTokens` | int | `1200` | `max_tokens` of the summary request. Must be > 0. A thinking model's reasoning counts against it: on `dmr`, `llamacpp` and `llmman` the summary request turns reasoning off (`chat_template_kwargs.enable_thinking: false`), on `ollama` too (`reasoning_effort: "none"`, the one switch Ollama obeys); elsewhere, if the error says the model "reasoned until the limit", raise it. |
46+| `summaryMaxTokens` | int | `1200` | `max_tokens` of the summary request. Must be > 0. A thinking model's reasoning counts against it: on `dmr`, `llamacpp` and `llmman` the summary request turns reasoning off (`chat_template_kwargs.enable_thinking: false`), on `ollama` too (`reasoning_effort: "none"`, the one switch Ollama obeys) — whatever `sampling` says; other `chat_template_kwargs` are kept, and a `reasoning_effort` set there is lowered to `low` (gpt-oss has no "off"); elsewhere, if the error says the model "reasoned until the limit", raise it. |
4747 | `prompt` | string | `""` | Replaces the built-in summary prompt when not empty. |
4848 | `showStats` | bool | `true` | Print the one-line `🗜️` report after each compression. |
4949
@@ -43,7 +43,7 @@
43 | `threshold` | int | `75` | Percentage of the context window beyond which the history is compressed before the next question. Between 1 and 100. |43 | `threshold` | int | `75` | Percentage of the context window beyond which the history is compressed before the next question. Between 1 and 100. |
44 | `maxMessages` | int | `80` | Message count that triggers a compression when the window is unknown or the estimate is low. `0` disables. Must be ≥ 0. |44 | `maxMessages` | int | `80` | Message count that triggers a compression when the window is unknown or the estimate is low. `0` disables. Must be ≥ 0. |
45 | `keepLastTurns` | int | `3` | Question turns kept raw at the end of the history. Must be ≥ 1. |45 | `keepLastTurns` | int | `3` | Question turns kept raw at the end of the history. Must be ≥ 1. |
46-| `summaryMaxTokens` | int | `1200` | `max_tokens` of the summary request. Must be > 0. A thinking model's reasoning counts against it: on `dmr`, `llamacpp` and `llmman` the summary request turns reasoning off (`chat_template_kwargs.enable_thinking: false`), on `ollama` too (`reasoning_effort: "none"`, the one switch Ollama obeys); elsewhere, if the error says the model "reasoned until the limit", raise it. |46+| `summaryMaxTokens` | int | `1200` | `max_tokens` of the summary request. Must be > 0. A thinking model's reasoning counts against it: on `dmr`, `llamacpp` and `llmman` the summary request turns reasoning off (`chat_template_kwargs.enable_thinking: false`), on `ollama` too (`reasoning_effort: "none"`, the one switch Ollama obeys) — whatever `sampling` says; other `chat_template_kwargs` are kept, and a `reasoning_effort` set there is lowered to `low` (gpt-oss has no "off"); elsewhere, if the error says the model "reasoned until the limit", raise it. |
47 | `prompt` | string | `""` | Replaces the built-in summary prompt when not empty. |47 | `prompt` | string | `""` | Replaces the built-in summary prompt when not empty. |
48 | `showStats` | bool | `true` | Print the one-line `🗜️` report after each compression. |48 | `showStats` | bool | `true` | Print the one-line `🗜️` report after each compression. |
49 49
modified docs/fr/how-to/turn-reasoning-off.md +17 -1
@@ -44,8 +44,24 @@ Un modèle à raisonnement (Qwen3.5, Qwen3.x, gpt-oss…) réfléchit avant de r
4444
4545 - **Masquer le raisonnement mais laisser le modèle réfléchir.** `showThinking: false` retire le bloc `💭`, l'entrée `‣ thinking` de la fenêtre et les morceaux de pensée envoyés à l'éditeur. Le modèle raisonne quand même — même durée, mêmes tokens.
4646 - **Le modèle ignore l'interrupteur.** `enable_thinking` est une variable du template de chat du modèle : un template qui ne la lit pas (un modèle sans mode de raisonnement, ou qui utilise un autre nom de variable) n'est pas concerné. Vérifiez sur la fiche du modèle la variable que lit son template.
47+- **Baisser l'effort plutôt que couper.** Certains templates lisent une variable graduée plutôt qu'un interrupteur — celui de gpt-oss lit `reasoning_effort`, qui vaut `low`, `medium` ou `high`. Avec `dmr`, `llamacpp` ou `llmman`, transmettez-la de la même façon, sous `chat_template_kwargs` :
48+
49+ ```yaml
50+ sampling:
51+ temperature: 0.0
52+ max_tokens: 4096
53+ chat_template_kwargs:
54+ reasoning_effort: low
55+ ```
56+
57+ Précautions d'usage :
58+ - Signalé comme fonctionnel par un utilisateur avec `llmman` et `llamacpp` ; non mesuré dans ce projet (ni test, ni chronométrage) et non essayé avec `dmr`.
59+ - Le nom de la variable et ses valeurs appartiennent au template du modèle, pas à `mm`, qui les transmet sans les vérifier. Un template qui ne lit pas la variable, ou une valeur qu'il ne connaît pas, est ignoré sans la moindre erreur : comparez la longueur du bloc `💭` avec et sans le réglage pour savoir s'il a pris effet.
60+ - Les templates Qwen3.x ne lisent que `enable_thinking` : marche/arrêt, pas de niveaux.
61+ - Avec `ollama`, `reasoning_effort` se place au premier niveau de `sampling`, pas sous `chat_template_kwargs` ; seule la valeur `none` y a été mesurée.
62+ - Les résumés de compaction ne le suivent pas — voir ci-dessous.
4763 - **Le couper pour une seule exécution.** Gardez deux fichiers de configuration, ou démarrez le serveur lui-même sans raisonnement (pour llama-server, voir son option `--reasoning-budget`).
48-- **Résumés de compaction.** Rien à faire : `mm` demande déjà au serveur de ne pas raisonner pour le résumé de `/compact` et de la compression automatique, quoi que dise `sampling`.
64+- **Résumés de compaction.** Rien à faire : `mm` coupe le raisonnement pour le résumé de `/compact` et de la compression automatique, quoi que dise `sampling`. Avec `dmr`, `llamacpp` et `llmman`, il fixe `enable_thinking: false` dans `chat_template_kwargs`, garde vos autres variables de template, et ramène à `low` un `reasoning_effort` que vous y avez mis — le template de gpt-oss n'a pas d'« arrêt », donc un résumé produit par ce modèle raisonne encore un peu, et cela est décompté de `context.summaryMaxTokens`. Avec `ollama`, il envoie `reasoning_effort: none`.
4965
5066 ## Voir aussi
5167
@@ -44,8 +44,24 @@ Un modèle à raisonnement (Qwen3.5, Qwen3.x, gpt-oss…) réfléchit avant de r
44 44
45 - **Masquer le raisonnement mais laisser le modèle réfléchir.** `showThinking: false` retire le bloc `💭`, l'entrée `‣ thinking` de la fenêtre et les morceaux de pensée envoyés à l'éditeur. Le modèle raisonne quand même — même durée, mêmes tokens.45 - **Masquer le raisonnement mais laisser le modèle réfléchir.** `showThinking: false` retire le bloc `💭`, l'entrée `‣ thinking` de la fenêtre et les morceaux de pensée envoyés à l'éditeur. Le modèle raisonne quand même — même durée, mêmes tokens.
46 - **Le modèle ignore l'interrupteur.** `enable_thinking` est une variable du template de chat du modèle : un template qui ne la lit pas (un modèle sans mode de raisonnement, ou qui utilise un autre nom de variable) n'est pas concerné. Vérifiez sur la fiche du modèle la variable que lit son template.46 - **Le modèle ignore l'interrupteur.** `enable_thinking` est une variable du template de chat du modèle : un template qui ne la lit pas (un modèle sans mode de raisonnement, ou qui utilise un autre nom de variable) n'est pas concerné. Vérifiez sur la fiche du modèle la variable que lit son template.
47+- **Baisser l'effort plutôt que couper.** Certains templates lisent une variable graduée plutôt qu'un interrupteur — celui de gpt-oss lit `reasoning_effort`, qui vaut `low`, `medium` ou `high`. Avec `dmr`, `llamacpp` ou `llmman`, transmettez-la de la même façon, sous `chat_template_kwargs` :
48+
49+ ```yaml
50+ sampling:
51+ temperature: 0.0
52+ max_tokens: 4096
53+ chat_template_kwargs:
54+ reasoning_effort: low
55+ ```
56+
57+ Précautions d'usage :
58+ - Signalé comme fonctionnel par un utilisateur avec `llmman` et `llamacpp` ; non mesuré dans ce projet (ni test, ni chronométrage) et non essayé avec `dmr`.
59+ - Le nom de la variable et ses valeurs appartiennent au template du modèle, pas à `mm`, qui les transmet sans les vérifier. Un template qui ne lit pas la variable, ou une valeur qu'il ne connaît pas, est ignoré sans la moindre erreur : comparez la longueur du bloc `💭` avec et sans le réglage pour savoir s'il a pris effet.
60+ - Les templates Qwen3.x ne lisent que `enable_thinking` : marche/arrêt, pas de niveaux.
61+ - Avec `ollama`, `reasoning_effort` se place au premier niveau de `sampling`, pas sous `chat_template_kwargs` ; seule la valeur `none` y a été mesurée.
62+ - Les résumés de compaction ne le suivent pas — voir ci-dessous.
47 - **Le couper pour une seule exécution.** Gardez deux fichiers de configuration, ou démarrez le serveur lui-même sans raisonnement (pour llama-server, voir son option `--reasoning-budget`).63 - **Le couper pour une seule exécution.** Gardez deux fichiers de configuration, ou démarrez le serveur lui-même sans raisonnement (pour llama-server, voir son option `--reasoning-budget`).
48-- **Résumés de compaction.** Rien à faire : `mm` demande déjà au serveur de ne pas raisonner pour le résumé de `/compact` et de la compression automatique, quoi que dise `sampling`.64+- **Résumés de compaction.** Rien à faire : `mm` coupe le raisonnement pour le résumé de `/compact` et de la compression automatique, quoi que dise `sampling`. Avec `dmr`, `llamacpp` et `llmman`, il fixe `enable_thinking: false` dans `chat_template_kwargs`, garde vos autres variables de template, et ramène à `low` un `reasoning_effort` que vous y avez mis — le template de gpt-oss n'a pas d'« arrêt », donc un résumé produit par ce modèle raisonne encore un peu, et cela est décompté de `context.summaryMaxTokens`. Avec `ollama`, il envoie `reasoning_effort: none`.
49 65
50 ## Voir aussi66 ## Voir aussi
51 67
modified docs/fr/reference/configuration.md +1 -1
@@ -43,7 +43,7 @@
4343 | `threshold` | entier | `75` | Pourcentage de la fenêtre de contexte au-delà duquel l'historique est compressé avant la question suivante. Entre 1 et 100. |
4444 | `maxMessages` | entier | `80` | Nombre de messages qui déclenche une compression quand la fenêtre est inconnue ou l'estimation trop basse. `0` désactive. Doit être ≥ 0. |
4545 | `keepLastTurns` | entier | `3` | Tours de question conservés bruts en fin d'historique. Doit être ≥ 1. |
46-| `summaryMaxTokens` | entier | `1200` | `max_tokens` de la requête de résumé. Doit être > 0. Le raisonnement d'un modèle qui réfléchit est décompté de ce budget : sur `dmr`, `llamacpp` et `llmman`, la requête de résumé désactive le raisonnement (`chat_template_kwargs.enable_thinking: false`), sur `ollama` aussi (`reasoning_effort: "none"`, le seul interrupteur qu'Ollama respecte) ; ailleurs, si l'erreur dit que le modèle « reasoned until the limit », augmentez-le. |
46+| `summaryMaxTokens` | entier | `1200` | `max_tokens` de la requête de résumé. Doit être > 0. Le raisonnement d'un modèle qui réfléchit est décompté de ce budget : sur `dmr`, `llamacpp` et `llmman`, la requête de résumé désactive le raisonnement (`chat_template_kwargs.enable_thinking: false`), sur `ollama` aussi (`reasoning_effort: "none"`, le seul interrupteur qu'Ollama respecte) — quoi que dise `sampling` ; les autres `chat_template_kwargs` sont gardés, et un `reasoning_effort` qui y est fixé est ramené à `low` (gpt-oss n'a pas d'« arrêt ») ; ailleurs, si l'erreur dit que le modèle « reasoned until the limit », augmentez-le. |
4747 | `prompt` | chaîne | `""` | Remplace le prompt de résumé intégré quand non vide. |
4848 | `showStats` | booléen | `true` | Affiche le rapport `🗜️` d'une ligne après chaque compression. |
4949
@@ -43,7 +43,7 @@
43 | `threshold` | entier | `75` | Pourcentage de la fenêtre de contexte au-delà duquel l'historique est compressé avant la question suivante. Entre 1 et 100. |43 | `threshold` | entier | `75` | Pourcentage de la fenêtre de contexte au-delà duquel l'historique est compressé avant la question suivante. Entre 1 et 100. |
44 | `maxMessages` | entier | `80` | Nombre de messages qui déclenche une compression quand la fenêtre est inconnue ou l'estimation trop basse. `0` désactive. Doit être ≥ 0. |44 | `maxMessages` | entier | `80` | Nombre de messages qui déclenche une compression quand la fenêtre est inconnue ou l'estimation trop basse. `0` désactive. Doit être ≥ 0. |
45 | `keepLastTurns` | entier | `3` | Tours de question conservés bruts en fin d'historique. Doit être ≥ 1. |45 | `keepLastTurns` | entier | `3` | Tours de question conservés bruts en fin d'historique. Doit être ≥ 1. |
46-| `summaryMaxTokens` | entier | `1200` | `max_tokens` de la requête de résumé. Doit être > 0. Le raisonnement d'un modèle qui réfléchit est décompté de ce budget : sur `dmr`, `llamacpp` et `llmman`, la requête de résumé désactive le raisonnement (`chat_template_kwargs.enable_thinking: false`), sur `ollama` aussi (`reasoning_effort: "none"`, le seul interrupteur qu'Ollama respecte) ; ailleurs, si l'erreur dit que le modèle « reasoned until the limit », augmentez-le. |46+| `summaryMaxTokens` | entier | `1200` | `max_tokens` de la requête de résumé. Doit être > 0. Le raisonnement d'un modèle qui réfléchit est décompté de ce budget : sur `dmr`, `llamacpp` et `llmman`, la requête de résumé désactive le raisonnement (`chat_template_kwargs.enable_thinking: false`), sur `ollama` aussi (`reasoning_effort: "none"`, le seul interrupteur qu'Ollama respecte) — quoi que dise `sampling` ; les autres `chat_template_kwargs` sont gardés, et un `reasoning_effort` qui y est fixé est ramené à `low` (gpt-oss n'a pas d'« arrêt ») ; ailleurs, si l'erreur dit que le modèle « reasoned until the limit », augmentez-le. |
47 | `prompt` | chaîne | `""` | Remplace le prompt de résumé intégré quand non vide. |47 | `prompt` | chaîne | `""` | Remplace le prompt de résumé intégré quand non vide. |
48 | `showStats` | booléen | `true` | Affiche le rapport `🗜️` d'une ligne après chaque compression. |48 | `showStats` | booléen | `true` | Affiche le rapport `🗜️` d'une ligne après chaque compression. |
49 49
modified internal/engine/engine.go +1 -1
@@ -525,7 +525,7 @@ func (e *Engine) Generate(ctx context.Context, messages []*ai.Message, tools []a
525525 // the watchdog while it is being written.
526526 //
527527 // A thinking model is asked not to reason (withoutThinking, llama.cpp
528-// providers): its reasoning counts against maxTokens and is not in
528+// providers and Ollama), over what the sampling config says: its reasoning counts against maxTokens and is not in
529529 // resp.Text(), so it can spend the whole budget and leave no summary. On the
530530 // other providers that can still happen; emptySummary then says so.
531531 func (e *Engine) Summarize(ctx context.Context, messages []*ai.Message, maxTokens int) (string, error) {
@@ -525,7 +525,7 @@ func (e *Engine) Generate(ctx context.Context, messages []*ai.Message, tools []a
525 // the watchdog while it is being written.525 // the watchdog while it is being written.
526 //526 //
527 // A thinking model is asked not to reason (withoutThinking, llama.cpp527 // A thinking model is asked not to reason (withoutThinking, llama.cpp
528-// providers): its reasoning counts against maxTokens and is not in528+// providers and Ollama), over what the sampling config says: its reasoning counts against maxTokens and is not in
529 // resp.Text(), so it can spend the whole budget and leave no summary. On the529 // resp.Text(), so it can spend the whole budget and leave no summary. On the
530 // other providers that can still happen; emptySummary then says so.530 // other providers that can still happen; emptySummary then says so.
531 func (e *Engine) Summarize(ctx context.Context, messages []*ai.Message, maxTokens int) (string, error) {531 func (e *Engine) Summarize(ctx context.Context, messages []*ai.Message, maxTokens int) (string, error) {
modified internal/engine/summary_test.go +44 -7
@@ -72,6 +72,36 @@ func TestSummaryTurnsReasoningOff(t *testing.T) {
7272 }
7373 }
7474
75+// A sampling config that turns reasoning on still gets a summary without it,
76+// while the turns keep what the YAML says.
77+func TestSummaryTurnsReasoningOffOverSampling(t *testing.T) {
78+ srv, bodies := recordingLlama(t, goodAnswer)
79+ e := engineOn(t, srv.URL+"/v1")
80+ old := config.Cfg.Sampling
81+ t.Cleanup(func() { config.Cfg.Sampling = old })
82+ config.Cfg.Sampling = map[string]any{
83+ "chat_template_kwargs": map[string]any{"enable_thinking": true},
84+ }
85+
86+ if _, err := e.Summarize(context.Background(), summaryRequest(), 100); err != nil {
87+ t.Fatalf("Summarize: %v", err)
88+ }
89+ if _, _, err := e.Generate(context.Background(), []*ai.Message{ai.NewUserTextMessage("hi")}, nil); err != nil {
90+ t.Fatalf("Generate: %v", err)
91+ }
92+
93+ got := bodies()
94+ if len(got) != 2 {
95+ t.Fatalf("%d request(s), want 2", len(got))
96+ }
97+ if kw, _ := got[0]["chat_template_kwargs"].(map[string]any); kw["enable_thinking"] != false {
98+ t.Errorf("summary request: chat_template_kwargs = %v, want enable_thinking false", got[0]["chat_template_kwargs"])
99+ }
100+ if kw, _ := got[1]["chat_template_kwargs"].(map[string]any); kw["enable_thinking"] != true {
101+ t.Errorf("turn request: chat_template_kwargs = %v, want the YAML's enable_thinking true", got[1]["chat_template_kwargs"])
102+ }
103+}
104+
75105 // A model that reasons until max_tokens and writes nothing: the error says so
76106 // and names the setting — the session file only ever said "empty summary".
77107 func TestEmptySummarySaysWhy(t *testing.T) {
@@ -105,7 +135,7 @@ func TestEmptySummaryWithoutReasoning(t *testing.T) {
105135 }
106136
107137 // The middleware turns the reasoning off only for a provider that knows how,
108-// only under a marked context, and never over a request that chose for itself.
138+// only under a marked context — and then over whatever the request chose.
109139 func TestStreamOptionsWithoutThinking(t *testing.T) {
110140 send := func(ctx context.Context, llamacpp bool, body string) string {
111141 off := thinkingOffUnknown
@@ -136,15 +166,22 @@ func TestStreamOptionsWithoutThinking(t *testing.T) {
136166 if got := send(marked, false, `{"stream":true}`); strings.Contains(got, "chat_template_kwargs") {
137167 t.Errorf("another provider, marked: %s", got)
138168 }
139- chose := `{"stream":false,"chat_template_kwargs":{"enable_thinking":true}}`
140- if got := send(marked, true, chose); got != chose {
141- t.Errorf("a request that chose was changed: %s", got)
169+ chose := `{"stream":false,"chat_template_kwargs":{"enable_thinking":true,"custom":1}}`
170+ if got := send(marked, true, chose); !strings.Contains(got, `"enable_thinking":false`) || !strings.Contains(got, `"custom":1`) {
171+ t.Errorf("a request that chose to reason: %s", got)
172+ }
173+ effort := `{"stream":false,"chat_template_kwargs":{"reasoning_effort":"high"}}`
174+ if got := send(marked, true, effort); !strings.Contains(got, `"reasoning_effort":"low"`) {
175+ t.Errorf("a graded effort was not lowered: %s", got)
176+ }
177+ if got := send(context.Background(), true, effort); !strings.Contains(got, `"reasoning_effort":"high"`) {
178+ t.Errorf("not marked, the effort was changed: %s", got)
142179 }
143180 }
144181
145182 // Ollama ignores chat_template_kwargs (measured: qwen3.5:2b reasoned through
146183 // the whole budget) and obeys reasoning_effort "none": its provider must send
147-// that, and only that, under a marked context — and leave a request that chose.
184+// that, and only that, under a marked context — even over a request that chose.
148185 func TestStreamOptionsWithoutThinkingOllama(t *testing.T) {
149186 send := func(ctx context.Context, body string) string {
150187 req, _ := http.NewRequestWithContext(ctx, http.MethodPost, "http://x/v1/chat/completions", strings.NewReader(body))
@@ -165,8 +202,8 @@ func TestStreamOptionsWithoutThinkingOllama(t *testing.T) {
165202 t.Errorf("ollama, not marked: %s", got)
166203 }
167204 chose := `{"stream":false,"reasoning_effort":"high"}`
168- if got := send(marked, chose); got != chose {
169- t.Errorf("a request that chose was changed: %s", got)
205+ if got := send(marked, chose); !strings.Contains(got, `"reasoning_effort":"none"`) {
206+ t.Errorf("a request that chose to reason: %s", got)
170207 }
171208 }
172209
@@ -72,6 +72,36 @@ func TestSummaryTurnsReasoningOff(t *testing.T) {
72 }72 }
73 }73 }
74 74
75+// A sampling config that turns reasoning on still gets a summary without it,
76+// while the turns keep what the YAML says.
77+func TestSummaryTurnsReasoningOffOverSampling(t *testing.T) {
78+ srv, bodies := recordingLlama(t, goodAnswer)
79+ e := engineOn(t, srv.URL+"/v1")
80+ old := config.Cfg.Sampling
81+ t.Cleanup(func() { config.Cfg.Sampling = old })
82+ config.Cfg.Sampling = map[string]any{
83+ "chat_template_kwargs": map[string]any{"enable_thinking": true},
84+ }
85+
86+ if _, err := e.Summarize(context.Background(), summaryRequest(), 100); err != nil {
87+ t.Fatalf("Summarize: %v", err)
88+ }
89+ if _, _, err := e.Generate(context.Background(), []*ai.Message{ai.NewUserTextMessage("hi")}, nil); err != nil {
90+ t.Fatalf("Generate: %v", err)
91+ }
92+
93+ got := bodies()
94+ if len(got) != 2 {
95+ t.Fatalf("%d request(s), want 2", len(got))
96+ }
97+ if kw, _ := got[0]["chat_template_kwargs"].(map[string]any); kw["enable_thinking"] != false {
98+ t.Errorf("summary request: chat_template_kwargs = %v, want enable_thinking false", got[0]["chat_template_kwargs"])
99+ }
100+ if kw, _ := got[1]["chat_template_kwargs"].(map[string]any); kw["enable_thinking"] != true {
101+ t.Errorf("turn request: chat_template_kwargs = %v, want the YAML's enable_thinking true", got[1]["chat_template_kwargs"])
102+ }
103+}
104+
75 // A model that reasons until max_tokens and writes nothing: the error says so105 // A model that reasons until max_tokens and writes nothing: the error says so
76 // and names the setting — the session file only ever said "empty summary".106 // and names the setting — the session file only ever said "empty summary".
77 func TestEmptySummarySaysWhy(t *testing.T) {107 func TestEmptySummarySaysWhy(t *testing.T) {
@@ -105,7 +135,7 @@ func TestEmptySummaryWithoutReasoning(t *testing.T) {
105 }135 }
106 136
107 // The middleware turns the reasoning off only for a provider that knows how,137 // The middleware turns the reasoning off only for a provider that knows how,
108-// only under a marked context, and never over a request that chose for itself.138+// only under a marked context — and then over whatever the request chose.
109 func TestStreamOptionsWithoutThinking(t *testing.T) {139 func TestStreamOptionsWithoutThinking(t *testing.T) {
110 send := func(ctx context.Context, llamacpp bool, body string) string {140 send := func(ctx context.Context, llamacpp bool, body string) string {
111 off := thinkingOffUnknown141 off := thinkingOffUnknown
@@ -136,15 +166,22 @@ func TestStreamOptionsWithoutThinking(t *testing.T) {
136 if got := send(marked, false, `{"stream":true}`); strings.Contains(got, "chat_template_kwargs") {166 if got := send(marked, false, `{"stream":true}`); strings.Contains(got, "chat_template_kwargs") {
137 t.Errorf("another provider, marked: %s", got)167 t.Errorf("another provider, marked: %s", got)
138 }168 }
139- chose := `{"stream":false,"chat_template_kwargs":{"enable_thinking":true}}`169+ chose := `{"stream":false,"chat_template_kwargs":{"enable_thinking":true,"custom":1}}`
140- if got := send(marked, true, chose); got != chose {170+ if got := send(marked, true, chose); !strings.Contains(got, `"enable_thinking":false`) || !strings.Contains(got, `"custom":1`) {
141- t.Errorf("a request that chose was changed: %s", got)171+ t.Errorf("a request that chose to reason: %s", got)
172+ }
173+ effort := `{"stream":false,"chat_template_kwargs":{"reasoning_effort":"high"}}`
174+ if got := send(marked, true, effort); !strings.Contains(got, `"reasoning_effort":"low"`) {
175+ t.Errorf("a graded effort was not lowered: %s", got)
176+ }
177+ if got := send(context.Background(), true, effort); !strings.Contains(got, `"reasoning_effort":"high"`) {
178+ t.Errorf("not marked, the effort was changed: %s", got)
142 }179 }
143 }180 }
144 181
145 // Ollama ignores chat_template_kwargs (measured: qwen3.5:2b reasoned through182 // Ollama ignores chat_template_kwargs (measured: qwen3.5:2b reasoned through
146 // the whole budget) and obeys reasoning_effort "none": its provider must send183 // the whole budget) and obeys reasoning_effort "none": its provider must send
147-// that, and only that, under a marked context — and leave a request that chose.184+// that, and only that, under a marked context — even over a request that chose.
148 func TestStreamOptionsWithoutThinkingOllama(t *testing.T) {185 func TestStreamOptionsWithoutThinkingOllama(t *testing.T) {
149 send := func(ctx context.Context, body string) string {186 send := func(ctx context.Context, body string) string {
150 req, _ := http.NewRequestWithContext(ctx, http.MethodPost, "http://x/v1/chat/completions", strings.NewReader(body))187 req, _ := http.NewRequestWithContext(ctx, http.MethodPost, "http://x/v1/chat/completions", strings.NewReader(body))
@@ -165,8 +202,8 @@ func TestStreamOptionsWithoutThinkingOllama(t *testing.T) {
165 t.Errorf("ollama, not marked: %s", got)202 t.Errorf("ollama, not marked: %s", got)
166 }203 }
167 chose := `{"stream":false,"reasoning_effort":"high"}`204 chose := `{"stream":false,"reasoning_effort":"high"}`
168- if got := send(marked, chose); got != chose {205+ if got := send(marked, chose); !strings.Contains(got, `"reasoning_effort":"none"`) {
169- t.Errorf("a request that chose was changed: %s", got)206+ t.Errorf("a request that chose to reason: %s", got)
170 }207 }
171 }208 }
172 209
modified internal/engine/usage.go +25 -18
@@ -44,8 +44,8 @@ import (
4444 // {"enable_thinking": false}` the same 300 tokens were the summary. On a real
4545 // session (18k tokens of history, Qwen3.8-27B) the reasoning ate the whole
4646 // summaryMaxTokens and every compaction failed with "empty summary". Notes
47-// need no reasoning. It is added here, per request, rather than in the
48-// sampling config: the summary needs it whatever the YAML says, and the
47+// need no reasoning. It is forced here, per request, over the sampling
48+// config: the summary needs it whatever the YAML says, and the
4949 // field's name depends on the server. (Until Genkit 1.12 the sampling config
5050 // could not carry it at all — compat_oai mapped it onto
5151 // openai.ChatCompletionNewParams, which has no such field, and dropped it;
@@ -134,27 +134,34 @@ func withStreamOptions(body []byte, progress bool) []byte {
134134 }
135135
136136 // withoutReasoning returns body with the reasoning turned off the way `off`
137-// says, streamed or not. A request that already carries the field has chosen
138-// for itself and is left as is, like a body that is not JSON, or a server
139-// with no known way (thinkingOffUnknown).
137+// says, streamed or not, overriding whatever the sampling config chose: the
138+// summary request clones it, and a summary that reasons can spend its whole
139+// budget thinking. Other template variables are kept. A body that is not
140+// JSON, or a server with no known way (thinkingOffUnknown), is left as is.
140141 func withoutReasoning(body []byte, off thinkingOff) []byte {
141- key, value := "", ""
142- switch off {
143- case thinkingOffTemplate:
144- key, value = "chat_template_kwargs", `{"enable_thinking":false}`
145- case thinkingOffEffort:
146- key, value = "reasoning_effort", `"none"`
147- default:
148- return body
149- }
150142 var m map[string]json.RawMessage
151- if json.Unmarshal(body, &m) != nil {
143+ if off == thinkingOffUnknown || json.Unmarshal(body, &m) != nil {
152144 return body
153145 }
154- if _, set := m[key]; set {
155- return body
146+ switch off {
147+ case thinkingOffTemplate:
148+ var kw map[string]any
149+ if json.Unmarshal(m["chat_template_kwargs"], &kw) != nil || kw == nil {
150+ kw = map[string]any{}
151+ }
152+ kw["enable_thinking"] = false
153+ // gpt-oss's template has no "off": low is the least it reasons.
154+ if _, set := kw["reasoning_effort"]; set {
155+ kw["reasoning_effort"] = "low"
156+ }
157+ raw, err := json.Marshal(kw)
158+ if err != nil {
159+ return body
160+ }
161+ m["chat_template_kwargs"] = raw
162+ case thinkingOffEffort:
163+ m["reasoning_effort"] = json.RawMessage(`"none"`)
156164 }
157- m[key] = json.RawMessage(value)
158165 out, err := json.Marshal(m)
159166 if err != nil {
160167 return body
@@ -44,8 +44,8 @@ import (
44 // {"enable_thinking": false}` the same 300 tokens were the summary. On a real44 // {"enable_thinking": false}` the same 300 tokens were the summary. On a real
45 // session (18k tokens of history, Qwen3.8-27B) the reasoning ate the whole45 // session (18k tokens of history, Qwen3.8-27B) the reasoning ate the whole
46 // summaryMaxTokens and every compaction failed with "empty summary". Notes46 // summaryMaxTokens and every compaction failed with "empty summary". Notes
47-// need no reasoning. It is added here, per request, rather than in the47+// need no reasoning. It is forced here, per request, over the sampling
48-// sampling config: the summary needs it whatever the YAML says, and the48+// config: the summary needs it whatever the YAML says, and the
49 // field's name depends on the server. (Until Genkit 1.12 the sampling config49 // field's name depends on the server. (Until Genkit 1.12 the sampling config
50 // could not carry it at all — compat_oai mapped it onto50 // could not carry it at all — compat_oai mapped it onto
51 // openai.ChatCompletionNewParams, which has no such field, and dropped it;51 // openai.ChatCompletionNewParams, which has no such field, and dropped it;
@@ -134,27 +134,34 @@ func withStreamOptions(body []byte, progress bool) []byte {
134 }134 }
135 135
136 // withoutReasoning returns body with the reasoning turned off the way `off`136 // withoutReasoning returns body with the reasoning turned off the way `off`
137-// says, streamed or not. A request that already carries the field has chosen137+// says, streamed or not, overriding whatever the sampling config chose: the
138-// for itself and is left as is, like a body that is not JSON, or a server138+// summary request clones it, and a summary that reasons can spend its whole
139-// with no known way (thinkingOffUnknown).139+// budget thinking. Other template variables are kept. A body that is not
140+// JSON, or a server with no known way (thinkingOffUnknown), is left as is.
140 func withoutReasoning(body []byte, off thinkingOff) []byte {141 func withoutReasoning(body []byte, off thinkingOff) []byte {
141- key, value := "", ""
142- switch off {
143- case thinkingOffTemplate:
144- key, value = "chat_template_kwargs", `{"enable_thinking":false}`
145- case thinkingOffEffort:
146- key, value = "reasoning_effort", `"none"`
147- default:
148- return body
149- }
150 var m map[string]json.RawMessage142 var m map[string]json.RawMessage
151- if json.Unmarshal(body, &m) != nil {143+ if off == thinkingOffUnknown || json.Unmarshal(body, &m) != nil {
152 return body144 return body
153 }145 }
154- if _, set := m[key]; set {146+ switch off {
155- return body147+ case thinkingOffTemplate:
148+ var kw map[string]any
149+ if json.Unmarshal(m["chat_template_kwargs"], &kw) != nil || kw == nil {
150+ kw = map[string]any{}
151+ }
152+ kw["enable_thinking"] = false
153+ // gpt-oss's template has no "off": low is the least it reasons.
154+ if _, set := kw["reasoning_effort"]; set {
155+ kw["reasoning_effort"] = "low"
156+ }
157+ raw, err := json.Marshal(kw)
158+ if err != nil {
159+ return body
160+ }
161+ m["chat_template_kwargs"] = raw
162+ case thinkingOffEffort:
163+ m["reasoning_effort"] = json.RawMessage(`"none"`)
156 }164 }
157- m[key] = json.RawMessage(value)
158 out, err := json.Marshal(m)165 out, err := json.Marshal(m)
159 if err != nil {166 if err != nil {
160 return body167 return body