🛟 Updated.Unverified
30a5de4 parent: 5f0c1e0 modified
demo-llmman/agent.llmman.yaml +1 -1 | @@ -192,7 +192,7 @@ sampling: | ||
| 192 | 192 | top_p: 0.9 |
| 193 | 193 | max_tokens: 4096 |
| 194 | 194 | chat_template_kwargs: |
| 195 | - enable_thinking: true | |
| 195 | + enable_thinking: false | |
| 196 | 196 | |
| 197 | 197 | # Silence allowed between two tokens once the answer has started. |
| 198 | 198 | watchdogTimeout: 3m |
| @@ -192,7 +192,7 @@ sampling: | |||
| 192 | top_p: 0.9 | 192 | top_p: 0.9 |
| 193 | max_tokens: 4096 | 193 | max_tokens: 4096 |
| 194 | chat_template_kwargs: | 194 | chat_template_kwargs: |
| 195 | - enable_thinking: true | 195 | + 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: 3m | 198 | 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 | ||
| 44 | 44 | |
| 45 | 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 | 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 | 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 | 66 | ## See also |
| 51 | 67 | |
| @@ -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 also | 66 | ## See also |
| 51 | 67 | ||
modified
docs/en/reference/configuration.md +1 -1 | @@ -43,7 +43,7 @@ | ||
| 43 | 43 | | `threshold` | int | `75` | Percentage of the context window beyond which the history is compressed before the next question. Between 1 and 100. | |
| 44 | 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 | 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 | 47 | | `prompt` | string | `""` | Replaces the built-in summary prompt when not empty. | |
| 48 | 48 | | `showStats` | bool | `true` | Print the one-line `🗜️` report after each compression. | |
| 49 | 49 | |
| @@ -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 | ||
| 44 | 44 | |
| 45 | 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 | 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 | 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 | 66 | ## Voir aussi |
| 51 | 67 | |
| @@ -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 aussi | 66 | ## Voir aussi |
| 51 | 67 | ||
modified
docs/fr/reference/configuration.md +1 -1 | @@ -43,7 +43,7 @@ | ||
| 43 | 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 | 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 | 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 | 47 | | `prompt` | chaîne | `""` | Remplace le prompt de résumé intégré quand non vide. | |
| 48 | 48 | | `showStats` | booléen | `true` | Affiche le rapport `🗜️` d'une ligne après chaque compression. | |
| 49 | 49 | |
| @@ -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 | ||
| 525 | 525 | // the watchdog while it is being written. |
| 526 | 526 | // |
| 527 | 527 | // 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 | |
| 529 | 529 | // resp.Text(), so it can spend the whole budget and leave no summary. On the |
| 530 | 530 | // other providers that can still happen; emptySummary then says so. |
| 531 | 531 | 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.cpp | 527 | // 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 |
| 529 | // resp.Text(), so it can spend the whole budget and leave no summary. On the | 529 | // 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) { | ||
| 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 | 105 | // A model that reasons until max_tokens and writes nothing: the error says so |
| 76 | 106 | // and names the setting — the session file only ever said "empty summary". |
| 77 | 107 | func TestEmptySummarySaysWhy(t *testing.T) { |
| @@ -105,7 +135,7 @@ func TestEmptySummaryWithoutReasoning(t *testing.T) { | ||
| 105 | 135 | } |
| 106 | 136 | |
| 107 | 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 | 139 | func TestStreamOptionsWithoutThinking(t *testing.T) { |
| 110 | 140 | send := func(ctx context.Context, llamacpp bool, body string) string { |
| 111 | 141 | off := thinkingOffUnknown |
| @@ -136,15 +166,22 @@ func TestStreamOptionsWithoutThinking(t *testing.T) { | ||
| 136 | 166 | if got := send(marked, false, `{"stream":true}`); strings.Contains(got, "chat_template_kwargs") { |
| 137 | 167 | t.Errorf("another provider, marked: %s", got) |
| 138 | 168 | } |
| 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) | |
| 142 | 179 | } |
| 143 | 180 | } |
| 144 | 181 | |
| 145 | 182 | // Ollama ignores chat_template_kwargs (measured: qwen3.5:2b reasoned through |
| 146 | 183 | // 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 | 185 | func TestStreamOptionsWithoutThinkingOllama(t *testing.T) { |
| 149 | 186 | send := func(ctx context.Context, body string) string { |
| 150 | 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 | 202 | t.Errorf("ollama, not marked: %s", got) |
| 166 | 203 | } |
| 167 | 204 | 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) | |
| 170 | 207 | } |
| 171 | 208 | } |
| 172 | 209 | |
| @@ -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 so | 105 | // 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 := thinkingOffUnknown | 141 | 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 through | 182 | // Ollama ignores chat_template_kwargs (measured: qwen3.5:2b reasoned through |
| 146 | // the whole budget) and obeys reasoning_effort "none": its provider must send | 183 | // 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 ( | ||
| 44 | 44 | // {"enable_thinking": false}` the same 300 tokens were the summary. On a real |
| 45 | 45 | // session (18k tokens of history, Qwen3.8-27B) the reasoning ate the whole |
| 46 | 46 | // 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 | |
| 49 | 49 | // field's name depends on the server. (Until Genkit 1.12 the sampling config |
| 50 | 50 | // could not carry it at all — compat_oai mapped it onto |
| 51 | 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 | 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 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. | |
| 140 | 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 | 142 | var m map[string]json.RawMessage |
| 151 | - if json.Unmarshal(body, &m) != nil { | |
| 143 | + if off == thinkingOffUnknown || json.Unmarshal(body, &m) != nil { | |
| 152 | 144 | return body |
| 153 | 145 | } |
| 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"`) | |
| 156 | 164 | } |
| 157 | - m[key] = json.RawMessage(value) | |
| 158 | 165 | out, err := json.Marshal(m) |
| 159 | 166 | if err != nil { |
| 160 | 167 | return body |
| @@ -44,8 +44,8 @@ import ( | |||
| 44 | // {"enable_thinking": false}` the same 300 tokens were the summary. On a real | 44 | // {"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 whole | 45 | // session (18k tokens of history, Qwen3.8-27B) the reasoning ate the whole |
| 46 | // summaryMaxTokens and every compaction failed with "empty summary". Notes | 46 | // summaryMaxTokens and every compaction failed with "empty summary". Notes |
| 47 | -// need no reasoning. It is added here, per request, rather than in the | 47 | +// need no reasoning. It is forced here, per request, over the sampling |
| 48 | -// sampling config: the summary needs it whatever the YAML says, and the | 48 | +// 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 config | 49 | // 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 onto | 50 | // 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 chosen | 137 | +// 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 server | 138 | +// 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.RawMessage | 142 | var m map[string]json.RawMessage |
| 151 | - if json.Unmarshal(body, &m) != nil { | 143 | + if off == thinkingOffUnknown || json.Unmarshal(body, &m) != nil { |
| 152 | return body | 144 | return body |
| 153 | } | 145 | } |
| 154 | - if _, set := m[key]; set { | 146 | + switch off { |
| 155 | - return body | 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"`) | ||
| 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 body | 167 | return body |