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

k33g committed 2026-10-22T20:05:29+02:00 Browse files
b2ee679 parent: d660856
deleted .tickets/issues/0121-mm-init-add-the-urls-in-the-agent-yaml-file.yaml +0 -14
deleted file mode 100644
@@ -1,14 +0,0 @@
1-# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2-id: 121
3-title: mm init, add the urls in the agent.yaml file
4-state: open
5-author:
6- name: k33g
7- email: ph.charriere@gmail.com
8-createdAt: 2026-10-22T03:55:46.925Z
9-updatedAt: 2026-10-22T03:55:46.925Z
10-labels: []
11-milestone: v1.0.0
12-body: ""
13-tasks: []
14-comments: []
deleted file mode 100644
@@ -1,14 +0,0 @@
1-# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2-id: 121
3-title: mm init, add the urls in the agent.yaml file
4-state: open
5-author:
6- name: k33g
7- email: ph.charriere@gmail.com
8-createdAt: 2026-10-22T03:55:46.925Z
9-updatedAt: 2026-10-22T03:55:46.925Z
10-labels: []
11-milestone: v1.0.0
12-body: ""
13-tasks: []
14-comments: []
added .tickets/issues/0121-mm-init-some-changes.yaml +62 -0
new file mode 100644
@@ -0,0 +1,62 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 121
3+title: mm init - some changes
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-10-22T03:55:46.925Z
9+updatedAt: 2026-10-23T00:10:00.000Z
10+labels: []
11+milestone: v1.0.0
12+body: |+
13+ ### add the urls in the agent.yaml file
14+
15+ When you use the default url for a provider
16+ add the urls in comment for memo (baseUrl and fallback)
17+
18+ ### more informations
19+
20+ Somebody told me this about the init feature:
21+
22+ One risk with tuned configs: models get deprecated and nobody remembers why agent.yaml holds those values. Have mm init write a comment per section with its assumptions: model family, context size, machine. Future you diffs assumptions instead of guessing. Hit this myself recently.
23+
24+ I answered that's already implemented by showing this https://rickub.com/bots-garden/one-day-one-feature/blob/main/01-init/agent.yaml
25+
26+ The person answered:
27+ Yes, exactly this. Two additions from config rot: mark which numbers you measured versus copied from the card, so a quant bump tells you what to re-derive. And note at the model line what couples to it: swap the family and sampling, max_tokens and contextWindow need redoing together.
28+
29+ Do you understand ?
30+
31+ Does it make sense to implement new things ?
32+
33+
34+
35+tasks: []
36+comments:
37+ - id: 1
38+ author:
39+ name: k33g
40+ email: ph.charriere@gmail.com
41+ createdAt: 2026-10-22T23:50:00.000Z
42+ body: |
43+ Done (2026-10-22), uncommitted on feature/open-terminal. Yes, the two
44+ suggestions make sense; both are implemented, plus the URLs.
45+
46+ - URLs: without --base-url, the file records the provider's default
47+ baseUrl and fallback as comments (engine.DefaultURLs, new), ready to
48+ uncomment; llamacpp/openai say they have no fallback.
49+ - Measured vs copied: presets.yaml families get `origin` per sampling
50+ block (measured | card | rule, from each source text); every number about
51+ the model is tagged in its comment: [measured] [card] [server] (GGUF
52+ sampling) [guide] (machine row) [rule] (max_tokens, the 0.6 floor). The
53+ header explains the tags the file uses.
54+ - Coupling: a comment above `model:` lists the settings chosen for it
55+ (sampling keys, chat_template_kwargs) to redo together on a change.
56+ - Assumptions: a `model facts` header line keeps what the server said
57+ (native window, GGUF sampling, reasoning switch + default, or none).
58+ - Tests: engine TestDefaultURLs; agentinit origin check in presets test,
59+ TestRenderRecordsTheDefaultURLs, TestRenderTagsWhereTheNumbersComeFrom,
60+ TestRenderModelCouplingAndFacts; init window test screen 40 → 60 rows
61+ (the header is longer). Live: mm init --check on llmman, file loads, model
62+ answers. Docs EN/FR: cli reference (mm init rows), create-an-agent how-to.
new file mode 100644
@@ -0,0 +1,62 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 121
3+title: mm init - some changes
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-10-22T03:55:46.925Z
9+updatedAt: 2026-10-23T00:10:00.000Z
10+labels: []
11+milestone: v1.0.0
12+body: |+
13+ ### add the urls in the agent.yaml file
14+
15+ When you use the default url for a provider
16+ add the urls in comment for memo (baseUrl and fallback)
17+
18+ ### more informations
19+
20+ Somebody told me this about the init feature:
21+
22+ One risk with tuned configs: models get deprecated and nobody remembers why agent.yaml holds those values. Have mm init write a comment per section with its assumptions: model family, context size, machine. Future you diffs assumptions instead of guessing. Hit this myself recently.
23+
24+ I answered that's already implemented by showing this https://rickub.com/bots-garden/one-day-one-feature/blob/main/01-init/agent.yaml
25+
26+ The person answered:
27+ Yes, exactly this. Two additions from config rot: mark which numbers you measured versus copied from the card, so a quant bump tells you what to re-derive. And note at the model line what couples to it: swap the family and sampling, max_tokens and contextWindow need redoing together.
28+
29+ Do you understand ?
30+
31+ Does it make sense to implement new things ?
32+
33+
34+
35+tasks: []
36+comments:
37+ - id: 1
38+ author:
39+ name: k33g
40+ email: ph.charriere@gmail.com
41+ createdAt: 2026-10-22T23:50:00.000Z
42+ body: |
43+ Done (2026-10-22), uncommitted on feature/open-terminal. Yes, the two
44+ suggestions make sense; both are implemented, plus the URLs.
45+
46+ - URLs: without --base-url, the file records the provider's default
47+ baseUrl and fallback as comments (engine.DefaultURLs, new), ready to
48+ uncomment; llamacpp/openai say they have no fallback.
49+ - Measured vs copied: presets.yaml families get `origin` per sampling
50+ block (measured | card | rule, from each source text); every number about
51+ the model is tagged in its comment: [measured] [card] [server] (GGUF
52+ sampling) [guide] (machine row) [rule] (max_tokens, the 0.6 floor). The
53+ header explains the tags the file uses.
54+ - Coupling: a comment above `model:` lists the settings chosen for it
55+ (sampling keys, chat_template_kwargs) to redo together on a change.
56+ - Assumptions: a `model facts` header line keeps what the server said
57+ (native window, GGUF sampling, reasoning switch + default, or none).
58+ - Tests: engine TestDefaultURLs; agentinit origin check in presets test,
59+ TestRenderRecordsTheDefaultURLs, TestRenderTagsWhereTheNumbersComeFrom,
60+ TestRenderModelCouplingAndFacts; init window test screen 40 → 60 rows
61+ (the header is longer). Live: mm init --check on llmman, file loads, model
62+ answers. Docs EN/FR: cli reference (mm init rows), create-an-agent how-to.
modified docs/en/how-to/create-an-agent.md +4 -2
@@ -46,7 +46,7 @@ This guide shows how to write a new agent.yaml for a model and a job, with mm's
4646 │ │ skillsDir: no-skills
4747 │ │ # usage docs
4848 │ │ maxTurns: 8
49- │ │ # settings guide, row "Small model (≤ 8B), short tasks"
49+ │ │ # [guide] settings guide, row "Small model (≤ 8B), short t…
5050 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
5151 ↑↓ choose · Enter next · Esc back · PgUp/PgDn preview · Ctrl-C quit
5252 ```
@@ -65,6 +65,8 @@ This guide shows how to write a new agent.yaml for a model and a job, with mm's
6565
6666 4. Read the file: each value is preceded by its reason — the GGUF's own sampling, a model card, a measured demo of this repository, the settings guide's row for your machine. Change what does not fit as you watch the agent work.
6767
68+ The numbers about the model carry a tag that says how they were obtained, explained at the top of the file: `[measured]`, `[card]`, `[server]`, `[guide]` or `[rule]`. The comment above `model:` lists the settings chosen for that model. When you change the model, or only its quantization, redo those together: re-measure the `[measured]` numbers, re-read the card for the `[card]` ones — or run `mm init` again and compare. The header's `model facts` line keeps what the server said about the model when the file was written.
69+
6870 ## Variants
6971
7072 - **Prove the model answers.** Add `--check`: after writing, mm asks the model "Say hello in one line." with the new file and says whether it answered. The first answer loads the model, which can take a while.
@@ -77,7 +79,7 @@ This guide shows how to write a new agent.yaml for a model and a job, with mm's
7779 ```
7880
7981 - **See the file without writing it.** `-o -` prints it on stdout.
80-- **A server elsewhere.** `--base-url http://192.168.1.20:17434/v1`; it is written into the file as `baseUrl`.
82+- **A server elsewhere.** `--base-url http://192.168.1.20:17434/v1`; it is written into the file as `baseUrl`. Without it, the file has no `baseUrl` and records the provider's default address and fallback as comments, ready to uncomment.
8183 - **A server that does not answer.** mm says so and asks for the model's name: the settings then come from the family table only.
8284 - **An existing file.** mm never overwrites it, and says so before asking anything: choose another name with `-o`.
8385 - **A JSON agent.** `--usage json` writes a `structuredOutput` example (Go types) to replace with yours, and turns every tool off, since a structured answer forbids tools.
@@ -46,7 +46,7 @@ This guide shows how to write a new agent.yaml for a model and a job, with mm's
46 │ │ skillsDir: no-skills46 │ │ skillsDir: no-skills
47 │ │ # usage docs47 │ │ # usage docs
48 │ │ maxTurns: 848 │ │ maxTurns: 8
49- │ │ # settings guide, row "Small model (≤ 8B), short tasks"49+ │ │ # [guide] settings guide, row "Small model (≤ 8B), short t…
50 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────50 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
51 ↑↓ choose · Enter next · Esc back · PgUp/PgDn preview · Ctrl-C quit51 ↑↓ choose · Enter next · Esc back · PgUp/PgDn preview · Ctrl-C quit
52 ```52 ```
@@ -65,6 +65,8 @@ This guide shows how to write a new agent.yaml for a model and a job, with mm's
65 65
66 4. Read the file: each value is preceded by its reason — the GGUF's own sampling, a model card, a measured demo of this repository, the settings guide's row for your machine. Change what does not fit as you watch the agent work.66 4. Read the file: each value is preceded by its reason — the GGUF's own sampling, a model card, a measured demo of this repository, the settings guide's row for your machine. Change what does not fit as you watch the agent work.
67 67
68+ The numbers about the model carry a tag that says how they were obtained, explained at the top of the file: `[measured]`, `[card]`, `[server]`, `[guide]` or `[rule]`. The comment above `model:` lists the settings chosen for that model. When you change the model, or only its quantization, redo those together: re-measure the `[measured]` numbers, re-read the card for the `[card]` ones — or run `mm init` again and compare. The header's `model facts` line keeps what the server said about the model when the file was written.
69+
68 ## Variants70 ## Variants
69 71
70 - **Prove the model answers.** Add `--check`: after writing, mm asks the model "Say hello in one line." with the new file and says whether it answered. The first answer loads the model, which can take a while.72 - **Prove the model answers.** Add `--check`: after writing, mm asks the model "Say hello in one line." with the new file and says whether it answered. The first answer loads the model, which can take a while.
@@ -77,7 +79,7 @@ This guide shows how to write a new agent.yaml for a model and a job, with mm's
77 ```79 ```
78 80
79 - **See the file without writing it.** `-o -` prints it on stdout.81 - **See the file without writing it.** `-o -` prints it on stdout.
80-- **A server elsewhere.** `--base-url http://192.168.1.20:17434/v1`; it is written into the file as `baseUrl`.82+- **A server elsewhere.** `--base-url http://192.168.1.20:17434/v1`; it is written into the file as `baseUrl`. Without it, the file has no `baseUrl` and records the provider's default address and fallback as comments, ready to uncomment.
81 - **A server that does not answer.** mm says so and asks for the model's name: the settings then come from the family table only.83 - **A server that does not answer.** mm says so and asks for the model's name: the settings then come from the family table only.
82 - **An existing file.** mm never overwrites it, and says so before asking anything: choose another name with `-o`.84 - **An existing file.** mm never overwrites it, and says so before asking anything: choose another name with `-o`.
83 - **A JSON agent.** `--usage json` writes a `structuredOutput` example (Go types) to replace with yours, and turns every tool off, since a structured answer forbids tools.85 - **A JSON agent.** `--usage json` writes a `structuredOutput` example (Go types) to replace with yours, and turns every tool off, since a structured answer forbids tools.
modified docs/en/reference/cli.md +4 -3
@@ -175,12 +175,13 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/
175175
176176 | Aspect | Behaviour |
177177 |--------|-----------|
178-| Window | When stdin and stdout are terminals (and neither `--yes` nor `--line`): a full-screen window — steps (✓ answered, ▸ current, · to come, each answer beside its step), the question with the highlighted choice's description, and the file as it would be written with that choice (preview beside the question from 100 columns, in its place with `Tab` below). Keys: `↑` `↓` `Home` `End` choose, `Enter` answers, `y` / `n` answer the yes-or-no question, `Esc` takes the previous answer back (answers given by flags are skipped), `PgUp` `PgDn` scroll the preview, `Ctrl-U` clears a text answer, `Ctrl-C` quits without writing (`nothing written`, exit 1). A refused answer shows on the bottom rule. Once every question is answered, `Enter` writes the file. |
178+| Window | When stdin and stdout are terminals (and neither `--yes` nor `--line`): a full-screen window — steps (✓ answered, ▸ current, · to come, each answer beside its step), the question with the highlighted choice's description (models are listed by their short name, as in the window's header — `Qwen3.5-4B:Q4_K_M` — and the highlighted model's full identifier is shown under the list), and the file as it would be written with that choice (preview beside the question from 100 columns, in its place with `Tab` below). Keys: `↑` `↓` `Home` `End` choose, `Enter` answers, `y` / `n` answer the yes-or-no question, `Esc` takes the previous answer back (answers given by flags are skipped), `PgUp` `PgDn` scroll the preview, `Ctrl-U` clears a text answer, `Ctrl-C` quits without writing (`nothing written`, exit 1). A refused answer shows on the bottom rule. Once every question is answered, `Enter` writes the file. |
179179 | Line mode | Otherwise: one question per screen line, numbered choices; a number, an option's text or another text answers; an empty line or `/skip` takes the default. Usages and machines are listed with a one-line description first. |
180180 | Model facts | llmman and Ollama: `POST /api/show` — the native window, the GGUF's sampling (`general.sampling.*`, Ollama's Modelfile parameters), the chat template's reasoning switch (`enable_thinking`, `reasoning_effort`) and its default. llama-server: `GET /props` — `n_ctx` and the chat template. Docker Model Runner and `openai`: none — the family table decides; `openai` lists its models with `OPENAI_API_KEY`. |
181-| Where each value comes from | The server's facts, then the family table, then the usage, then the machine row; the table is `internal/agentinit/presets.yaml`, each entry with its source. |
181+| Where each value comes from | The server's facts, then the family table, then the usage, then the machine row; the table is `internal/agentinit/presets.yaml`, each entry with its source and, per sampling block, its `origin` (`measured`, `card` or `rule`). |
182+| Origin tags | The comment of a number about the model starts with a tag: `[measured]` (a demo of this repository ran with it), `[card]` (model card, GGUF or API reference, not measured), `[server]` (read from the server: the GGUF's sampling), `[guide]` (the settings guide's machine row: `maxOutput`, the timeouts), `[rule]` (a rule of thumb: `max_tokens`, the 0.6 floor with reasoning, a family without published values). Values that follow from the answers or mm's defaults have no tag. The header explains the tags the file uses, and only those. |
182183 | Sampling | Reasoning on (the usage asks for it and the model has a switch): the family's thinking sampling, never a temperature under 0.6, `max_tokens: 16384`. Reasoning off: the family's general sampling, or its precise temperature for tool-heavy usages, `max_tokens: 4096`. The GGUF's own sampling replaces the family's. `parallel_tool_calls: false` when any tool is on. |
183-| Written file | Every value preceded by a comment saying why it has that value; `contextWindow: 0` (mm asks the server), with the native window in its comment; `context.threshold: 75`; the usage's English system prompt; for `docs` a `docSearch` block, for `json` a `structuredOutput` example and every tool off, for `coordinator` a commented `mmConfigs` example. |
184+| Written file | A header with the family, the usage, the machine, `model facts` (what the server said: native window, GGUF sampling, reasoning switch and its default — or `none`) and the origin legend; above `model:`, the settings chosen for that model (sampling keys, `chat_template_kwargs`) to redo together when it changes; without `--base-url`, the provider's default `baseUrl` and `fallback` as comments; every value preceded by a comment saying why it has that value; `contextWindow: 0` (mm asks the server), with the native window in its comment; `context.threshold: 75`; the usage's English system prompt; for `docs` a `docSearch` block, for `json` a `structuredOutput` example and every tool off, for `coordinator` a commented `mmConfigs` example. |
184185 | Existing file | Never overwritten: `⚠️ <file> already exists — nothing was written; choose another name with -o`, exit 1, before any question. |
185186 | After writing | The file is loaded as mm loads a configuration; an error is printed and the exit code is 1. |
186187 | Exit codes | 0 written (and checked); 1 an unknown provider, usage or machine, no model, an existing file, a file mm does not load, a failed check; 2 a bad flag. |
@@ -175,12 +175,13 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/
175 175
176 | Aspect | Behaviour |176 | Aspect | Behaviour |
177 |--------|-----------|177 |--------|-----------|
178-| Window | When stdin and stdout are terminals (and neither `--yes` nor `--line`): a full-screen window — steps (✓ answered, ▸ current, · to come, each answer beside its step), the question with the highlighted choice's description, and the file as it would be written with that choice (preview beside the question from 100 columns, in its place with `Tab` below). Keys: `↑` `↓` `Home` `End` choose, `Enter` answers, `y` / `n` answer the yes-or-no question, `Esc` takes the previous answer back (answers given by flags are skipped), `PgUp` `PgDn` scroll the preview, `Ctrl-U` clears a text answer, `Ctrl-C` quits without writing (`nothing written`, exit 1). A refused answer shows on the bottom rule. Once every question is answered, `Enter` writes the file. |178+| Window | When stdin and stdout are terminals (and neither `--yes` nor `--line`): a full-screen window — steps (✓ answered, ▸ current, · to come, each answer beside its step), the question with the highlighted choice's description (models are listed by their short name, as in the window's header — `Qwen3.5-4B:Q4_K_M` — and the highlighted model's full identifier is shown under the list), and the file as it would be written with that choice (preview beside the question from 100 columns, in its place with `Tab` below). Keys: `↑` `↓` `Home` `End` choose, `Enter` answers, `y` / `n` answer the yes-or-no question, `Esc` takes the previous answer back (answers given by flags are skipped), `PgUp` `PgDn` scroll the preview, `Ctrl-U` clears a text answer, `Ctrl-C` quits without writing (`nothing written`, exit 1). A refused answer shows on the bottom rule. Once every question is answered, `Enter` writes the file. |
179 | Line mode | Otherwise: one question per screen line, numbered choices; a number, an option's text or another text answers; an empty line or `/skip` takes the default. Usages and machines are listed with a one-line description first. |179 | Line mode | Otherwise: one question per screen line, numbered choices; a number, an option's text or another text answers; an empty line or `/skip` takes the default. Usages and machines are listed with a one-line description first. |
180 | Model facts | llmman and Ollama: `POST /api/show` — the native window, the GGUF's sampling (`general.sampling.*`, Ollama's Modelfile parameters), the chat template's reasoning switch (`enable_thinking`, `reasoning_effort`) and its default. llama-server: `GET /props` — `n_ctx` and the chat template. Docker Model Runner and `openai`: none — the family table decides; `openai` lists its models with `OPENAI_API_KEY`. |180 | Model facts | llmman and Ollama: `POST /api/show` — the native window, the GGUF's sampling (`general.sampling.*`, Ollama's Modelfile parameters), the chat template's reasoning switch (`enable_thinking`, `reasoning_effort`) and its default. llama-server: `GET /props` — `n_ctx` and the chat template. Docker Model Runner and `openai`: none — the family table decides; `openai` lists its models with `OPENAI_API_KEY`. |
181-| Where each value comes from | The server's facts, then the family table, then the usage, then the machine row; the table is `internal/agentinit/presets.yaml`, each entry with its source. |181+| Where each value comes from | The server's facts, then the family table, then the usage, then the machine row; the table is `internal/agentinit/presets.yaml`, each entry with its source and, per sampling block, its `origin` (`measured`, `card` or `rule`). |
182+| Origin tags | The comment of a number about the model starts with a tag: `[measured]` (a demo of this repository ran with it), `[card]` (model card, GGUF or API reference, not measured), `[server]` (read from the server: the GGUF's sampling), `[guide]` (the settings guide's machine row: `maxOutput`, the timeouts), `[rule]` (a rule of thumb: `max_tokens`, the 0.6 floor with reasoning, a family without published values). Values that follow from the answers or mm's defaults have no tag. The header explains the tags the file uses, and only those. |
182 | Sampling | Reasoning on (the usage asks for it and the model has a switch): the family's thinking sampling, never a temperature under 0.6, `max_tokens: 16384`. Reasoning off: the family's general sampling, or its precise temperature for tool-heavy usages, `max_tokens: 4096`. The GGUF's own sampling replaces the family's. `parallel_tool_calls: false` when any tool is on. |183 | Sampling | Reasoning on (the usage asks for it and the model has a switch): the family's thinking sampling, never a temperature under 0.6, `max_tokens: 16384`. Reasoning off: the family's general sampling, or its precise temperature for tool-heavy usages, `max_tokens: 4096`. The GGUF's own sampling replaces the family's. `parallel_tool_calls: false` when any tool is on. |
183-| Written file | Every value preceded by a comment saying why it has that value; `contextWindow: 0` (mm asks the server), with the native window in its comment; `context.threshold: 75`; the usage's English system prompt; for `docs` a `docSearch` block, for `json` a `structuredOutput` example and every tool off, for `coordinator` a commented `mmConfigs` example. |184+| Written file | A header with the family, the usage, the machine, `model facts` (what the server said: native window, GGUF sampling, reasoning switch and its default — or `none`) and the origin legend; above `model:`, the settings chosen for that model (sampling keys, `chat_template_kwargs`) to redo together when it changes; without `--base-url`, the provider's default `baseUrl` and `fallback` as comments; every value preceded by a comment saying why it has that value; `contextWindow: 0` (mm asks the server), with the native window in its comment; `context.threshold: 75`; the usage's English system prompt; for `docs` a `docSearch` block, for `json` a `structuredOutput` example and every tool off, for `coordinator` a commented `mmConfigs` example. |
184 | Existing file | Never overwritten: `⚠️ <file> already exists — nothing was written; choose another name with -o`, exit 1, before any question. |185 | Existing file | Never overwritten: `⚠️ <file> already exists — nothing was written; choose another name with -o`, exit 1, before any question. |
185 | After writing | The file is loaded as mm loads a configuration; an error is printed and the exit code is 1. |186 | After writing | The file is loaded as mm loads a configuration; an error is printed and the exit code is 1. |
186 | Exit codes | 0 written (and checked); 1 an unknown provider, usage or machine, no model, an existing file, a file mm does not load, a failed check; 2 a bad flag. |187 | Exit codes | 0 written (and checked); 1 an unknown provider, usage or machine, no model, an existing file, a file mm does not load, a failed check; 2 a bad flag. |
modified docs/fr/how-to/create-an-agent.md +4 -2
@@ -46,7 +46,7 @@ Ce guide montre comment écrire un nouvel agent.yaml pour un modèle et une tâc
4646 │ │ skillsDir: no-skills
4747 │ │ # usage docs
4848 │ │ maxTurns: 8
49- │ │ # settings guide, row "Small model (≤ 8B), short tasks"
49+ │ │ # [guide] settings guide, row "Small model (≤ 8B), short t…
5050 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
5151 ↑↓ choose · Enter next · Esc back · PgUp/PgDn preview · Ctrl-C quit
5252 ```
@@ -65,6 +65,8 @@ Ce guide montre comment écrire un nouvel agent.yaml pour un modèle et une tâc
6565
6666 4. Lisez le fichier : chaque valeur est précédée de sa raison — l'échantillonnage du GGUF lui-même, une carte de modèle, une démo mesurée de ce dépôt, la ligne du guide des réglages pour votre machine. Modifiez ce qui ne convient pas en regardant l'agent travailler.
6767
68+ Les nombres qui concernent le modèle portent un tag qui dit comment ils ont été obtenus, expliqué en haut du fichier : `[measured]`, `[card]`, `[server]`, `[guide]` ou `[rule]`. Le commentaire au-dessus de `model:` liste les réglages choisis pour ce modèle. Quand vous changez de modèle, ou seulement de quantification, refaites-les ensemble : remesurez les nombres `[measured]`, relisez la carte pour les `[card]` — ou relancez `mm init` et comparez. La ligne `model facts` de l'en-tête garde ce que le serveur disait du modèle quand le fichier a été écrit.
69+
6870 ## Variantes
6971
7072 - **Vérifier que le modèle répond.** Ajoutez `--check` : après l'écriture, mm pose au modèle la question « Say hello in one line. » avec le nouveau fichier et indique s'il a répondu. La première réponse charge le modèle, ce qui peut prendre un moment.
@@ -77,7 +79,7 @@ Ce guide montre comment écrire un nouvel agent.yaml pour un modèle et une tâc
7779 ```
7880
7981 - **Voir le fichier sans l'écrire.** `-o -` l'affiche sur la sortie standard.
80-- **Un serveur ailleurs.** `--base-url http://192.168.1.20:17434/v1` ; il est écrit dans le fichier comme `baseUrl`.
82+- **Un serveur ailleurs.** `--base-url http://192.168.1.20:17434/v1` ; il est écrit dans le fichier comme `baseUrl`. Sans lui, le fichier n'a pas de `baseUrl` et garde en commentaire l'adresse par défaut du fournisseur et son fallback, prêts à être décommentés.
8183 - **Un serveur qui ne répond pas.** mm le dit et demande le nom du modèle : les réglages viennent alors de la seule table des familles.
8284 - **Un fichier existant.** mm ne l'écrase jamais, et le dit avant de poser la moindre question : choisissez un autre nom avec `-o`.
8385 - **Un agent JSON.** `--usage json` écrit un exemple de `structuredOutput` (des types Go) à remplacer par les vôtres, et désactive tous les outils, puisqu'une réponse structurée interdit les outils.
@@ -46,7 +46,7 @@ Ce guide montre comment écrire un nouvel agent.yaml pour un modèle et une tâc
46 │ │ skillsDir: no-skills46 │ │ skillsDir: no-skills
47 │ │ # usage docs47 │ │ # usage docs
48 │ │ maxTurns: 848 │ │ maxTurns: 8
49- │ │ # settings guide, row "Small model (≤ 8B), short tasks"49+ │ │ # [guide] settings guide, row "Small model (≤ 8B), short t…
50 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────50 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
51 ↑↓ choose · Enter next · Esc back · PgUp/PgDn preview · Ctrl-C quit51 ↑↓ choose · Enter next · Esc back · PgUp/PgDn preview · Ctrl-C quit
52 ```52 ```
@@ -65,6 +65,8 @@ Ce guide montre comment écrire un nouvel agent.yaml pour un modèle et une tâc
65 65
66 4. Lisez le fichier : chaque valeur est précédée de sa raison — l'échantillonnage du GGUF lui-même, une carte de modèle, une démo mesurée de ce dépôt, la ligne du guide des réglages pour votre machine. Modifiez ce qui ne convient pas en regardant l'agent travailler.66 4. Lisez le fichier : chaque valeur est précédée de sa raison — l'échantillonnage du GGUF lui-même, une carte de modèle, une démo mesurée de ce dépôt, la ligne du guide des réglages pour votre machine. Modifiez ce qui ne convient pas en regardant l'agent travailler.
67 67
68+ Les nombres qui concernent le modèle portent un tag qui dit comment ils ont été obtenus, expliqué en haut du fichier : `[measured]`, `[card]`, `[server]`, `[guide]` ou `[rule]`. Le commentaire au-dessus de `model:` liste les réglages choisis pour ce modèle. Quand vous changez de modèle, ou seulement de quantification, refaites-les ensemble : remesurez les nombres `[measured]`, relisez la carte pour les `[card]` — ou relancez `mm init` et comparez. La ligne `model facts` de l'en-tête garde ce que le serveur disait du modèle quand le fichier a été écrit.
69+
68 ## Variantes70 ## Variantes
69 71
70 - **Vérifier que le modèle répond.** Ajoutez `--check` : après l'écriture, mm pose au modèle la question « Say hello in one line. » avec le nouveau fichier et indique s'il a répondu. La première réponse charge le modèle, ce qui peut prendre un moment.72 - **Vérifier que le modèle répond.** Ajoutez `--check` : après l'écriture, mm pose au modèle la question « Say hello in one line. » avec le nouveau fichier et indique s'il a répondu. La première réponse charge le modèle, ce qui peut prendre un moment.
@@ -77,7 +79,7 @@ Ce guide montre comment écrire un nouvel agent.yaml pour un modèle et une tâc
77 ```79 ```
78 80
79 - **Voir le fichier sans l'écrire.** `-o -` l'affiche sur la sortie standard.81 - **Voir le fichier sans l'écrire.** `-o -` l'affiche sur la sortie standard.
80-- **Un serveur ailleurs.** `--base-url http://192.168.1.20:17434/v1` ; il est écrit dans le fichier comme `baseUrl`.82+- **Un serveur ailleurs.** `--base-url http://192.168.1.20:17434/v1` ; il est écrit dans le fichier comme `baseUrl`. Sans lui, le fichier n'a pas de `baseUrl` et garde en commentaire l'adresse par défaut du fournisseur et son fallback, prêts à être décommentés.
81 - **Un serveur qui ne répond pas.** mm le dit et demande le nom du modèle : les réglages viennent alors de la seule table des familles.83 - **Un serveur qui ne répond pas.** mm le dit et demande le nom du modèle : les réglages viennent alors de la seule table des familles.
82 - **Un fichier existant.** mm ne l'écrase jamais, et le dit avant de poser la moindre question : choisissez un autre nom avec `-o`.84 - **Un fichier existant.** mm ne l'écrase jamais, et le dit avant de poser la moindre question : choisissez un autre nom avec `-o`.
83 - **Un agent JSON.** `--usage json` écrit un exemple de `structuredOutput` (des types Go) à remplacer par les vôtres, et désactive tous les outils, puisqu'une réponse structurée interdit les outils.85 - **Un agent JSON.** `--usage json` écrit un exemple de `structuredOutput` (des types Go) à remplacer par les vôtres, et désactive tous les outils, puisqu'une réponse structurée interdit les outils.
modified docs/fr/reference/cli.md +4 -3
@@ -175,12 +175,13 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/
175175
176176 | Aspect | Comportement |
177177 |--------|--------------|
178-| Fenêtre | Quand l'entrée et la sortie standard sont des terminaux (et ni `--yes` ni `--line`) : une fenêtre plein écran — les étapes (✓ répondue, ▸ en cours, · à venir, chaque réponse à côté de son étape), la question avec la description du choix surligné, et le fichier tel qu'il serait écrit avec ce choix (aperçu à côté de la question à partir de 100 colonnes, à sa place avec `Tab` en dessous). Touches : `↑` `↓` `Début` `Fin` choisissent, `Entrée` répond, `y` / `n` répondent à la question oui ou non, `Échap` reprend la réponse précédente (les réponses données par option sont sautées), `PgUp` `PgDn` font défiler l'aperçu, `Ctrl-U` efface une réponse de texte, `Ctrl-C` quitte sans rien écrire (`nothing written`, code 1). Une réponse refusée s'affiche sur le trait du bas. Quand toutes les questions ont une réponse, `Entrée` écrit le fichier. |
178+| Fenêtre | Quand l'entrée et la sortie standard sont des terminaux (et ni `--yes` ni `--line`) : une fenêtre plein écran — les étapes (✓ répondue, ▸ en cours, · à venir, chaque réponse à côté de son étape), la question avec la description du choix surligné (les modèles sont listés par leur nom court, comme dans l'en-tête de la fenêtre — `Qwen3.5-4B:Q4_K_M` — et l'identifiant complet du modèle surligné s'affiche sous la liste), et le fichier tel qu'il serait écrit avec ce choix (aperçu à côté de la question à partir de 100 colonnes, à sa place avec `Tab` en dessous). Touches : `↑` `↓` `Début` `Fin` choisissent, `Entrée` répond, `y` / `n` répondent à la question oui ou non, `Échap` reprend la réponse précédente (les réponses données par option sont sautées), `PgUp` `PgDn` font défiler l'aperçu, `Ctrl-U` efface une réponse de texte, `Ctrl-C` quitte sans rien écrire (`nothing written`, code 1). Une réponse refusée s'affiche sur le trait du bas. Quand toutes les questions ont une réponse, `Entrée` écrit le fichier. |
179179 | Mode ligne | Sinon : une question par ligne, choix numérotés ; un numéro, le texte d'une option ou un autre texte y répond ; une ligne vide ou `/skip` prend le défaut. Les usages et les machines sont d'abord listés avec une description d'une ligne. |
180180 | Faits sur le modèle | llmman et Ollama : `POST /api/show` — la fenêtre native, l'échantillonnage du GGUF (`general.sampling.*`, les paramètres du Modelfile d'Ollama), l'interrupteur de raisonnement du chat template (`enable_thinking`, `reasoning_effort`) et son défaut. llama-server : `GET /props` — `n_ctx` et le chat template. Docker Model Runner et `openai` : rien — la table des familles décide ; `openai` liste ses modèles avec `OPENAI_API_KEY`. |
181-| Origine de chaque valeur | Les faits du serveur, puis la table des familles, puis l'usage, puis la ligne de la machine ; la table est `internal/agentinit/presets.yaml`, chaque entrée avec sa source. |
181+| Origine de chaque valeur | Les faits du serveur, puis la table des familles, puis l'usage, puis la ligne de la machine ; la table est `internal/agentinit/presets.yaml`, chaque entrée avec sa source et, par bloc d'échantillonnage, son `origin` (`measured`, `card` ou `rule`). |
182+| Tags d'origine | Le commentaire d'un nombre qui concerne le modèle commence par un tag : `[measured]` (une démo de ce dépôt a tourné avec), `[card]` (carte du modèle, GGUF ou référence d'API, non mesuré), `[server]` (lu sur le serveur : l'échantillonnage du GGUF), `[guide]` (la ligne machine du guide des réglages : `maxOutput`, les délais), `[rule]` (une règle empirique : `max_tokens`, le plancher de 0,6 avec raisonnement, une famille sans valeurs publiées). Les valeurs qui découlent des réponses ou des défauts de mm n'ont pas de tag. L'en-tête explique les tags que le fichier utilise, et seulement ceux-là. |
182183 | Échantillonnage | Raisonnement activé (l'usage le demande et le modèle a un interrupteur) : l'échantillonnage « thinking » de la famille, jamais une température sous 0,6, `max_tokens: 16384`. Raisonnement désactivé : l'échantillonnage général de la famille, ou sa température « précise » pour les usages qui utilisent beaucoup d'outils, `max_tokens: 4096`. L'échantillonnage du GGUF remplace celui de la famille. `parallel_tool_calls: false` dès qu'un outil est actif. |
183-| Fichier écrit | Chaque valeur précédée d'un commentaire qui dit pourquoi elle a cette valeur ; `contextWindow: 0` (mm demande au serveur), avec la fenêtre native en commentaire ; `context.threshold: 75` ; le prompt système en anglais de l'usage ; pour `docs` un bloc `docSearch`, pour `json` un exemple de `structuredOutput` et tous les outils désactivés, pour `coordinator` un exemple commenté de `mmConfigs`. |
184+| Fichier écrit | Un en-tête avec la famille, l'usage, la machine, `model facts` (ce que disait le serveur : fenêtre native, échantillonnage du GGUF, interrupteur de raisonnement et son défaut — ou `none`) et la légende des origines ; au-dessus de `model:`, les réglages choisis pour ce modèle (clés d'échantillonnage, `chat_template_kwargs`), à refaire ensemble s'il change ; sans `--base-url`, le `baseUrl` et le `fallback` par défaut du fournisseur en commentaire ; chaque valeur précédée d'un commentaire qui dit pourquoi elle a cette valeur ; `contextWindow: 0` (mm demande au serveur), avec la fenêtre native en commentaire ; `context.threshold: 75` ; le prompt système en anglais de l'usage ; pour `docs` un bloc `docSearch`, pour `json` un exemple de `structuredOutput` et tous les outils désactivés, pour `coordinator` un exemple commenté de `mmConfigs`. |
184185 | Fichier existant | Jamais écrasé : `⚠️ <fichier> already exists — nothing was written; choose another name with -o`, code de sortie 1, avant toute question. |
185186 | Après l'écriture | Le fichier est chargé comme mm charge une configuration ; une erreur est affichée et le code de sortie vaut 1. |
186187 | Codes de sortie | 0 écrit (et vérifié) ; 1 fournisseur, usage ou machine inconnu, pas de modèle, fichier existant, fichier que mm ne charge pas, vérification en échec ; 2 option invalide. |
@@ -175,12 +175,13 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/
175 175
176 | Aspect | Comportement |176 | Aspect | Comportement |
177 |--------|--------------|177 |--------|--------------|
178-| Fenêtre | Quand l'entrée et la sortie standard sont des terminaux (et ni `--yes` ni `--line`) : une fenêtre plein écran — les étapes (✓ répondue, ▸ en cours, · à venir, chaque réponse à côté de son étape), la question avec la description du choix surligné, et le fichier tel qu'il serait écrit avec ce choix (aperçu à côté de la question à partir de 100 colonnes, à sa place avec `Tab` en dessous). Touches : `↑` `↓` `Début` `Fin` choisissent, `Entrée` répond, `y` / `n` répondent à la question oui ou non, `Échap` reprend la réponse précédente (les réponses données par option sont sautées), `PgUp` `PgDn` font défiler l'aperçu, `Ctrl-U` efface une réponse de texte, `Ctrl-C` quitte sans rien écrire (`nothing written`, code 1). Une réponse refusée s'affiche sur le trait du bas. Quand toutes les questions ont une réponse, `Entrée` écrit le fichier. |178+| Fenêtre | Quand l'entrée et la sortie standard sont des terminaux (et ni `--yes` ni `--line`) : une fenêtre plein écran — les étapes (✓ répondue, ▸ en cours, · à venir, chaque réponse à côté de son étape), la question avec la description du choix surligné (les modèles sont listés par leur nom court, comme dans l'en-tête de la fenêtre — `Qwen3.5-4B:Q4_K_M` — et l'identifiant complet du modèle surligné s'affiche sous la liste), et le fichier tel qu'il serait écrit avec ce choix (aperçu à côté de la question à partir de 100 colonnes, à sa place avec `Tab` en dessous). Touches : `↑` `↓` `Début` `Fin` choisissent, `Entrée` répond, `y` / `n` répondent à la question oui ou non, `Échap` reprend la réponse précédente (les réponses données par option sont sautées), `PgUp` `PgDn` font défiler l'aperçu, `Ctrl-U` efface une réponse de texte, `Ctrl-C` quitte sans rien écrire (`nothing written`, code 1). Une réponse refusée s'affiche sur le trait du bas. Quand toutes les questions ont une réponse, `Entrée` écrit le fichier. |
179 | Mode ligne | Sinon : une question par ligne, choix numérotés ; un numéro, le texte d'une option ou un autre texte y répond ; une ligne vide ou `/skip` prend le défaut. Les usages et les machines sont d'abord listés avec une description d'une ligne. |179 | Mode ligne | Sinon : une question par ligne, choix numérotés ; un numéro, le texte d'une option ou un autre texte y répond ; une ligne vide ou `/skip` prend le défaut. Les usages et les machines sont d'abord listés avec une description d'une ligne. |
180 | Faits sur le modèle | llmman et Ollama : `POST /api/show` — la fenêtre native, l'échantillonnage du GGUF (`general.sampling.*`, les paramètres du Modelfile d'Ollama), l'interrupteur de raisonnement du chat template (`enable_thinking`, `reasoning_effort`) et son défaut. llama-server : `GET /props` — `n_ctx` et le chat template. Docker Model Runner et `openai` : rien — la table des familles décide ; `openai` liste ses modèles avec `OPENAI_API_KEY`. |180 | Faits sur le modèle | llmman et Ollama : `POST /api/show` — la fenêtre native, l'échantillonnage du GGUF (`general.sampling.*`, les paramètres du Modelfile d'Ollama), l'interrupteur de raisonnement du chat template (`enable_thinking`, `reasoning_effort`) et son défaut. llama-server : `GET /props` — `n_ctx` et le chat template. Docker Model Runner et `openai` : rien — la table des familles décide ; `openai` liste ses modèles avec `OPENAI_API_KEY`. |
181-| Origine de chaque valeur | Les faits du serveur, puis la table des familles, puis l'usage, puis la ligne de la machine ; la table est `internal/agentinit/presets.yaml`, chaque entrée avec sa source. |181+| Origine de chaque valeur | Les faits du serveur, puis la table des familles, puis l'usage, puis la ligne de la machine ; la table est `internal/agentinit/presets.yaml`, chaque entrée avec sa source et, par bloc d'échantillonnage, son `origin` (`measured`, `card` ou `rule`). |
182+| Tags d'origine | Le commentaire d'un nombre qui concerne le modèle commence par un tag : `[measured]` (une démo de ce dépôt a tourné avec), `[card]` (carte du modèle, GGUF ou référence d'API, non mesuré), `[server]` (lu sur le serveur : l'échantillonnage du GGUF), `[guide]` (la ligne machine du guide des réglages : `maxOutput`, les délais), `[rule]` (une règle empirique : `max_tokens`, le plancher de 0,6 avec raisonnement, une famille sans valeurs publiées). Les valeurs qui découlent des réponses ou des défauts de mm n'ont pas de tag. L'en-tête explique les tags que le fichier utilise, et seulement ceux-là. |
182 | Échantillonnage | Raisonnement activé (l'usage le demande et le modèle a un interrupteur) : l'échantillonnage « thinking » de la famille, jamais une température sous 0,6, `max_tokens: 16384`. Raisonnement désactivé : l'échantillonnage général de la famille, ou sa température « précise » pour les usages qui utilisent beaucoup d'outils, `max_tokens: 4096`. L'échantillonnage du GGUF remplace celui de la famille. `parallel_tool_calls: false` dès qu'un outil est actif. |183 | Échantillonnage | Raisonnement activé (l'usage le demande et le modèle a un interrupteur) : l'échantillonnage « thinking » de la famille, jamais une température sous 0,6, `max_tokens: 16384`. Raisonnement désactivé : l'échantillonnage général de la famille, ou sa température « précise » pour les usages qui utilisent beaucoup d'outils, `max_tokens: 4096`. L'échantillonnage du GGUF remplace celui de la famille. `parallel_tool_calls: false` dès qu'un outil est actif. |
183-| Fichier écrit | Chaque valeur précédée d'un commentaire qui dit pourquoi elle a cette valeur ; `contextWindow: 0` (mm demande au serveur), avec la fenêtre native en commentaire ; `context.threshold: 75` ; le prompt système en anglais de l'usage ; pour `docs` un bloc `docSearch`, pour `json` un exemple de `structuredOutput` et tous les outils désactivés, pour `coordinator` un exemple commenté de `mmConfigs`. |184+| Fichier écrit | Un en-tête avec la famille, l'usage, la machine, `model facts` (ce que disait le serveur : fenêtre native, échantillonnage du GGUF, interrupteur de raisonnement et son défaut — ou `none`) et la légende des origines ; au-dessus de `model:`, les réglages choisis pour ce modèle (clés d'échantillonnage, `chat_template_kwargs`), à refaire ensemble s'il change ; sans `--base-url`, le `baseUrl` et le `fallback` par défaut du fournisseur en commentaire ; chaque valeur précédée d'un commentaire qui dit pourquoi elle a cette valeur ; `contextWindow: 0` (mm demande au serveur), avec la fenêtre native en commentaire ; `context.threshold: 75` ; le prompt système en anglais de l'usage ; pour `docs` un bloc `docSearch`, pour `json` un exemple de `structuredOutput` et tous les outils désactivés, pour `coordinator` un exemple commenté de `mmConfigs`. |
184 | Fichier existant | Jamais écrasé : `⚠️ <fichier> already exists — nothing was written; choose another name with -o`, code de sortie 1, avant toute question. |185 | Fichier existant | Jamais écrasé : `⚠️ <fichier> already exists — nothing was written; choose another name with -o`, code de sortie 1, avant toute question. |
185 | Après l'écriture | Le fichier est chargé comme mm charge une configuration ; une erreur est affichée et le code de sortie vaut 1. |186 | Après l'écriture | Le fichier est chargé comme mm charge une configuration ; une erreur est affichée et le code de sortie vaut 1. |
186 | Codes de sortie | 0 écrit (et vérifié) ; 1 fournisseur, usage ou machine inconnu, pas de modèle, fichier existant, fichier que mm ne charge pas, vérification en échec ; 2 option invalide. |187 | Codes de sortie | 0 écrit (et vérifié) ; 1 fournisseur, usage ou machine inconnu, pas de modèle, fichier existant, fichier que mm ne charge pas, vérification en échec ; 2 option invalide. |
modified internal/agentinit/decide.go +38 -18
@@ -23,8 +23,23 @@ type Setting struct {
2323 Key string
2424 Value string // already formatted as YAML
2525 Reason string
26+ // Origin tags a number about the model with how it was obtained (one of
27+ // the Origin constants, written "[measured]" before the reason); "" for
28+ // a value that follows from the answers or from mm's defaults.
29+ Origin string
2630 }
2731
32+// How a number of the generated file was obtained (ticket 121): after a
33+// model or quantization change, the tag says what to redo — re-measure,
34+// re-read the card, ask the server again.
35+const (
36+ OriginMeasured = "measured" // a demo of this repository ran with it
37+ OriginCard = "card" // copied from the model card, the GGUF or the API reference
38+ OriginRule = "rule" // a rule of thumb, nothing specific to the model
39+ OriginServer = "server" // read from the model server when the file was written
40+ OriginGuide = "guide" // the settings guide's row for the machine
41+)
42+
2843 // Settings is everything the generated file holds.
2944 type Settings struct {
3045 Choices Choices
@@ -74,7 +89,11 @@ func reasons(fam Family, f Facts) bool {
7489 }
7590
7691 func (s *Settings) add(key, value, reason string) {
77- s.Top = append(s.Top, Setting{key, value, reason})
92+ s.addFrom(key, value, reason, "")
93+}
94+
95+func (s *Settings) addFrom(key, value, reason, origin string) {
96+ s.Top = append(s.Top, Setting{Key: key, Value: value, Reason: reason, Origin: origin})
7897 }
7998
8099 func (s *Settings) decideTop() {
@@ -107,23 +126,23 @@ func (s *Settings) decideTop() {
107126 s.add("skillsDir", "no-skills", "no skills: add a skills/ folder and set skillsDir to use some (see docs/en/how-to/add-a-skill.md)")
108127
109128 s.add("maxTurns", fmt.Sprint(u.MaxTurns), "usage "+u.Name)
110- s.add("maxOutput", fmt.Sprint(m.MaxOutput), m.Source)
111- s.add("firstTokenTimeout", m.FirstTokenTimeout, m.Source)
129+ s.addFrom("maxOutput", fmt.Sprint(m.MaxOutput), m.Source, OriginGuide)
130+ s.addFrom("firstTokenTimeout", m.FirstTokenTimeout, m.Source, OriginGuide)
112131 watchdog, watchdogReason := m.WatchdogTimeout, m.Source
113132 if u.Tools["bashTool"] && m.WatchdogTimeout != m.BashTimeout {
114133 watchdog, watchdogReason = m.BashTimeout, "aligned on bashTimeout, as the settings guide says when bash is on (section 3)"
115134 }
116- s.add("watchdogTimeout", watchdog, watchdogReason)
135+ s.addFrom("watchdogTimeout", watchdog, watchdogReason, OriginGuide)
117136 if u.Tools["bashTool"] {
118- s.add("bashTimeout", m.BashTimeout, m.Source)
137+ s.addFrom("bashTimeout", m.BashTimeout, m.Source, OriginGuide)
119138 }
120139 }
121140
122141 func (s *Settings) decideSampling() {
123142 fam, f := s.Family, s.Facts
124- base, source := fam.General, "family "+fam.Name+": "+fam.Source
143+ base, source, origin := fam.General, "family "+fam.Name+": "+fam.Source, fam.Origin["general"]
125144 if s.Thinking && fam.Thinking.Temperature != nil {
126- base = fam.Thinking
145+ base, origin = fam.Thinking, fam.Origin["thinking"]
127146 }
128147 values := map[string]float64{}
129148 put := func(key string, v *float64) {
@@ -137,25 +156,26 @@ func (s *Settings) decideSampling() {
137156 if base.TopK != nil {
138157 values["top_k"] = float64(*base.TopK)
139158 }
140- reasons := map[string]string{}
159+ reasons, origins := map[string]string{}, map[string]string{}
141160 for k := range values {
142- reasons[k] = source
161+ reasons[k], origins[k] = source, origin
143162 }
144163 if len(f.Sampling) > 0 {
145164 for k, v := range f.Sampling {
146- values[k], reasons[k] = v, "the GGUF's own sampling, read from the server"
165+ values[k], reasons[k], origins[k] = v, "the GGUF's own sampling, read from the server", OriginServer
147166 }
148167 }
149168 if !s.Thinking && s.Usage.Precise && fam.Precise != nil {
150169 values["temperature"] = *fam.Precise
151170 reasons["temperature"] = fmt.Sprintf("precise temperature for a %s agent without reasoning (family %s)", s.Usage.Name, fam.Name)
171+ origins["temperature"] = fam.Origin["precise"]
152172 }
153173 if s.Thinking && values["temperature"] < 0.5 {
154- values["temperature"], reasons["temperature"] = 0.6, "reasoning is on: a thinking model loops at a low temperature"
174+ values["temperature"], reasons["temperature"], origins["temperature"] = 0.6, "reasoning is on: a thinking model loops at a low temperature", OriginRule
155175 }
156176 for _, k := range []string{"temperature", "top_p", "top_k", "min_p"} {
157177 if v, ok := values[k]; ok {
158- s.Sampling = append(s.Sampling, Setting{k, number(k, v), reasons[k]})
178+ s.Sampling = append(s.Sampling, Setting{Key: k, Value: number(k, v), Reason: reasons[k], Origin: origins[k]})
159179 }
160180 }
161181 tokens, tokensReason := "4096", "room for an answer or a whole file in one tool call"
@@ -166,9 +186,9 @@ func (s *Settings) decideSampling() {
166186 if s.Family.MaxTokensKey != "" {
167187 maxKey, tokensReason = s.Family.MaxTokensKey, tokensReason+" (family "+s.Family.Name+" names it "+s.Family.MaxTokensKey+")"
168188 }
169- s.Sampling = append(s.Sampling, Setting{maxKey, tokens, tokensReason})
189+ s.Sampling = append(s.Sampling, Setting{Key: maxKey, Value: tokens, Reason: tokensReason, Origin: OriginRule})
170190 if s.anyTool() {
171- s.Sampling = append(s.Sampling, Setting{"parallel_tool_calls", "false", "one tool call per reply: small models run a check before the change it checks has landed otherwise"})
191+ s.Sampling = append(s.Sampling, Setting{Key: "parallel_tool_calls", Value: "false", Reason: "one tool call per reply: small models run a check before the change it checks has landed otherwise"})
172192 }
173193 s.decideKwargs()
174194 }
@@ -188,7 +208,7 @@ func (s *Settings) decideKwargs() {
188208 if d := s.Facts.Thinking.Default; d != "" {
189209 why += "; the template's default is " + d
190210 }
191- s.Kwargs = append(s.Kwargs, Setting{"enable_thinking", boolYAML(s.Thinking), why + " (" + source + ")"})
211+ s.Kwargs = append(s.Kwargs, Setting{Key: "enable_thinking", Value: boolYAML(s.Thinking), Reason: why + " (" + source + ")"})
192212 case "reasoning_effort":
193213 values := s.Facts.Thinking.EffortValues
194214 if len(values) == 0 {
@@ -201,17 +221,17 @@ func (s *Settings) decideKwargs() {
201221 if len(values) > 0 && !contains(values, effort) {
202222 effort = values[0]
203223 }
204- s.Kwargs = append(s.Kwargs, Setting{"reasoning_effort", effort, fmt.Sprintf("one of %s (%s); a value the template does not know may make every request fail", strings.Join(values, ", "), source)})
224+ s.Kwargs = append(s.Kwargs, Setting{Key: "reasoning_effort", Value: effort, Reason: fmt.Sprintf("one of %s (%s); a value the template does not know may make every request fail", strings.Join(values, ", "), source)})
205225 }
206226 }
207227
208228 func (s *Settings) decideContext() {
209- s.Context = append(s.Context, Setting{"enabled", "true", "compress the history before the window is full (settings guide, every row)"})
229+ s.Context = append(s.Context, Setting{Key: "enabled", Value: "true", Reason: "compress the history before the window is full (settings guide, every row)"})
210230 // The percentage applies to the window the server SERVES, which mm
211231 // probes (contextWindow: 0) and which may be far below the model's
212232 // native one (Ollama often serves 4k–8k): a threshold computed from the
213233 // native window would compress almost at once. mm's default it is.
214- s.Context = append(s.Context, Setting{"threshold", "75", "mm's default: 75 % of the window the server serves, a quarter left for the next question and its tool outputs"})
234+ s.Context = append(s.Context, Setting{Key: "threshold", Value: "75", Reason: "mm's default: 75 % of the window the server serves, a quarter left for the next question and its tool outputs"})
215235 }
216236
217237 func (s *Settings) decideNotes() {
@@ -23,8 +23,23 @@ type Setting struct {
23 Key string23 Key string
24 Value string // already formatted as YAML24 Value string // already formatted as YAML
25 Reason string25 Reason string
26+ // Origin tags a number about the model with how it was obtained (one of
27+ // the Origin constants, written "[measured]" before the reason); "" for
28+ // a value that follows from the answers or from mm's defaults.
29+ Origin string
26 }30 }
27 31
32+// How a number of the generated file was obtained (ticket 121): after a
33+// model or quantization change, the tag says what to redo — re-measure,
34+// re-read the card, ask the server again.
35+const (
36+ OriginMeasured = "measured" // a demo of this repository ran with it
37+ OriginCard = "card" // copied from the model card, the GGUF or the API reference
38+ OriginRule = "rule" // a rule of thumb, nothing specific to the model
39+ OriginServer = "server" // read from the model server when the file was written
40+ OriginGuide = "guide" // the settings guide's row for the machine
41+)
42+
28 // Settings is everything the generated file holds.43 // Settings is everything the generated file holds.
29 type Settings struct {44 type Settings struct {
30 Choices Choices45 Choices Choices
@@ -74,7 +89,11 @@ func reasons(fam Family, f Facts) bool {
74 }89 }
75 90
76 func (s *Settings) add(key, value, reason string) {91 func (s *Settings) add(key, value, reason string) {
77- s.Top = append(s.Top, Setting{key, value, reason})92+ s.addFrom(key, value, reason, "")
93+}
94+
95+func (s *Settings) addFrom(key, value, reason, origin string) {
96+ s.Top = append(s.Top, Setting{Key: key, Value: value, Reason: reason, Origin: origin})
78 }97 }
79 98
80 func (s *Settings) decideTop() {99 func (s *Settings) decideTop() {
@@ -107,23 +126,23 @@ func (s *Settings) decideTop() {
107 s.add("skillsDir", "no-skills", "no skills: add a skills/ folder and set skillsDir to use some (see docs/en/how-to/add-a-skill.md)")126 s.add("skillsDir", "no-skills", "no skills: add a skills/ folder and set skillsDir to use some (see docs/en/how-to/add-a-skill.md)")
108 127
109 s.add("maxTurns", fmt.Sprint(u.MaxTurns), "usage "+u.Name)128 s.add("maxTurns", fmt.Sprint(u.MaxTurns), "usage "+u.Name)
110- s.add("maxOutput", fmt.Sprint(m.MaxOutput), m.Source)129+ s.addFrom("maxOutput", fmt.Sprint(m.MaxOutput), m.Source, OriginGuide)
111- s.add("firstTokenTimeout", m.FirstTokenTimeout, m.Source)130+ s.addFrom("firstTokenTimeout", m.FirstTokenTimeout, m.Source, OriginGuide)
112 watchdog, watchdogReason := m.WatchdogTimeout, m.Source131 watchdog, watchdogReason := m.WatchdogTimeout, m.Source
113 if u.Tools["bashTool"] && m.WatchdogTimeout != m.BashTimeout {132 if u.Tools["bashTool"] && m.WatchdogTimeout != m.BashTimeout {
114 watchdog, watchdogReason = m.BashTimeout, "aligned on bashTimeout, as the settings guide says when bash is on (section 3)"133 watchdog, watchdogReason = m.BashTimeout, "aligned on bashTimeout, as the settings guide says when bash is on (section 3)"
115 }134 }
116- s.add("watchdogTimeout", watchdog, watchdogReason)135+ s.addFrom("watchdogTimeout", watchdog, watchdogReason, OriginGuide)
117 if u.Tools["bashTool"] {136 if u.Tools["bashTool"] {
118- s.add("bashTimeout", m.BashTimeout, m.Source)137+ s.addFrom("bashTimeout", m.BashTimeout, m.Source, OriginGuide)
119 }138 }
120 }139 }
121 140
122 func (s *Settings) decideSampling() {141 func (s *Settings) decideSampling() {
123 fam, f := s.Family, s.Facts142 fam, f := s.Family, s.Facts
124- base, source := fam.General, "family "+fam.Name+": "+fam.Source143+ base, source, origin := fam.General, "family "+fam.Name+": "+fam.Source, fam.Origin["general"]
125 if s.Thinking && fam.Thinking.Temperature != nil {144 if s.Thinking && fam.Thinking.Temperature != nil {
126- base = fam.Thinking145+ base, origin = fam.Thinking, fam.Origin["thinking"]
127 }146 }
128 values := map[string]float64{}147 values := map[string]float64{}
129 put := func(key string, v *float64) {148 put := func(key string, v *float64) {
@@ -137,25 +156,26 @@ func (s *Settings) decideSampling() {
137 if base.TopK != nil {156 if base.TopK != nil {
138 values["top_k"] = float64(*base.TopK)157 values["top_k"] = float64(*base.TopK)
139 }158 }
140- reasons := map[string]string{}159+ reasons, origins := map[string]string{}, map[string]string{}
141 for k := range values {160 for k := range values {
142- reasons[k] = source161+ reasons[k], origins[k] = source, origin
143 }162 }
144 if len(f.Sampling) > 0 {163 if len(f.Sampling) > 0 {
145 for k, v := range f.Sampling {164 for k, v := range f.Sampling {
146- values[k], reasons[k] = v, "the GGUF's own sampling, read from the server"165+ values[k], reasons[k], origins[k] = v, "the GGUF's own sampling, read from the server", OriginServer
147 }166 }
148 }167 }
149 if !s.Thinking && s.Usage.Precise && fam.Precise != nil {168 if !s.Thinking && s.Usage.Precise && fam.Precise != nil {
150 values["temperature"] = *fam.Precise169 values["temperature"] = *fam.Precise
151 reasons["temperature"] = fmt.Sprintf("precise temperature for a %s agent without reasoning (family %s)", s.Usage.Name, fam.Name)170 reasons["temperature"] = fmt.Sprintf("precise temperature for a %s agent without reasoning (family %s)", s.Usage.Name, fam.Name)
171+ origins["temperature"] = fam.Origin["precise"]
152 }172 }
153 if s.Thinking && values["temperature"] < 0.5 {173 if s.Thinking && values["temperature"] < 0.5 {
154- values["temperature"], reasons["temperature"] = 0.6, "reasoning is on: a thinking model loops at a low temperature"174+ values["temperature"], reasons["temperature"], origins["temperature"] = 0.6, "reasoning is on: a thinking model loops at a low temperature", OriginRule
155 }175 }
156 for _, k := range []string{"temperature", "top_p", "top_k", "min_p"} {176 for _, k := range []string{"temperature", "top_p", "top_k", "min_p"} {
157 if v, ok := values[k]; ok {177 if v, ok := values[k]; ok {
158- s.Sampling = append(s.Sampling, Setting{k, number(k, v), reasons[k]})178+ s.Sampling = append(s.Sampling, Setting{Key: k, Value: number(k, v), Reason: reasons[k], Origin: origins[k]})
159 }179 }
160 }180 }
161 tokens, tokensReason := "4096", "room for an answer or a whole file in one tool call"181 tokens, tokensReason := "4096", "room for an answer or a whole file in one tool call"
@@ -166,9 +186,9 @@ func (s *Settings) decideSampling() {
166 if s.Family.MaxTokensKey != "" {186 if s.Family.MaxTokensKey != "" {
167 maxKey, tokensReason = s.Family.MaxTokensKey, tokensReason+" (family "+s.Family.Name+" names it "+s.Family.MaxTokensKey+")"187 maxKey, tokensReason = s.Family.MaxTokensKey, tokensReason+" (family "+s.Family.Name+" names it "+s.Family.MaxTokensKey+")"
168 }188 }
169- s.Sampling = append(s.Sampling, Setting{maxKey, tokens, tokensReason})189+ s.Sampling = append(s.Sampling, Setting{Key: maxKey, Value: tokens, Reason: tokensReason, Origin: OriginRule})
170 if s.anyTool() {190 if s.anyTool() {
171- s.Sampling = append(s.Sampling, Setting{"parallel_tool_calls", "false", "one tool call per reply: small models run a check before the change it checks has landed otherwise"})191+ s.Sampling = append(s.Sampling, Setting{Key: "parallel_tool_calls", Value: "false", Reason: "one tool call per reply: small models run a check before the change it checks has landed otherwise"})
172 }192 }
173 s.decideKwargs()193 s.decideKwargs()
174 }194 }
@@ -188,7 +208,7 @@ func (s *Settings) decideKwargs() {
188 if d := s.Facts.Thinking.Default; d != "" {208 if d := s.Facts.Thinking.Default; d != "" {
189 why += "; the template's default is " + d209 why += "; the template's default is " + d
190 }210 }
191- s.Kwargs = append(s.Kwargs, Setting{"enable_thinking", boolYAML(s.Thinking), why + " (" + source + ")"})211+ s.Kwargs = append(s.Kwargs, Setting{Key: "enable_thinking", Value: boolYAML(s.Thinking), Reason: why + " (" + source + ")"})
192 case "reasoning_effort":212 case "reasoning_effort":
193 values := s.Facts.Thinking.EffortValues213 values := s.Facts.Thinking.EffortValues
194 if len(values) == 0 {214 if len(values) == 0 {
@@ -201,17 +221,17 @@ func (s *Settings) decideKwargs() {
201 if len(values) > 0 && !contains(values, effort) {221 if len(values) > 0 && !contains(values, effort) {
202 effort = values[0]222 effort = values[0]
203 }223 }
204- s.Kwargs = append(s.Kwargs, Setting{"reasoning_effort", effort, fmt.Sprintf("one of %s (%s); a value the template does not know may make every request fail", strings.Join(values, ", "), source)})224+ s.Kwargs = append(s.Kwargs, Setting{Key: "reasoning_effort", Value: effort, Reason: fmt.Sprintf("one of %s (%s); a value the template does not know may make every request fail", strings.Join(values, ", "), source)})
205 }225 }
206 }226 }
207 227
208 func (s *Settings) decideContext() {228 func (s *Settings) decideContext() {
209- s.Context = append(s.Context, Setting{"enabled", "true", "compress the history before the window is full (settings guide, every row)"})229+ s.Context = append(s.Context, Setting{Key: "enabled", Value: "true", Reason: "compress the history before the window is full (settings guide, every row)"})
210 // The percentage applies to the window the server SERVES, which mm230 // The percentage applies to the window the server SERVES, which mm
211 // probes (contextWindow: 0) and which may be far below the model's231 // probes (contextWindow: 0) and which may be far below the model's
212 // native one (Ollama often serves 4k–8k): a threshold computed from the232 // native one (Ollama often serves 4k–8k): a threshold computed from the
213 // native window would compress almost at once. mm's default it is.233 // native window would compress almost at once. mm's default it is.
214- s.Context = append(s.Context, Setting{"threshold", "75", "mm's default: 75 % of the window the server serves, a quarter left for the next question and its tool outputs"})234+ s.Context = append(s.Context, Setting{Key: "threshold", Value: "75", Reason: "mm's default: 75 % of the window the server serves, a quarter left for the next question and its tool outputs"})
215 }235 }
216 236
217 func (s *Settings) decideNotes() {237 func (s *Settings) decideNotes() {
modified internal/agentinit/decide_test.go +92 -1
@@ -145,7 +145,7 @@ func TestOpenAIFamilyAndProvider(t *testing.T) {
145145 }
146146 text := Render(s)
147147 flat := strings.ReplaceAll(text, "\n# ", " ") // comments are wrapped
148- if !strings.Contains(flat, "OPENAI_API_KEY") || !strings.Contains(flat, "No baseUrl: the OpenAI API") || !strings.Contains(flat, "cannot ask the server for its window") {
148+ if !strings.Contains(flat, "OPENAI_API_KEY") || !strings.Contains(flat, `No baseUrl: openai's defaults apply. For the record: baseUrl: "https://api.openai.com/v1"`) || !strings.Contains(flat, "cannot ask the server for its window") {
149149 t.Errorf("key / address comment missing:\n%s", text)
150150 }
151151 loadRendered(t, text)
@@ -153,3 +153,94 @@ func TestOpenAIFamilyAndProvider(t *testing.T) {
153153 t.Error("openai must not be probed for the first question's default")
154154 }
155155 }
156+
157+// A file without baseUrl records the provider's default addresses as
158+// comments (ticket 121): with the fallback when there is one; a file with a
159+// baseUrl gets none of it.
160+func TestRenderRecordsTheDefaultURLs(t *testing.T) {
161+ p, _ := LoadPresets()
162+ llmman := Render(Decide(p, Choices{Provider: "llmman", Model: "qwen3", Usage: "assistant", Machine: "small"}, Facts{}))
163+ if !strings.Contains(llmman, "# baseUrl: \"http://127.0.0.1:17434/v1\"\n# fallback: \"http://host.docker.internal:17434/v1\" # tried when baseUrl does not answer\n") {
164+ t.Errorf("llmman's default URLs missing:\n%s", llmman)
165+ }
166+ llamacpp := Render(Decide(p, Choices{Provider: "llamacpp", Model: "qwen3", Usage: "assistant", Machine: "small"}, Facts{}))
167+ if !strings.Contains(llamacpp, "# baseUrl: \"http://127.0.0.1:8080/v1\"\n# (llamacpp has no fallback address)\n") {
168+ t.Errorf("llamacpp's default URL missing:\n%s", llamacpp)
169+ }
170+ own := Render(Decide(p, Choices{Provider: "llmman", BaseURL: "http://box:17434/v1", Model: "qwen3", Usage: "assistant", Machine: "small"}, Facts{}))
171+ if strings.Contains(own, "For the record") || !strings.Contains(own, "baseUrl: \"http://box:17434/v1\"") {
172+ t.Errorf("a file with its own baseUrl:\n%s", own)
173+ }
174+ // Uncommented, the two lines are the same settings, written out.
175+ uncommented := strings.Replace(strings.Replace(llmman, "# baseUrl:", "baseUrl:", 1), "# fallback:", "fallback:", 1)
176+ loadRendered(t, uncommented)
177+}
178+
179+// Every number about the model is tagged with how it was obtained, and the
180+// header explains the tags the file uses — only those (ticket 121).
181+func TestRenderTagsWhereTheNumbersComeFrom(t *testing.T) {
182+ p, _ := LoadPresets()
183+ // qwen, reasoning on: the thinking sampling is copied from the card.
184+ dev := Decide(p, Choices{Provider: "llmman", Model: "qwen3.5-4b", Usage: "developer", Machine: "small"}, Facts{})
185+ if origin := settingOrigin(dev.Sampling, "temperature"); origin != OriginCard {
186+ t.Errorf("qwen thinking temperature: origin %q", origin)
187+ }
188+ if origin := settingOrigin(dev.Sampling, "max_tokens"); origin != OriginRule {
189+ t.Errorf("max_tokens: origin %q", origin)
190+ }
191+ if origin := settingOrigin(dev.Top, "maxOutput"); origin != OriginGuide {
192+ t.Errorf("maxOutput: origin %q", origin)
193+ }
194+ if origin := settingOrigin(dev.Top, "maxTurns"); origin != "" {
195+ t.Errorf("maxTurns follows from the usage, not the model: origin %q", origin)
196+ }
197+ // qwen, docs usage: the precise temperature was measured.
198+ docs := Decide(p, Choices{Provider: "llmman", Model: "qwen3.5-4b", Usage: "docs", Machine: "small"}, Facts{})
199+ if origin := settingOrigin(docs.Sampling, "temperature"); origin != OriginMeasured {
200+ t.Errorf("qwen precise temperature: origin %q", origin)
201+ }
202+ // The GGUF's own sampling, read from the server.
203+ gguf := Decide(p, Choices{Provider: "llmman", Model: "qwen3.5-4b", Usage: "writer", Machine: "small"}, Facts{Known: true, Sampling: map[string]float64{"top_k": 40}})
204+ if origin := settingOrigin(gguf.Sampling, "top_k"); origin != OriginServer {
205+ t.Errorf("GGUF top_k: origin %q", origin)
206+ }
207+
208+ text := Render(dev)
209+ for _, want := range []string{" # [card] family qwen:", "# [guide] settings guide", "# [card] from the model card", "# [rule] a rule of thumb"} {
210+ if !strings.Contains(text, want) {
211+ t.Errorf("missing %q:\n%s", want, text)
212+ }
213+ }
214+ if strings.Contains(text, "[server]") || strings.Contains(text, "[measured]") {
215+ t.Errorf("the legend explains a tag the file does not use:\n%s", text)
216+ }
217+ loadRendered(t, text)
218+}
219+
220+// The model line says what was chosen for it, and the header what the
221+// server said about the model.
222+func TestRenderModelCouplingAndFacts(t *testing.T) {
223+ p, _ := LoadPresets()
224+ facts := Facts{Known: true, ContextLength: 262144, Thinking: Thinking{Switch: "enable_thinking", Default: "off"}}
225+ text := Render(Decide(p, Choices{Provider: "llmman", Model: "qwen3.5-4b", Usage: "developer", Machine: "small"}, facts))
226+ flat := strings.ReplaceAll(text, "\n# ", " ")
227+ if !strings.Contains(flat, "The model. Chosen for it (family qwen): sampling (temperature, top_p, top_k, max_tokens) and sampling.chat_template_kwargs (enable_thinking). Change the model and redo them together") {
228+ t.Errorf("coupling comment missing:\n%s", text)
229+ }
230+ if !strings.Contains(text, "# model facts: read from llmman: native window 262144 tokens; reasoning switch enable_thinking (default off)\n") {
231+ t.Errorf("facts line missing:\n%s", text)
232+ }
233+ none := Render(Decide(p, Choices{Provider: "dmr", Model: "x", Usage: "assistant", Machine: "small"}, Facts{}))
234+ if !strings.Contains(none, "# model facts: none — dmr said nothing about this model beyond its name\n") {
235+ t.Errorf("facts line for a silent server:\n%s", none)
236+ }
237+}
238+
239+func settingOrigin(list []Setting, key string) string {
240+ for _, st := range list {
241+ if st.Key == key {
242+ return st.Origin
243+ }
244+ }
245+ return "<absent>"
246+}
@@ -145,7 +145,7 @@ func TestOpenAIFamilyAndProvider(t *testing.T) {
145 }145 }
146 text := Render(s)146 text := Render(s)
147 flat := strings.ReplaceAll(text, "\n# ", " ") // comments are wrapped147 flat := strings.ReplaceAll(text, "\n# ", " ") // comments are wrapped
148- if !strings.Contains(flat, "OPENAI_API_KEY") || !strings.Contains(flat, "No baseUrl: the OpenAI API") || !strings.Contains(flat, "cannot ask the server for its window") {148+ if !strings.Contains(flat, "OPENAI_API_KEY") || !strings.Contains(flat, `No baseUrl: openai's defaults apply. For the record: baseUrl: "https://api.openai.com/v1"`) || !strings.Contains(flat, "cannot ask the server for its window") {
149 t.Errorf("key / address comment missing:\n%s", text)149 t.Errorf("key / address comment missing:\n%s", text)
150 }150 }
151 loadRendered(t, text)151 loadRendered(t, text)
@@ -153,3 +153,94 @@ func TestOpenAIFamilyAndProvider(t *testing.T) {
153 t.Error("openai must not be probed for the first question's default")153 t.Error("openai must not be probed for the first question's default")
154 }154 }
155 }155 }
156+
157+// A file without baseUrl records the provider's default addresses as
158+// comments (ticket 121): with the fallback when there is one; a file with a
159+// baseUrl gets none of it.
160+func TestRenderRecordsTheDefaultURLs(t *testing.T) {
161+ p, _ := LoadPresets()
162+ llmman := Render(Decide(p, Choices{Provider: "llmman", Model: "qwen3", Usage: "assistant", Machine: "small"}, Facts{}))
163+ if !strings.Contains(llmman, "# baseUrl: \"http://127.0.0.1:17434/v1\"\n# fallback: \"http://host.docker.internal:17434/v1\" # tried when baseUrl does not answer\n") {
164+ t.Errorf("llmman's default URLs missing:\n%s", llmman)
165+ }
166+ llamacpp := Render(Decide(p, Choices{Provider: "llamacpp", Model: "qwen3", Usage: "assistant", Machine: "small"}, Facts{}))
167+ if !strings.Contains(llamacpp, "# baseUrl: \"http://127.0.0.1:8080/v1\"\n# (llamacpp has no fallback address)\n") {
168+ t.Errorf("llamacpp's default URL missing:\n%s", llamacpp)
169+ }
170+ own := Render(Decide(p, Choices{Provider: "llmman", BaseURL: "http://box:17434/v1", Model: "qwen3", Usage: "assistant", Machine: "small"}, Facts{}))
171+ if strings.Contains(own, "For the record") || !strings.Contains(own, "baseUrl: \"http://box:17434/v1\"") {
172+ t.Errorf("a file with its own baseUrl:\n%s", own)
173+ }
174+ // Uncommented, the two lines are the same settings, written out.
175+ uncommented := strings.Replace(strings.Replace(llmman, "# baseUrl:", "baseUrl:", 1), "# fallback:", "fallback:", 1)
176+ loadRendered(t, uncommented)
177+}
178+
179+// Every number about the model is tagged with how it was obtained, and the
180+// header explains the tags the file uses — only those (ticket 121).
181+func TestRenderTagsWhereTheNumbersComeFrom(t *testing.T) {
182+ p, _ := LoadPresets()
183+ // qwen, reasoning on: the thinking sampling is copied from the card.
184+ dev := Decide(p, Choices{Provider: "llmman", Model: "qwen3.5-4b", Usage: "developer", Machine: "small"}, Facts{})
185+ if origin := settingOrigin(dev.Sampling, "temperature"); origin != OriginCard {
186+ t.Errorf("qwen thinking temperature: origin %q", origin)
187+ }
188+ if origin := settingOrigin(dev.Sampling, "max_tokens"); origin != OriginRule {
189+ t.Errorf("max_tokens: origin %q", origin)
190+ }
191+ if origin := settingOrigin(dev.Top, "maxOutput"); origin != OriginGuide {
192+ t.Errorf("maxOutput: origin %q", origin)
193+ }
194+ if origin := settingOrigin(dev.Top, "maxTurns"); origin != "" {
195+ t.Errorf("maxTurns follows from the usage, not the model: origin %q", origin)
196+ }
197+ // qwen, docs usage: the precise temperature was measured.
198+ docs := Decide(p, Choices{Provider: "llmman", Model: "qwen3.5-4b", Usage: "docs", Machine: "small"}, Facts{})
199+ if origin := settingOrigin(docs.Sampling, "temperature"); origin != OriginMeasured {
200+ t.Errorf("qwen precise temperature: origin %q", origin)
201+ }
202+ // The GGUF's own sampling, read from the server.
203+ gguf := Decide(p, Choices{Provider: "llmman", Model: "qwen3.5-4b", Usage: "writer", Machine: "small"}, Facts{Known: true, Sampling: map[string]float64{"top_k": 40}})
204+ if origin := settingOrigin(gguf.Sampling, "top_k"); origin != OriginServer {
205+ t.Errorf("GGUF top_k: origin %q", origin)
206+ }
207+
208+ text := Render(dev)
209+ for _, want := range []string{" # [card] family qwen:", "# [guide] settings guide", "# [card] from the model card", "# [rule] a rule of thumb"} {
210+ if !strings.Contains(text, want) {
211+ t.Errorf("missing %q:\n%s", want, text)
212+ }
213+ }
214+ if strings.Contains(text, "[server]") || strings.Contains(text, "[measured]") {
215+ t.Errorf("the legend explains a tag the file does not use:\n%s", text)
216+ }
217+ loadRendered(t, text)
218+}
219+
220+// The model line says what was chosen for it, and the header what the
221+// server said about the model.
222+func TestRenderModelCouplingAndFacts(t *testing.T) {
223+ p, _ := LoadPresets()
224+ facts := Facts{Known: true, ContextLength: 262144, Thinking: Thinking{Switch: "enable_thinking", Default: "off"}}
225+ text := Render(Decide(p, Choices{Provider: "llmman", Model: "qwen3.5-4b", Usage: "developer", Machine: "small"}, facts))
226+ flat := strings.ReplaceAll(text, "\n# ", " ")
227+ if !strings.Contains(flat, "The model. Chosen for it (family qwen): sampling (temperature, top_p, top_k, max_tokens) and sampling.chat_template_kwargs (enable_thinking). Change the model and redo them together") {
228+ t.Errorf("coupling comment missing:\n%s", text)
229+ }
230+ if !strings.Contains(text, "# model facts: read from llmman: native window 262144 tokens; reasoning switch enable_thinking (default off)\n") {
231+ t.Errorf("facts line missing:\n%s", text)
232+ }
233+ none := Render(Decide(p, Choices{Provider: "dmr", Model: "x", Usage: "assistant", Machine: "small"}, Facts{}))
234+ if !strings.Contains(none, "# model facts: none — dmr said nothing about this model beyond its name\n") {
235+ t.Errorf("facts line for a silent server:\n%s", none)
236+ }
237+}
238+
239+func settingOrigin(list []Setting, key string) string {
240+ for _, st := range list {
241+ if st.Key == key {
242+ return st.Origin
243+ }
244+ }
245+ return "<absent>"
246+}
modified internal/agentinit/presets.go +4 -0
@@ -42,6 +42,10 @@ type Family struct {
4242 MaxTokensKey string `yaml:"maxTokensKey"` // "" = max_tokens
4343 Note string `yaml:"note"`
4444 Source string `yaml:"source"`
45+ // Origin says how General, Thinking and Precise were obtained, keyed
46+ // "general", "thinking", "precise": OriginMeasured, OriginCard or
47+ // OriginRule. The generated file tags their numbers with it.
48+ Origin map[string]string `yaml:"origin"`
4549 }
4650
4751 // Usage is what an agent is for.
@@ -42,6 +42,10 @@ type Family struct {
42 MaxTokensKey string `yaml:"maxTokensKey"` // "" = max_tokens42 MaxTokensKey string `yaml:"maxTokensKey"` // "" = max_tokens
43 Note string `yaml:"note"`43 Note string `yaml:"note"`
44 Source string `yaml:"source"`44 Source string `yaml:"source"`
45+ // Origin says how General, Thinking and Precise were obtained, keyed
46+ // "general", "thinking", "precise": OriginMeasured, OriginCard or
47+ // OriginRule. The generated file tags their numbers with it.
48+ Origin map[string]string `yaml:"origin"`
45 }49 }
46 50
47 // Usage is what an agent is for.51 // Usage is what an agent is for.
modified internal/agentinit/presets.yaml +21 -0
@@ -12,6 +12,13 @@
1212 # temperature for tool-heavy, thinking-off agents (developer, docs, JSON…).
1313 # When the model server tells the GGUF's own sampling (llmman, Ollama), it
1414 # replaces `general` and `thinking` — the GGUF is the most specific source.
15+#
16+# `origin` says, for `general`, `thinking` and `precise`, how each was
17+# obtained: `measured` (a demo of this repository ran with it), `card` (copied
18+# from the model card, the GGUF or the API reference, not measured here) or
19+# `rule` (a generic rule of thumb, nothing specific to the family). The
20+# generated file tags its numbers with it (ticket 121): after a model or
21+# quantization change, a measured number is re-measured, a card number re-read.
1522
1623 families:
1724 - name: qwen-coder
@@ -20,6 +27,7 @@ families:
2027 general: {temperature: 1.0, top_p: 0.95, top_k: 40}
2128 precise: 0.2
2229 source: "demos/qwen-style (Qwen3-Coder-Next, measured: 1.0 / 0.95 / 40 from the GGUF)"
30+ origin: {general: measured, precise: rule}
2331
2432 - name: qwen
2533 match: [qwen]
@@ -28,6 +36,7 @@ families:
2836 thinking: {temperature: 0.6, top_p: 0.95, top_k: 20}
2937 precise: 0.0
3038 source: "Qwen model cards (thinking 0.6 / 0.95 / 20, non-thinking 0.7 / 0.8 / 20); precise 0.0 measured with Qwen3.5 4B, thinking off (demos/docs-search, memory-search, plans)"
39+ origin: {general: card, thinking: card, precise: measured}
3140
3241 - name: gemma
3342 match: [gemma]
@@ -36,6 +45,7 @@ families:
3645 thinking: {temperature: 1.0, top_p: 0.95, top_k: 64}
3746 precise: 0.2
3847 source: "Gemma 4 GGUF sampling 1.0 / 0.95 / 64 (demos/gemini-style); precise 0.2 measured with gemma-4-E4B (demos/hooks, guards); enable_thinking is off by default in the template"
48+ origin: {general: card, thinking: card, precise: measured}
3949
4050 - name: mellum-thinking
4151 match: [mellum, thinking]
@@ -44,6 +54,7 @@ families:
4454 thinking: {temperature: 0.6, top_p: 0.95}
4555 precise: 0.2
4656 source: "demos/coding-helpers (Mellum2 12B Thinking: 0.6 / 0.95, thinking on); demos/programming-answers (Mellum2.1: 0.2 / 0.9)"
57+ origin: {general: measured, thinking: measured, precise: measured}
4758
4859 - name: mellum
4960 match: [mellum]
@@ -51,6 +62,7 @@ families:
5162 general: {temperature: 0.2, top_p: 0.9}
5263 precise: 0.0
5364 source: "demos/little-coder (Mellum2 12B Instruct, measured 0.2 / 0.9); agent.dmr.yaml (0.0 / 0.9)"
65+ origin: {general: measured, precise: rule}
5466
5567 - name: gpt-oss
5668 match: [gpt-oss]
@@ -60,6 +72,7 @@ families:
6072 thinking: {temperature: 1.0, top_p: 1.0}
6173 precise: 1.0
6274 source: "demos/codex-style (gpt-oss 20B: 1.0 / 1.0, harmony format, reasoning_effort low | medium | high, default medium) — the model always reasons: never a low temperature"
75+ origin: {general: measured, thinking: measured, precise: measured}
6376
6477 - name: openai
6578 match: [gpt-]
@@ -69,6 +82,7 @@ families:
6982 precise: 1.0
7083 note: "a hosted OpenAI model: its reasoning models (gpt-5, the o-series) accept only the default temperature and take max_completion_tokens instead of max_tokens — remove a sampling key the API refuses"
7184 source: "OpenAI API reference; not measured in this repository"
85+ origin: {general: card, precise: card}
7286
7387 - name: minicpm
7488 match: [minicpm]
@@ -77,6 +91,7 @@ families:
7791 thinking: {temperature: 0.6, top_p: 0.95}
7892 precise: 0.3
7993 source: "demos/coding-helpers (MiniCPM5 2B: 0.3 / 0.95, enable_thinking false — its template has no default: always set it)"
94+ origin: {general: measured, thinking: rule, precise: measured}
8095
8196 - name: ornith
8297 match: [ornith]
@@ -85,6 +100,7 @@ families:
85100 thinking: {temperature: 0.6, top_p: 0.95, top_k: 20}
86101 precise: 0.3
87102 source: "demos/team (Ornith 1.5 9B: 0.3–0.4 / 0.8 / 20 thinking off, 0.6 / 0.95 / 20 thinking on)"
103+ origin: {general: measured, thinking: measured, precise: measured}
88104
89105 - name: granite-guardian
90106 match: [granite, guardian]
@@ -93,6 +109,7 @@ families:
93109 precise: 0.0
94110 note: "a guard model: it answers Yes / No about a risk named in the system message, it is not a general agent — see demos/guard"
95111 source: "demos/guard (Granite Guardian 3.2 3B-A800M: temperature 0.0)"
112+ origin: {general: measured, precise: measured}
96113
97114 - name: menlo
98115 match: [menlo]
@@ -100,6 +117,7 @@ families:
100117 general: {temperature: 0.0, top_p: 0.9}
101118 precise: 0.0
102119 source: "demos/llmman (Jan-nano, Lucy: 0.0 / 0.9)"
120+ origin: {general: measured, precise: measured}
103121
104122 - name: llama
105123 match: [llama]
@@ -107,6 +125,7 @@ families:
107125 general: {temperature: 0.6, top_p: 0.9}
108126 precise: 0.2
109127 source: "Llama 3.1 8B Instruct generation_config.json (0.6 / 0.9); no measured demo in this repository"
128+ origin: {general: card, precise: rule}
110129
111130 - name: mistral
112131 match: [mistral]
@@ -114,6 +133,7 @@ families:
114133 general: {temperature: 0.15}
115134 precise: 0.15
116135 source: "Mistral Small 3.1 model card (\"a relatively low temperature, such as temperature=0.15\"); no measured demo in this repository"
136+ origin: {general: card, precise: card}
117137
118138 - name: other
119139 match: []
@@ -121,6 +141,7 @@ families:
121141 general: {temperature: 0.7, top_p: 0.9}
122142 precise: 0.2
123143 source: "skills/create-agent rule when nothing is published: 0.7 / 0.9, 0.2 for a code agent without reasoning"
144+ origin: {general: rule, precise: rule}
124145
125146 # Usages: what the agent is for. `tools` are the YAML switches; `thinking`
126147 # asks for reasoning when the family has a switch; `precise` picks the
@@ -12,6 +12,13 @@
12 # temperature for tool-heavy, thinking-off agents (developer, docs, JSON…).12 # temperature for tool-heavy, thinking-off agents (developer, docs, JSON…).
13 # When the model server tells the GGUF's own sampling (llmman, Ollama), it13 # When the model server tells the GGUF's own sampling (llmman, Ollama), it
14 # replaces `general` and `thinking` — the GGUF is the most specific source.14 # replaces `general` and `thinking` — the GGUF is the most specific source.
15+#
16+# `origin` says, for `general`, `thinking` and `precise`, how each was
17+# obtained: `measured` (a demo of this repository ran with it), `card` (copied
18+# from the model card, the GGUF or the API reference, not measured here) or
19+# `rule` (a generic rule of thumb, nothing specific to the family). The
20+# generated file tags its numbers with it (ticket 121): after a model or
21+# quantization change, a measured number is re-measured, a card number re-read.
15 22
16 families:23 families:
17 - name: qwen-coder24 - name: qwen-coder
@@ -20,6 +27,7 @@ families:
20 general: {temperature: 1.0, top_p: 0.95, top_k: 40}27 general: {temperature: 1.0, top_p: 0.95, top_k: 40}
21 precise: 0.228 precise: 0.2
22 source: "demos/qwen-style (Qwen3-Coder-Next, measured: 1.0 / 0.95 / 40 from the GGUF)"29 source: "demos/qwen-style (Qwen3-Coder-Next, measured: 1.0 / 0.95 / 40 from the GGUF)"
30+ origin: {general: measured, precise: rule}
23 31
24 - name: qwen32 - name: qwen
25 match: [qwen]33 match: [qwen]
@@ -28,6 +36,7 @@ families:
28 thinking: {temperature: 0.6, top_p: 0.95, top_k: 20}36 thinking: {temperature: 0.6, top_p: 0.95, top_k: 20}
29 precise: 0.037 precise: 0.0
30 source: "Qwen model cards (thinking 0.6 / 0.95 / 20, non-thinking 0.7 / 0.8 / 20); precise 0.0 measured with Qwen3.5 4B, thinking off (demos/docs-search, memory-search, plans)"38 source: "Qwen model cards (thinking 0.6 / 0.95 / 20, non-thinking 0.7 / 0.8 / 20); precise 0.0 measured with Qwen3.5 4B, thinking off (demos/docs-search, memory-search, plans)"
39+ origin: {general: card, thinking: card, precise: measured}
31 40
32 - name: gemma41 - name: gemma
33 match: [gemma]42 match: [gemma]
@@ -36,6 +45,7 @@ families:
36 thinking: {temperature: 1.0, top_p: 0.95, top_k: 64}45 thinking: {temperature: 1.0, top_p: 0.95, top_k: 64}
37 precise: 0.246 precise: 0.2
38 source: "Gemma 4 GGUF sampling 1.0 / 0.95 / 64 (demos/gemini-style); precise 0.2 measured with gemma-4-E4B (demos/hooks, guards); enable_thinking is off by default in the template"47 source: "Gemma 4 GGUF sampling 1.0 / 0.95 / 64 (demos/gemini-style); precise 0.2 measured with gemma-4-E4B (demos/hooks, guards); enable_thinking is off by default in the template"
48+ origin: {general: card, thinking: card, precise: measured}
39 49
40 - name: mellum-thinking50 - name: mellum-thinking
41 match: [mellum, thinking]51 match: [mellum, thinking]
@@ -44,6 +54,7 @@ families:
44 thinking: {temperature: 0.6, top_p: 0.95}54 thinking: {temperature: 0.6, top_p: 0.95}
45 precise: 0.255 precise: 0.2
46 source: "demos/coding-helpers (Mellum2 12B Thinking: 0.6 / 0.95, thinking on); demos/programming-answers (Mellum2.1: 0.2 / 0.9)"56 source: "demos/coding-helpers (Mellum2 12B Thinking: 0.6 / 0.95, thinking on); demos/programming-answers (Mellum2.1: 0.2 / 0.9)"
57+ origin: {general: measured, thinking: measured, precise: measured}
47 58
48 - name: mellum59 - name: mellum
49 match: [mellum]60 match: [mellum]
@@ -51,6 +62,7 @@ families:
51 general: {temperature: 0.2, top_p: 0.9}62 general: {temperature: 0.2, top_p: 0.9}
52 precise: 0.063 precise: 0.0
53 source: "demos/little-coder (Mellum2 12B Instruct, measured 0.2 / 0.9); agent.dmr.yaml (0.0 / 0.9)"64 source: "demos/little-coder (Mellum2 12B Instruct, measured 0.2 / 0.9); agent.dmr.yaml (0.0 / 0.9)"
65+ origin: {general: measured, precise: rule}
54 66
55 - name: gpt-oss67 - name: gpt-oss
56 match: [gpt-oss]68 match: [gpt-oss]
@@ -60,6 +72,7 @@ families:
60 thinking: {temperature: 1.0, top_p: 1.0}72 thinking: {temperature: 1.0, top_p: 1.0}
61 precise: 1.073 precise: 1.0
62 source: "demos/codex-style (gpt-oss 20B: 1.0 / 1.0, harmony format, reasoning_effort low | medium | high, default medium) — the model always reasons: never a low temperature"74 source: "demos/codex-style (gpt-oss 20B: 1.0 / 1.0, harmony format, reasoning_effort low | medium | high, default medium) — the model always reasons: never a low temperature"
75+ origin: {general: measured, thinking: measured, precise: measured}
63 76
64 - name: openai77 - name: openai
65 match: [gpt-]78 match: [gpt-]
@@ -69,6 +82,7 @@ families:
69 precise: 1.082 precise: 1.0
70 note: "a hosted OpenAI model: its reasoning models (gpt-5, the o-series) accept only the default temperature and take max_completion_tokens instead of max_tokens — remove a sampling key the API refuses"83 note: "a hosted OpenAI model: its reasoning models (gpt-5, the o-series) accept only the default temperature and take max_completion_tokens instead of max_tokens — remove a sampling key the API refuses"
71 source: "OpenAI API reference; not measured in this repository"84 source: "OpenAI API reference; not measured in this repository"
85+ origin: {general: card, precise: card}
72 86
73 - name: minicpm87 - name: minicpm
74 match: [minicpm]88 match: [minicpm]
@@ -77,6 +91,7 @@ families:
77 thinking: {temperature: 0.6, top_p: 0.95}91 thinking: {temperature: 0.6, top_p: 0.95}
78 precise: 0.392 precise: 0.3
79 source: "demos/coding-helpers (MiniCPM5 2B: 0.3 / 0.95, enable_thinking false — its template has no default: always set it)"93 source: "demos/coding-helpers (MiniCPM5 2B: 0.3 / 0.95, enable_thinking false — its template has no default: always set it)"
94+ origin: {general: measured, thinking: rule, precise: measured}
80 95
81 - name: ornith96 - name: ornith
82 match: [ornith]97 match: [ornith]
@@ -85,6 +100,7 @@ families:
85 thinking: {temperature: 0.6, top_p: 0.95, top_k: 20}100 thinking: {temperature: 0.6, top_p: 0.95, top_k: 20}
86 precise: 0.3101 precise: 0.3
87 source: "demos/team (Ornith 1.5 9B: 0.3–0.4 / 0.8 / 20 thinking off, 0.6 / 0.95 / 20 thinking on)"102 source: "demos/team (Ornith 1.5 9B: 0.3–0.4 / 0.8 / 20 thinking off, 0.6 / 0.95 / 20 thinking on)"
103+ origin: {general: measured, thinking: measured, precise: measured}
88 104
89 - name: granite-guardian105 - name: granite-guardian
90 match: [granite, guardian]106 match: [granite, guardian]
@@ -93,6 +109,7 @@ families:
93 precise: 0.0109 precise: 0.0
94 note: "a guard model: it answers Yes / No about a risk named in the system message, it is not a general agent — see demos/guard"110 note: "a guard model: it answers Yes / No about a risk named in the system message, it is not a general agent — see demos/guard"
95 source: "demos/guard (Granite Guardian 3.2 3B-A800M: temperature 0.0)"111 source: "demos/guard (Granite Guardian 3.2 3B-A800M: temperature 0.0)"
112+ origin: {general: measured, precise: measured}
96 113
97 - name: menlo114 - name: menlo
98 match: [menlo]115 match: [menlo]
@@ -100,6 +117,7 @@ families:
100 general: {temperature: 0.0, top_p: 0.9}117 general: {temperature: 0.0, top_p: 0.9}
101 precise: 0.0118 precise: 0.0
102 source: "demos/llmman (Jan-nano, Lucy: 0.0 / 0.9)"119 source: "demos/llmman (Jan-nano, Lucy: 0.0 / 0.9)"
120+ origin: {general: measured, precise: measured}
103 121
104 - name: llama122 - name: llama
105 match: [llama]123 match: [llama]
@@ -107,6 +125,7 @@ families:
107 general: {temperature: 0.6, top_p: 0.9}125 general: {temperature: 0.6, top_p: 0.9}
108 precise: 0.2126 precise: 0.2
109 source: "Llama 3.1 8B Instruct generation_config.json (0.6 / 0.9); no measured demo in this repository"127 source: "Llama 3.1 8B Instruct generation_config.json (0.6 / 0.9); no measured demo in this repository"
128+ origin: {general: card, precise: rule}
110 129
111 - name: mistral130 - name: mistral
112 match: [mistral]131 match: [mistral]
@@ -114,6 +133,7 @@ families:
114 general: {temperature: 0.15}133 general: {temperature: 0.15}
115 precise: 0.15134 precise: 0.15
116 source: "Mistral Small 3.1 model card (\"a relatively low temperature, such as temperature=0.15\"); no measured demo in this repository"135 source: "Mistral Small 3.1 model card (\"a relatively low temperature, such as temperature=0.15\"); no measured demo in this repository"
136+ origin: {general: card, precise: card}
117 137
118 - name: other138 - name: other
119 match: []139 match: []
@@ -121,6 +141,7 @@ families:
121 general: {temperature: 0.7, top_p: 0.9}141 general: {temperature: 0.7, top_p: 0.9}
122 precise: 0.2142 precise: 0.2
123 source: "skills/create-agent rule when nothing is published: 0.7 / 0.9, 0.2 for a code agent without reasoning"143 source: "skills/create-agent rule when nothing is published: 0.7 / 0.9, 0.2 for a code agent without reasoning"
144+ origin: {general: rule, precise: rule}
124 145
125 # Usages: what the agent is for. `tools` are the YAML switches; `thinking`146 # Usages: what the agent is for. `tools` are the YAML switches; `thinking`
126 # asks for reasoning when the family has a switch; `precise` picks the147 # asks for reasoning when the family has a switch; `precise` picks the
modified internal/agentinit/presets_test.go +10 -0
@@ -20,6 +20,16 @@ func TestPresetsLoadAndEveryEntryIsSourced(t *testing.T) {
2020 if f.Reasoning != "none" && f.Thinking.Temperature == nil {
2121 t.Errorf("family %s reasons but has no thinking sampling", f.Name)
2222 }
23+ // Every block of numbers says how it was obtained (ticket 121).
24+ blocks := []string{"general", "precise"}
25+ if f.Thinking.Temperature != nil {
26+ blocks = append(blocks, "thinking")
27+ }
28+ for _, b := range blocks {
29+ if o := f.Origin[b]; o != OriginMeasured && o != OriginCard && o != OriginRule {
30+ t.Errorf("family %s: origin.%s = %q, want measured, card or rule", f.Name, b, o)
31+ }
32+ }
2333 }
2434 for _, u := range p.Usages {
2535 if u.System == "" || u.MaxTurns <= 0 || u.Description == "" {
@@ -20,6 +20,16 @@ func TestPresetsLoadAndEveryEntryIsSourced(t *testing.T) {
20 if f.Reasoning != "none" && f.Thinking.Temperature == nil {20 if f.Reasoning != "none" && f.Thinking.Temperature == nil {
21 t.Errorf("family %s reasons but has no thinking sampling", f.Name)21 t.Errorf("family %s reasons but has no thinking sampling", f.Name)
22 }22 }
23+ // Every block of numbers says how it was obtained (ticket 121).
24+ blocks := []string{"general", "precise"}
25+ if f.Thinking.Temperature != nil {
26+ blocks = append(blocks, "thinking")
27+ }
28+ for _, b := range blocks {
29+ if o := f.Origin[b]; o != OriginMeasured && o != OriginCard && o != OriginRule {
30+ t.Errorf("family %s: origin.%s = %q, want measured, card or rule", f.Name, b, o)
31+ }
32+ }
23 }33 }
24 for _, u := range p.Usages {34 for _, u := range p.Usages {
25 if u.System == "" || u.MaxTurns <= 0 || u.Description == "" {35 if u.System == "" || u.MaxTurns <= 0 || u.Description == "" {
modified internal/agentinit/render.go +119 -5
@@ -4,6 +4,8 @@ import (
44 "fmt"
55 "strconv"
66 "strings"
7+
8+ "mm/internal/engine"
79 )
810
911 // Render writes the settings as a commented agent.yaml: every value with the
@@ -16,17 +18,24 @@ func Render(s Settings) string {
1618 b.WriteString("#\n# Written by `mm init`. Every value says why it has that value; change\n")
1719 b.WriteString("# them as you watch the agent work (guides/agent-settings-for-small-models.md).\n#\n")
1820 fmt.Fprintf(&b, "# model family: %s\n# usage: %s\n# machine: %s — %s\n", s.Family.Name, u.Name, s.Machine.Name, s.Machine.Description)
21+ fmt.Fprintf(&b, "# model facts: %s\n", factsLine(s))
22+ writeLegend(&b, s)
1923 fmt.Fprintf(&b, "#\n# mm %s or mm -tui %s\n\n", "-config <this file>", "<this file>")
2024
2125 fmt.Fprintf(&b, "provider: %s\n", c.Provider)
26+ for _, line := range wrap(coupledTo(s), 76) {
27+ fmt.Fprintf(&b, "# %s\n", line)
28+ }
2229 fmt.Fprintf(&b, "model: %s\n", quote(c.Model))
2330 if c.BaseURL != "" {
2431 fmt.Fprintf(&b, "baseUrl: %s\n", quote(c.BaseURL))
32+ } else {
33+ writeDefaultURLs(&b, c.Provider)
2534 }
2635 if c.Provider == "openai" {
2736 b.WriteString("# The key, when the server needs one, is read from OPENAI_API_KEY (or the\n# variable apiKeyEnv names): export it — it never goes in this file.\n")
2837 if c.BaseURL == "" {
29- b.WriteString("# No baseUrl: the OpenAI API. Set one for any other OpenAI-compatible server.\n")
38+ b.WriteString("# The default is the OpenAI API: set baseUrl for any other OpenAI-compatible server.\n")
3039 }
3140 }
3241 b.WriteString("\n")
@@ -100,20 +109,125 @@ const jsonExample = `type Contact struct {
100109 var Answer Contact`
101110
102111 // writeSettings writes a block of settings; consecutive settings with the
103-// same reason share one comment.
112+// same reason and origin share one comment, which starts with the origin's
113+// tag ("[measured] …") when the setting has one.
104114 func writeSettings(b *strings.Builder, indent string, settings []Setting) {
105115 last := ""
106116 for _, st := range settings {
107- if st.Reason != last {
108- for _, line := range wrap(st.Reason, 76-len(indent)) {
117+ comment := st.Reason
118+ if st.Origin != "" {
119+ comment = "[" + st.Origin + "] " + comment
120+ }
121+ if comment != last {
122+ for _, line := range wrap(comment, 76-len(indent)) {
109123 fmt.Fprintf(b, "%s# %s\n", indent, line)
110124 }
111- last = st.Reason
125+ last = comment
112126 }
113127 fmt.Fprintf(b, "%s%s: %s\n", indent, st.Key, st.Value)
114128 }
115129 }
116130
131+// legend explains each origin tag, in the order the header lists them.
132+var legend = []struct{ origin, text string }{
133+ {OriginMeasured, "measured in a demo of mm — re-measure after a model or quant change"},
134+ {OriginCard, "from the model card or the GGUF, not measured — re-read the new card"},
135+ {OriginServer, "read from the server — run mm init again after a change"},
136+ {OriginGuide, "the settings guide's row for this machine"},
137+ {OriginRule, "a rule of thumb, not specific to this model"},
138+}
139+
140+// writeLegend explains the origin tags the file uses, and only those.
141+func writeLegend(b *strings.Builder, s Settings) {
142+ used := map[string]bool{}
143+ for _, block := range [][]Setting{s.Top, s.Sampling, s.Kwargs, s.Context} {
144+ for _, st := range block {
145+ used[st.Origin] = true
146+ }
147+ }
148+ first := true
149+ for _, l := range legend {
150+ if !used[l.origin] {
151+ continue
152+ }
153+ if first {
154+ b.WriteString("#\n# Where the numbers come from (the tag before their comment):\n")
155+ first = false
156+ }
157+ fmt.Fprintf(b, "# %-10s %s\n", "["+l.origin+"]", l.text)
158+ }
159+}
160+
161+// factsLine is what the server said about the model when the file was
162+// written: what to compare with when the model or the server changes.
163+func factsLine(s Settings) string {
164+ f := s.Facts
165+ if !f.Known {
166+ return "none — " + s.Choices.Provider + " said nothing about this model beyond its name"
167+ }
168+ var parts []string
169+ if f.ContextLength > 0 {
170+ parts = append(parts, fmt.Sprintf("native window %d tokens", f.ContextLength))
171+ }
172+ if len(f.Sampling) > 0 {
173+ keys := make([]string, 0, len(f.Sampling))
174+ for _, k := range []string{"temperature", "top_p", "top_k", "min_p"} {
175+ if v, ok := f.Sampling[k]; ok {
176+ keys = append(keys, k+" "+number(k, v))
177+ }
178+ }
179+ parts = append(parts, "GGUF sampling "+strings.Join(keys, ", "))
180+ }
181+ if f.Thinking.Switch != "" {
182+ sw := "reasoning switch " + f.Thinking.Switch
183+ if f.Thinking.Default != "" {
184+ sw += " (default " + f.Thinking.Default + ")"
185+ }
186+ parts = append(parts, sw)
187+ }
188+ if len(parts) == 0 {
189+ return "read from " + s.Choices.Provider + ": nothing beyond the model's name"
190+ }
191+ return "read from " + s.Choices.Provider + ": " + strings.Join(parts, "; ")
192+}
193+
194+// coupledTo is the comment above the model line: the settings chosen for
195+// this model, which a change of model invalidates together.
196+func coupledTo(s Settings) string {
197+ var sampling, kwargs []string
198+ for _, st := range s.Sampling {
199+ if st.Key != "parallel_tool_calls" {
200+ sampling = append(sampling, st.Key)
201+ }
202+ }
203+ for _, st := range s.Kwargs {
204+ kwargs = append(kwargs, st.Key)
205+ }
206+ text := "The model. Chosen for it (family " + s.Family.Name + "): sampling (" + strings.Join(sampling, ", ") + ")"
207+ if len(kwargs) > 0 {
208+ text += " and sampling.chat_template_kwargs (" + strings.Join(kwargs, ", ") + ")"
209+ }
210+ return text + ". Change the model and redo them together — running mm init again is the quick way."
211+}
212+
213+// writeDefaultURLs records, as comments, the addresses a file without
214+// baseUrl relies on: the provider's defaults may change, and the file then
215+// still says what they were when it was written. Uncommented, they are the
216+// same settings, written out.
217+func writeDefaultURLs(b *strings.Builder, provider string) {
218+ base, fallback, ok := engine.DefaultURLs(provider)
219+ if !ok {
220+ return
221+ }
222+ fmt.Fprintf(b, "# No baseUrl: %s's defaults apply. For the record:\n", provider)
223+ fmt.Fprintf(b, "# baseUrl: %s\n", quote(base))
224+ if fallback != "" {
225+ fmt.Fprintf(b, "# fallback: %s # tried when baseUrl does not answer\n", quote(fallback))
226+ } else {
227+ fmt.Fprintf(b, "# (%s has no fallback address)\n", provider)
228+ }
229+}
230+
117231 func titleOf(name string, u Usage) string {
118232 if name == "" {
119233 return u.Description
@@ -4,6 +4,8 @@ import (
4 "fmt"4 "fmt"
5 "strconv"5 "strconv"
6 "strings"6 "strings"
7+
8+ "mm/internal/engine"
7 )9 )
8 10
9 // Render writes the settings as a commented agent.yaml: every value with the11 // Render writes the settings as a commented agent.yaml: every value with the
@@ -16,17 +18,24 @@ func Render(s Settings) string {
16 b.WriteString("#\n# Written by `mm init`. Every value says why it has that value; change\n")18 b.WriteString("#\n# Written by `mm init`. Every value says why it has that value; change\n")
17 b.WriteString("# them as you watch the agent work (guides/agent-settings-for-small-models.md).\n#\n")19 b.WriteString("# them as you watch the agent work (guides/agent-settings-for-small-models.md).\n#\n")
18 fmt.Fprintf(&b, "# model family: %s\n# usage: %s\n# machine: %s — %s\n", s.Family.Name, u.Name, s.Machine.Name, s.Machine.Description)20 fmt.Fprintf(&b, "# model family: %s\n# usage: %s\n# machine: %s — %s\n", s.Family.Name, u.Name, s.Machine.Name, s.Machine.Description)
21+ fmt.Fprintf(&b, "# model facts: %s\n", factsLine(s))
22+ writeLegend(&b, s)
19 fmt.Fprintf(&b, "#\n# mm %s or mm -tui %s\n\n", "-config <this file>", "<this file>")23 fmt.Fprintf(&b, "#\n# mm %s or mm -tui %s\n\n", "-config <this file>", "<this file>")
20 24
21 fmt.Fprintf(&b, "provider: %s\n", c.Provider)25 fmt.Fprintf(&b, "provider: %s\n", c.Provider)
26+ for _, line := range wrap(coupledTo(s), 76) {
27+ fmt.Fprintf(&b, "# %s\n", line)
28+ }
22 fmt.Fprintf(&b, "model: %s\n", quote(c.Model))29 fmt.Fprintf(&b, "model: %s\n", quote(c.Model))
23 if c.BaseURL != "" {30 if c.BaseURL != "" {
24 fmt.Fprintf(&b, "baseUrl: %s\n", quote(c.BaseURL))31 fmt.Fprintf(&b, "baseUrl: %s\n", quote(c.BaseURL))
32+ } else {
33+ writeDefaultURLs(&b, c.Provider)
25 }34 }
26 if c.Provider == "openai" {35 if c.Provider == "openai" {
27 b.WriteString("# The key, when the server needs one, is read from OPENAI_API_KEY (or the\n# variable apiKeyEnv names): export it — it never goes in this file.\n")36 b.WriteString("# The key, when the server needs one, is read from OPENAI_API_KEY (or the\n# variable apiKeyEnv names): export it — it never goes in this file.\n")
28 if c.BaseURL == "" {37 if c.BaseURL == "" {
29- b.WriteString("# No baseUrl: the OpenAI API. Set one for any other OpenAI-compatible server.\n")38+ b.WriteString("# The default is the OpenAI API: set baseUrl for any other OpenAI-compatible server.\n")
30 }39 }
31 }40 }
32 b.WriteString("\n")41 b.WriteString("\n")
@@ -100,20 +109,125 @@ const jsonExample = `type Contact struct {
100 var Answer Contact`109 var Answer Contact`
101 110
102 // writeSettings writes a block of settings; consecutive settings with the111 // writeSettings writes a block of settings; consecutive settings with the
103-// same reason share one comment.112+// same reason and origin share one comment, which starts with the origin's
113+// tag ("[measured] …") when the setting has one.
104 func writeSettings(b *strings.Builder, indent string, settings []Setting) {114 func writeSettings(b *strings.Builder, indent string, settings []Setting) {
105 last := ""115 last := ""
106 for _, st := range settings {116 for _, st := range settings {
107- if st.Reason != last {117+ comment := st.Reason
108- for _, line := range wrap(st.Reason, 76-len(indent)) {118+ if st.Origin != "" {
119+ comment = "[" + st.Origin + "] " + comment
120+ }
121+ if comment != last {
122+ for _, line := range wrap(comment, 76-len(indent)) {
109 fmt.Fprintf(b, "%s# %s\n", indent, line)123 fmt.Fprintf(b, "%s# %s\n", indent, line)
110 }124 }
111- last = st.Reason125+ last = comment
112 }126 }
113 fmt.Fprintf(b, "%s%s: %s\n", indent, st.Key, st.Value)127 fmt.Fprintf(b, "%s%s: %s\n", indent, st.Key, st.Value)
114 }128 }
115 }129 }
116 130
131+// legend explains each origin tag, in the order the header lists them.
132+var legend = []struct{ origin, text string }{
133+ {OriginMeasured, "measured in a demo of mm — re-measure after a model or quant change"},
134+ {OriginCard, "from the model card or the GGUF, not measured — re-read the new card"},
135+ {OriginServer, "read from the server — run mm init again after a change"},
136+ {OriginGuide, "the settings guide's row for this machine"},
137+ {OriginRule, "a rule of thumb, not specific to this model"},
138+}
139+
140+// writeLegend explains the origin tags the file uses, and only those.
141+func writeLegend(b *strings.Builder, s Settings) {
142+ used := map[string]bool{}
143+ for _, block := range [][]Setting{s.Top, s.Sampling, s.Kwargs, s.Context} {
144+ for _, st := range block {
145+ used[st.Origin] = true
146+ }
147+ }
148+ first := true
149+ for _, l := range legend {
150+ if !used[l.origin] {
151+ continue
152+ }
153+ if first {
154+ b.WriteString("#\n# Where the numbers come from (the tag before their comment):\n")
155+ first = false
156+ }
157+ fmt.Fprintf(b, "# %-10s %s\n", "["+l.origin+"]", l.text)
158+ }
159+}
160+
161+// factsLine is what the server said about the model when the file was
162+// written: what to compare with when the model or the server changes.
163+func factsLine(s Settings) string {
164+ f := s.Facts
165+ if !f.Known {
166+ return "none — " + s.Choices.Provider + " said nothing about this model beyond its name"
167+ }
168+ var parts []string
169+ if f.ContextLength > 0 {
170+ parts = append(parts, fmt.Sprintf("native window %d tokens", f.ContextLength))
171+ }
172+ if len(f.Sampling) > 0 {
173+ keys := make([]string, 0, len(f.Sampling))
174+ for _, k := range []string{"temperature", "top_p", "top_k", "min_p"} {
175+ if v, ok := f.Sampling[k]; ok {
176+ keys = append(keys, k+" "+number(k, v))
177+ }
178+ }
179+ parts = append(parts, "GGUF sampling "+strings.Join(keys, ", "))
180+ }
181+ if f.Thinking.Switch != "" {
182+ sw := "reasoning switch " + f.Thinking.Switch
183+ if f.Thinking.Default != "" {
184+ sw += " (default " + f.Thinking.Default + ")"
185+ }
186+ parts = append(parts, sw)
187+ }
188+ if len(parts) == 0 {
189+ return "read from " + s.Choices.Provider + ": nothing beyond the model's name"
190+ }
191+ return "read from " + s.Choices.Provider + ": " + strings.Join(parts, "; ")
192+}
193+
194+// coupledTo is the comment above the model line: the settings chosen for
195+// this model, which a change of model invalidates together.
196+func coupledTo(s Settings) string {
197+ var sampling, kwargs []string
198+ for _, st := range s.Sampling {
199+ if st.Key != "parallel_tool_calls" {
200+ sampling = append(sampling, st.Key)
201+ }
202+ }
203+ for _, st := range s.Kwargs {
204+ kwargs = append(kwargs, st.Key)
205+ }
206+ text := "The model. Chosen for it (family " + s.Family.Name + "): sampling (" + strings.Join(sampling, ", ") + ")"
207+ if len(kwargs) > 0 {
208+ text += " and sampling.chat_template_kwargs (" + strings.Join(kwargs, ", ") + ")"
209+ }
210+ return text + ". Change the model and redo them together — running mm init again is the quick way."
211+}
212+
213+// writeDefaultURLs records, as comments, the addresses a file without
214+// baseUrl relies on: the provider's defaults may change, and the file then
215+// still says what they were when it was written. Uncommented, they are the
216+// same settings, written out.
217+func writeDefaultURLs(b *strings.Builder, provider string) {
218+ base, fallback, ok := engine.DefaultURLs(provider)
219+ if !ok {
220+ return
221+ }
222+ fmt.Fprintf(b, "# No baseUrl: %s's defaults apply. For the record:\n", provider)
223+ fmt.Fprintf(b, "# baseUrl: %s\n", quote(base))
224+ if fallback != "" {
225+ fmt.Fprintf(b, "# fallback: %s # tried when baseUrl does not answer\n", quote(fallback))
226+ } else {
227+ fmt.Fprintf(b, "# (%s has no fallback address)\n", provider)
228+ }
229+}
230+
117 func titleOf(name string, u Usage) string {231 func titleOf(name string, u Usage) string {
118 if name == "" {232 if name == "" {
119 return u.Description233 return u.Description
modified internal/engine/openai_compat.go +16 -0
@@ -64,6 +64,22 @@ type openaiCompat struct {
6464 unload unloadFunc
6565 }
6666
67+// DefaultURLs returns the address a provider uses when the config gives no
68+// baseUrl, and the one it falls back to when that address does not answer
69+// ("" = no fallback). ok is false for a provider this build does not know.
70+// `mm init` writes them as a comment, so a file that relies on them says
71+// what they were (ticket 121).
72+//
73+// base, fallback, _ := engine.DefaultURLs("llmman")
74+// // "http://127.0.0.1:17434/v1", "http://host.docker.internal:17434/v1"
75+func DefaultURLs(provider string) (baseURL, fallback string, ok bool) {
76+ p, found := registry[provider].(*openaiCompat)
77+ if !found {
78+ return "", "", false
79+ }
80+ return p.baseURL, p.fallback, true
81+}
82+
6783 func init() {
6884 register(&openaiCompat{
6985 name: "dmr",
@@ -64,6 +64,22 @@ type openaiCompat struct {
64 unload unloadFunc64 unload unloadFunc
65 }65 }
66 66
67+// DefaultURLs returns the address a provider uses when the config gives no
68+// baseUrl, and the one it falls back to when that address does not answer
69+// ("" = no fallback). ok is false for a provider this build does not know.
70+// `mm init` writes them as a comment, so a file that relies on them says
71+// what they were (ticket 121).
72+//
73+// base, fallback, _ := engine.DefaultURLs("llmman")
74+// // "http://127.0.0.1:17434/v1", "http://host.docker.internal:17434/v1"
75+func DefaultURLs(provider string) (baseURL, fallback string, ok bool) {
76+ p, found := registry[provider].(*openaiCompat)
77+ if !found {
78+ return "", "", false
79+ }
80+ return p.baseURL, p.fallback, true
81+}
82+
67 func init() {83 func init() {
68 register(&openaiCompat{84 register(&openaiCompat{
69 name: "dmr",85 name: "dmr",
modified internal/engine/provider_test.go +21 -0
@@ -547,3 +547,24 @@ func TestOpenKeepsGenkitQuiet(t *testing.T) {
547547 t.Errorf("Genkit log level = %v after Open, want warn or above", got)
548548 }
549549 }
550+
551+// DefaultURLs gives the addresses a provider uses with no baseUrl: the
552+// fallback when it has one, nothing for an unknown provider.
553+func TestDefaultURLs(t *testing.T) {
554+ cases := []struct{ provider, base, fallback string }{
555+ {"llmman", "http://127.0.0.1:17434/v1", "http://host.docker.internal:17434/v1"},
556+ {"ollama", "http://localhost:11434/v1", "http://host.docker.internal:11434/v1"},
557+ {"dmr", "http://localhost:12434/engines/v1", "http://host.docker.internal:12434/engines/v1"},
558+ {"llamacpp", "http://127.0.0.1:8080/v1", ""},
559+ {"openai", "https://api.openai.com/v1", ""},
560+ }
561+ for _, c := range cases {
562+ base, fallback, ok := DefaultURLs(c.provider)
563+ if !ok || base != c.base || fallback != c.fallback {
564+ t.Errorf("DefaultURLs(%s) = %q, %q, %v", c.provider, base, fallback, ok)
565+ }
566+ }
567+ if _, _, ok := DefaultURLs("nope"); ok {
568+ t.Error("an unknown provider has default URLs")
569+ }
570+}
@@ -547,3 +547,24 @@ func TestOpenKeepsGenkitQuiet(t *testing.T) {
547 t.Errorf("Genkit log level = %v after Open, want warn or above", got)547 t.Errorf("Genkit log level = %v after Open, want warn or above", got)
548 }548 }
549 }549 }
550+
551+// DefaultURLs gives the addresses a provider uses with no baseUrl: the
552+// fallback when it has one, nothing for an unknown provider.
553+func TestDefaultURLs(t *testing.T) {
554+ cases := []struct{ provider, base, fallback string }{
555+ {"llmman", "http://127.0.0.1:17434/v1", "http://host.docker.internal:17434/v1"},
556+ {"ollama", "http://localhost:11434/v1", "http://host.docker.internal:11434/v1"},
557+ {"dmr", "http://localhost:12434/engines/v1", "http://host.docker.internal:12434/engines/v1"},
558+ {"llamacpp", "http://127.0.0.1:8080/v1", ""},
559+ {"openai", "https://api.openai.com/v1", ""},
560+ }
561+ for _, c := range cases {
562+ base, fallback, ok := DefaultURLs(c.provider)
563+ if !ok || base != c.base || fallback != c.fallback {
564+ t.Errorf("DefaultURLs(%s) = %q, %q, %v", c.provider, base, fallback, ok)
565+ }
566+ }
567+ if _, _, ok := DefaultURLs("nope"); ok {
568+ t.Error("an unknown provider has default URLs")
569+ }
570+}
modified internal/tui/initwin.go +22 -2
@@ -335,7 +335,7 @@ func (w *initWin) drawQuestion(p painter) {
335335 top := min(max(sel-listH/2, 0), max(len(st.Options)-listH, 0))
336336 for row := 0; row < listH && top+row < len(st.Options); row++ {
337337 o := st.Options[top+row]
338- label := " " + o.Label
338+ label := " " + optionLabel(st, o)
339339 if o.Value == st.Default {
340340 label += " (default)"
341341 }
@@ -346,7 +346,7 @@ func (w *initWin) drawQuestion(p painter) {
346346 }
347347 p.textLimited(1, 2+row, p.width()-2, padTo(label, p.width()-2), style)
348348 }
349- if d := st.Options[sel].Detail; d != "" {
349+ if d := optionDetail(st, st.Options[sel]); d != "" {
350350 y := 3 + min(listH, len(st.Options))
351351 for i, line := range wrap(d, p.width()-2) {
352352 if y+i < p.height() {
@@ -356,6 +356,26 @@ func (w *initWin) drawQuestion(p painter) {
356356 }
357357 }
358358
359+// optionLabel is how an option is listed. A model is listed by the
360+// header's short name: its identifier is an address of seventy characters,
361+// cut by the column before the part that tells two models apart.
362+func optionLabel(st agentinit.Step, o agentinit.Option) string {
363+ if st.Key == "model" {
364+ return shortModel(o.Label)
365+ }
366+ return o.Label
367+}
368+
369+// optionDetail is the line under the list for the highlighted option. For a
370+// model it is the full identifier: two short names can be the same, and the
371+// identifier is what the file will hold.
372+func optionDetail(st agentinit.Step, o agentinit.Option) string {
373+ if st.Key == "model" && o.Detail == "" {
374+ return o.Value
375+ }
376+ return o.Detail
377+}
378+
359379 func padTo(s string, width int) string {
360380 if n := len([]rune(s)); n < width {
361381 return s + strings.Repeat(" ", width-n)
@@ -335,7 +335,7 @@ func (w *initWin) drawQuestion(p painter) {
335 top := min(max(sel-listH/2, 0), max(len(st.Options)-listH, 0))335 top := min(max(sel-listH/2, 0), max(len(st.Options)-listH, 0))
336 for row := 0; row < listH && top+row < len(st.Options); row++ {336 for row := 0; row < listH && top+row < len(st.Options); row++ {
337 o := st.Options[top+row]337 o := st.Options[top+row]
338- label := " " + o.Label338+ label := " " + optionLabel(st, o)
339 if o.Value == st.Default {339 if o.Value == st.Default {
340 label += " (default)"340 label += " (default)"
341 }341 }
@@ -346,7 +346,7 @@ func (w *initWin) drawQuestion(p painter) {
346 }346 }
347 p.textLimited(1, 2+row, p.width()-2, padTo(label, p.width()-2), style)347 p.textLimited(1, 2+row, p.width()-2, padTo(label, p.width()-2), style)
348 }348 }
349- if d := st.Options[sel].Detail; d != "" {349+ if d := optionDetail(st, st.Options[sel]); d != "" {
350 y := 3 + min(listH, len(st.Options))350 y := 3 + min(listH, len(st.Options))
351 for i, line := range wrap(d, p.width()-2) {351 for i, line := range wrap(d, p.width()-2) {
352 if y+i < p.height() {352 if y+i < p.height() {
@@ -356,6 +356,26 @@ func (w *initWin) drawQuestion(p painter) {
356 }356 }
357 }357 }
358 358
359+// optionLabel is how an option is listed. A model is listed by the
360+// header's short name: its identifier is an address of seventy characters,
361+// cut by the column before the part that tells two models apart.
362+func optionLabel(st agentinit.Step, o agentinit.Option) string {
363+ if st.Key == "model" {
364+ return shortModel(o.Label)
365+ }
366+ return o.Label
367+}
368+
369+// optionDetail is the line under the list for the highlighted option. For a
370+// model it is the full identifier: two short names can be the same, and the
371+// identifier is what the file will hold.
372+func optionDetail(st agentinit.Step, o agentinit.Option) string {
373+ if st.Key == "model" && o.Detail == "" {
374+ return o.Value
375+ }
376+ return o.Detail
377+}
378+
359 func padTo(s string, width int) string {379 func padTo(s string, width int) string {
360 if n := len([]rune(s)); n < width {380 if n := len([]rune(s)); n < width {
361 return s + strings.Repeat(" ", width-n)381 return s + strings.Repeat(" ", width-n)
modified internal/tui/initwin_test.go +27 -1
@@ -99,7 +99,9 @@ func TestInitWindowDefaultsAllTheWay(t *testing.T) {
9999 // steps list marks what is answered; the detail of the highlighted option
100100 // is shown.
101101 func TestInitWindowPreviewFollowsTheSelection(t *testing.T) {
102- win, screen := newInitWin(t, initFlow(t, "out.yaml"), 130, 40)
102+ // 60 rows: the file's header (facts, the origin legend, the model's
103+ // comment) takes some 25 lines before docSearch.
104+ win, screen := newInitWin(t, initFlow(t, "out.yaml"), 130, 60)
103105 key(win, tcell.KeyEnter) // model: the first
104106 key(win, tcell.KeyEnter) // family: detected (qwen)
105107 shown := screenOf(win, screen)
@@ -162,3 +164,27 @@ func TestInitWindowNarrowTabShowsThePreview(t *testing.T) {
162164 t.Errorf("Tab did not show the preview:\n%s", shown)
163165 }
164166 }
167+
168+// The model list shows the header's short names — the full identifiers are
169+// cut by the column before the part that tells models apart — and the line
170+// under the list the highlighted model's full identifier.
171+func TestInitWindowListsModelsByShortName(t *testing.T) {
172+ win, screen := newInitWin(t, initFlow(t, "out.yaml"), 100, 30)
173+ if st := win.current(); st.Key != "model" {
174+ t.Fatalf("first step %q, want model", st.Key)
175+ }
176+ shown := screenOf(win, screen)
177+ for _, want := range []string{"▸ Qwen3.5-4B:Q4_K_M (defau", " gemma-4-E4B-it:Q4_K_M", "hf.co/unsloth/Qwen3.5"} {
178+ if !strings.Contains(shown, want) {
179+ t.Errorf("missing %q:\n%s", want, shown)
180+ }
181+ }
182+ key(win, tcell.KeyDown)
183+ if shown = screenOf(win, screen); !strings.Contains(shown, "hf.co/unsloth/gemma-4") {
184+ t.Errorf("the detail does not follow the highlighted model:\n%s", shown)
185+ }
186+ key(win, tcell.KeyEnter)
187+ if got := win.steps()[1].Value; got != "hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M" {
188+ t.Errorf("the answer is %q, want the full identifier", got)
189+ }
190+}
@@ -99,7 +99,9 @@ func TestInitWindowDefaultsAllTheWay(t *testing.T) {
99 // steps list marks what is answered; the detail of the highlighted option99 // steps list marks what is answered; the detail of the highlighted option
100 // is shown.100 // is shown.
101 func TestInitWindowPreviewFollowsTheSelection(t *testing.T) {101 func TestInitWindowPreviewFollowsTheSelection(t *testing.T) {
102- win, screen := newInitWin(t, initFlow(t, "out.yaml"), 130, 40)102+ // 60 rows: the file's header (facts, the origin legend, the model's
103+ // comment) takes some 25 lines before docSearch.
104+ win, screen := newInitWin(t, initFlow(t, "out.yaml"), 130, 60)
103 key(win, tcell.KeyEnter) // model: the first105 key(win, tcell.KeyEnter) // model: the first
104 key(win, tcell.KeyEnter) // family: detected (qwen)106 key(win, tcell.KeyEnter) // family: detected (qwen)
105 shown := screenOf(win, screen)107 shown := screenOf(win, screen)
@@ -162,3 +164,27 @@ func TestInitWindowNarrowTabShowsThePreview(t *testing.T) {
162 t.Errorf("Tab did not show the preview:\n%s", shown)164 t.Errorf("Tab did not show the preview:\n%s", shown)
163 }165 }
164 }166 }
167+
168+// The model list shows the header's short names — the full identifiers are
169+// cut by the column before the part that tells models apart — and the line
170+// under the list the highlighted model's full identifier.
171+func TestInitWindowListsModelsByShortName(t *testing.T) {
172+ win, screen := newInitWin(t, initFlow(t, "out.yaml"), 100, 30)
173+ if st := win.current(); st.Key != "model" {
174+ t.Fatalf("first step %q, want model", st.Key)
175+ }
176+ shown := screenOf(win, screen)
177+ for _, want := range []string{"▸ Qwen3.5-4B:Q4_K_M (defau", " gemma-4-E4B-it:Q4_K_M", "hf.co/unsloth/Qwen3.5"} {
178+ if !strings.Contains(shown, want) {
179+ t.Errorf("missing %q:\n%s", want, shown)
180+ }
181+ }
182+ key(win, tcell.KeyDown)
183+ if shown = screenOf(win, screen); !strings.Contains(shown, "hf.co/unsloth/gemma-4") {
184+ t.Errorf("the detail does not follow the highlighted model:\n%s", shown)
185+ }
186+ key(win, tcell.KeyEnter)
187+ if got := win.steps()[1].Value; got != "hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M" {
188+ t.Errorf("the answer is %q, want the full identifier", got)
189+ }
190+}