🛟 Updated.
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 | ||
| 46 | 46 | │ │ skillsDir: no-skills |
| 47 | 47 | │ │ # usage docs |
| 48 | 48 | │ │ maxTurns: 8 |
| 49 | - │ │ # settings guide, row "Small model (≤ 8B), short tasks" | |
| 49 | + │ │ # [guide] settings guide, row "Small model (≤ 8B), short t… | |
| 50 | 50 | ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── |
| 51 | 51 | ↑↓ 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 | 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 | 70 | ## Variants |
| 69 | 71 | |
| 70 | 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 | 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 | 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 | 84 | - **An existing file.** mm never overwrites it, and says so before asking anything: choose another name with `-o`. |
| 83 | 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. |
| @@ -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-skills | 46 | │ │ skillsDir: no-skills |
| 47 | │ │ # usage docs | 47 | │ │ # usage docs |
| 48 | │ │ maxTurns: 8 | 48 | │ │ 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 quit | 51 | ↑↓ 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 | ## Variants | 70 | ## 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/ | ||
| 175 | 175 | |
| 176 | 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 | 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 | 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 | 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 | 185 | | Existing file | Never overwritten: `⚠️ <file> already exists — nothing was written; choose another name with -o`, exit 1, before any question. | |
| 185 | 186 | | After writing | The file is loaded as mm loads a configuration; an error is printed and the exit code is 1. | |
| 186 | 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. | |
| @@ -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 | ||
| 46 | 46 | │ │ skillsDir: no-skills |
| 47 | 47 | │ │ # usage docs |
| 48 | 48 | │ │ maxTurns: 8 |
| 49 | - │ │ # settings guide, row "Small model (≤ 8B), short tasks" | |
| 49 | + │ │ # [guide] settings guide, row "Small model (≤ 8B), short t… | |
| 50 | 50 | ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── |
| 51 | 51 | ↑↓ 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 | 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 | 70 | ## Variantes |
| 69 | 71 | |
| 70 | 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 | 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 | 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 | 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 | 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. |
| @@ -46,7 +46,7 @@ Ce guide montre comment écrire un nouvel agent.yaml pour un modèle et une tâc | |||
| 46 | │ │ skillsDir: no-skills | 46 | │ │ skillsDir: no-skills |
| 47 | │ │ # usage docs | 47 | │ │ # usage docs |
| 48 | │ │ maxTurns: 8 | 48 | │ │ 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 quit | 51 | ↑↓ 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 | ## Variantes | 70 | ## 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/ | ||
| 175 | 175 | |
| 176 | 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 | 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 | 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 | 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 | 185 | | Fichier existant | Jamais écrasé : `⚠️ <fichier> already exists — nothing was written; choose another name with -o`, code de sortie 1, avant toute question. | |
| 185 | 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 | 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. | |
| @@ -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 { | ||
| 23 | 23 | Key string |
| 24 | 24 | Value string // already formatted as YAML |
| 25 | 25 | 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 | 43 | // Settings is everything the generated file holds. |
| 29 | 44 | type Settings struct { |
| 30 | 45 | Choices Choices |
| @@ -74,7 +89,11 @@ func reasons(fam Family, f Facts) bool { | ||
| 74 | 89 | } |
| 75 | 90 | |
| 76 | 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 | 99 | func (s *Settings) decideTop() { |
| @@ -107,23 +126,23 @@ func (s *Settings) decideTop() { | ||
| 107 | 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 | 128 | 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) | |
| 112 | 131 | watchdog, watchdogReason := m.WatchdogTimeout, m.Source |
| 113 | 132 | if u.Tools["bashTool"] && m.WatchdogTimeout != m.BashTimeout { |
| 114 | 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 | 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 | 141 | func (s *Settings) decideSampling() { |
| 123 | 142 | 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"] | |
| 125 | 144 | if s.Thinking && fam.Thinking.Temperature != nil { |
| 126 | - base = fam.Thinking | |
| 145 | + base, origin = fam.Thinking, fam.Origin["thinking"] | |
| 127 | 146 | } |
| 128 | 147 | values := map[string]float64{} |
| 129 | 148 | put := func(key string, v *float64) { |
| @@ -137,25 +156,26 @@ func (s *Settings) decideSampling() { | ||
| 137 | 156 | if base.TopK != nil { |
| 138 | 157 | values["top_k"] = float64(*base.TopK) |
| 139 | 158 | } |
| 140 | - reasons := map[string]string{} | |
| 159 | + reasons, origins := map[string]string{}, map[string]string{} | |
| 141 | 160 | for k := range values { |
| 142 | - reasons[k] = source | |
| 161 | + reasons[k], origins[k] = source, origin | |
| 143 | 162 | } |
| 144 | 163 | if len(f.Sampling) > 0 { |
| 145 | 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 | 168 | if !s.Thinking && s.Usage.Precise && fam.Precise != nil { |
| 150 | 169 | values["temperature"] = *fam.Precise |
| 151 | 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 | 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 | 176 | for _, k := range []string{"temperature", "top_p", "top_k", "min_p"} { |
| 157 | 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 | 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 | 186 | if s.Family.MaxTokensKey != "" { |
| 167 | 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 | 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 | 193 | s.decideKwargs() |
| 174 | 194 | } |
| @@ -188,7 +208,7 @@ func (s *Settings) decideKwargs() { | ||
| 188 | 208 | if d := s.Facts.Thinking.Default; d != "" { |
| 189 | 209 | 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 | 212 | case "reasoning_effort": |
| 193 | 213 | values := s.Facts.Thinking.EffortValues |
| 194 | 214 | if len(values) == 0 { |
| @@ -201,17 +221,17 @@ func (s *Settings) decideKwargs() { | ||
| 201 | 221 | if len(values) > 0 && !contains(values, effort) { |
| 202 | 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 | 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 | 230 | // The percentage applies to the window the server SERVES, which mm |
| 211 | 231 | // probes (contextWindow: 0) and which may be far below the model's |
| 212 | 232 | // native one (Ollama often serves 4k–8k): a threshold computed from the |
| 213 | 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 | 237 | func (s *Settings) decideNotes() { |
| @@ -23,8 +23,23 @@ type Setting struct { | |||
| 23 | Key string | 23 | Key string |
| 24 | Value string // already formatted as YAML | 24 | Value string // already formatted as YAML |
| 25 | Reason string | 25 | 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 Choices | 45 | 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.Source | 131 | 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.Facts | 142 | 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"] |
| 125 | if s.Thinking && fam.Thinking.Temperature != nil { | 144 | if s.Thinking && fam.Thinking.Temperature != nil { |
| 126 | - base = fam.Thinking | 145 | + 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] = source | 161 | + 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.Precise | 169 | 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 " + d | 209 | 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.EffortValues | 213 | 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 mm | 230 | // The percentage applies to the window the server SERVES, which mm |
| 211 | // probes (contextWindow: 0) and which may be far below the model's | 231 | // probes (contextWindow: 0) and which may be far below the model's |
| 212 | // native one (Ollama often serves 4k–8k): a threshold computed from the | 232 | // 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) { | ||
| 145 | 145 | } |
| 146 | 146 | text := Render(s) |
| 147 | 147 | 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 | 149 | t.Errorf("key / address comment missing:\n%s", text) |
| 150 | 150 | } |
| 151 | 151 | loadRendered(t, text) |
| @@ -153,3 +153,94 @@ func TestOpenAIFamilyAndProvider(t *testing.T) { | ||
| 153 | 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 | +} | |
| @@ -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 wrapped | 147 | 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 { | ||
| 42 | 42 | MaxTokensKey string `yaml:"maxTokensKey"` // "" = max_tokens |
| 43 | 43 | Note string `yaml:"note"` |
| 44 | 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 | 51 | // Usage is what an agent is for. |
| @@ -42,6 +42,10 @@ type Family struct { | |||
| 42 | MaxTokensKey string `yaml:"maxTokensKey"` // "" = max_tokens | 42 | 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 @@ | ||
| 12 | 12 | # temperature for tool-heavy, thinking-off agents (developer, docs, JSON…). |
| 13 | 13 | # When the model server tells the GGUF's own sampling (llmman, Ollama), it |
| 14 | 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 | 23 | families: |
| 17 | 24 | - name: qwen-coder |
| @@ -20,6 +27,7 @@ families: | ||
| 20 | 27 | general: {temperature: 1.0, top_p: 0.95, top_k: 40} |
| 21 | 28 | precise: 0.2 |
| 22 | 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 | 32 | - name: qwen |
| 25 | 33 | match: [qwen] |
| @@ -28,6 +36,7 @@ families: | ||
| 28 | 36 | thinking: {temperature: 0.6, top_p: 0.95, top_k: 20} |
| 29 | 37 | precise: 0.0 |
| 30 | 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 | 41 | - name: gemma |
| 33 | 42 | match: [gemma] |
| @@ -36,6 +45,7 @@ families: | ||
| 36 | 45 | thinking: {temperature: 1.0, top_p: 0.95, top_k: 64} |
| 37 | 46 | precise: 0.2 |
| 38 | 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 | 50 | - name: mellum-thinking |
| 41 | 51 | match: [mellum, thinking] |
| @@ -44,6 +54,7 @@ families: | ||
| 44 | 54 | thinking: {temperature: 0.6, top_p: 0.95} |
| 45 | 55 | precise: 0.2 |
| 46 | 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 | 59 | - name: mellum |
| 49 | 60 | match: [mellum] |
| @@ -51,6 +62,7 @@ families: | ||
| 51 | 62 | general: {temperature: 0.2, top_p: 0.9} |
| 52 | 63 | precise: 0.0 |
| 53 | 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 | 67 | - name: gpt-oss |
| 56 | 68 | match: [gpt-oss] |
| @@ -60,6 +72,7 @@ families: | ||
| 60 | 72 | thinking: {temperature: 1.0, top_p: 1.0} |
| 61 | 73 | precise: 1.0 |
| 62 | 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 | 77 | - name: openai |
| 65 | 78 | match: [gpt-] |
| @@ -69,6 +82,7 @@ families: | ||
| 69 | 82 | precise: 1.0 |
| 70 | 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 | 84 | source: "OpenAI API reference; not measured in this repository" |
| 85 | + origin: {general: card, precise: card} | |
| 72 | 86 | |
| 73 | 87 | - name: minicpm |
| 74 | 88 | match: [minicpm] |
| @@ -77,6 +91,7 @@ families: | ||
| 77 | 91 | thinking: {temperature: 0.6, top_p: 0.95} |
| 78 | 92 | precise: 0.3 |
| 79 | 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 | 96 | - name: ornith |
| 82 | 97 | match: [ornith] |
| @@ -85,6 +100,7 @@ families: | ||
| 85 | 100 | thinking: {temperature: 0.6, top_p: 0.95, top_k: 20} |
| 86 | 101 | precise: 0.3 |
| 87 | 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 | 105 | - name: granite-guardian |
| 90 | 106 | match: [granite, guardian] |
| @@ -93,6 +109,7 @@ families: | ||
| 93 | 109 | precise: 0.0 |
| 94 | 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 | 111 | source: "demos/guard (Granite Guardian 3.2 3B-A800M: temperature 0.0)" |
| 112 | + origin: {general: measured, precise: measured} | |
| 96 | 113 | |
| 97 | 114 | - name: menlo |
| 98 | 115 | match: [menlo] |
| @@ -100,6 +117,7 @@ families: | ||
| 100 | 117 | general: {temperature: 0.0, top_p: 0.9} |
| 101 | 118 | precise: 0.0 |
| 102 | 119 | source: "demos/llmman (Jan-nano, Lucy: 0.0 / 0.9)" |
| 120 | + origin: {general: measured, precise: measured} | |
| 103 | 121 | |
| 104 | 122 | - name: llama |
| 105 | 123 | match: [llama] |
| @@ -107,6 +125,7 @@ families: | ||
| 107 | 125 | general: {temperature: 0.6, top_p: 0.9} |
| 108 | 126 | precise: 0.2 |
| 109 | 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 | 130 | - name: mistral |
| 112 | 131 | match: [mistral] |
| @@ -114,6 +133,7 @@ families: | ||
| 114 | 133 | general: {temperature: 0.15} |
| 115 | 134 | precise: 0.15 |
| 116 | 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 | 138 | - name: other |
| 119 | 139 | match: [] |
| @@ -121,6 +141,7 @@ families: | ||
| 121 | 141 | general: {temperature: 0.7, top_p: 0.9} |
| 122 | 142 | precise: 0.2 |
| 123 | 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 | 146 | # Usages: what the agent is for. `tools` are the YAML switches; `thinking` |
| 126 | 147 | # 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), it | 13 | # 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-coder | 24 | - 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.2 | 28 | 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: qwen | 32 | - 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.0 | 37 | 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: gemma | 41 | - 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.2 | 46 | 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-thinking | 50 | - 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.2 | 55 | 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: mellum | 59 | - 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.0 | 63 | 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-oss | 67 | - 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.0 | 73 | 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: openai | 77 | - name: openai |
| 65 | match: [gpt-] | 78 | match: [gpt-] |
| @@ -69,6 +82,7 @@ families: | |||
| 69 | precise: 1.0 | 82 | 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: minicpm | 87 | - 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.3 | 92 | 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: ornith | 96 | - 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.3 | 101 | 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-guardian | 105 | - name: granite-guardian |
| 90 | match: [granite, guardian] | 106 | match: [granite, guardian] |
| @@ -93,6 +109,7 @@ families: | |||
| 93 | precise: 0.0 | 109 | 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: menlo | 114 | - 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.0 | 118 | 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: llama | 122 | - 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.2 | 126 | 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: mistral | 130 | - 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.15 | 134 | 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: other | 138 | - 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.2 | 142 | 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 the | 147 | # 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) { | ||
| 20 | 20 | if f.Reasoning != "none" && f.Thinking.Temperature == nil { |
| 21 | 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 | 34 | for _, u := range p.Usages { |
| 25 | 35 | 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 ( | ||
| 4 | 4 | "fmt" |
| 5 | 5 | "strconv" |
| 6 | 6 | "strings" |
| 7 | + | |
| 8 | + "mm/internal/engine" | |
| 7 | 9 | ) |
| 8 | 10 | |
| 9 | 11 | // Render writes the settings as a commented agent.yaml: every value with the |
| @@ -16,17 +18,24 @@ func Render(s Settings) string { | ||
| 16 | 18 | b.WriteString("#\n# Written by `mm init`. Every value says why it has that value; change\n") |
| 17 | 19 | b.WriteString("# them as you watch the agent work (guides/agent-settings-for-small-models.md).\n#\n") |
| 18 | 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 | 23 | fmt.Fprintf(&b, "#\n# mm %s or mm -tui %s\n\n", "-config <this file>", "<this file>") |
| 20 | 24 | |
| 21 | 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 | 29 | fmt.Fprintf(&b, "model: %s\n", quote(c.Model)) |
| 23 | 30 | if c.BaseURL != "" { |
| 24 | 31 | fmt.Fprintf(&b, "baseUrl: %s\n", quote(c.BaseURL)) |
| 32 | + } else { | |
| 33 | + writeDefaultURLs(&b, c.Provider) | |
| 25 | 34 | } |
| 26 | 35 | if c.Provider == "openai" { |
| 27 | 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 | 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 | 41 | b.WriteString("\n") |
| @@ -100,20 +109,125 @@ const jsonExample = `type Contact struct { | ||
| 100 | 109 | var Answer Contact` |
| 101 | 110 | |
| 102 | 111 | // 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 | 114 | func writeSettings(b *strings.Builder, indent string, settings []Setting) { |
| 105 | 115 | last := "" |
| 106 | 116 | 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)) { | |
| 109 | 123 | fmt.Fprintf(b, "%s# %s\n", indent, line) |
| 110 | 124 | } |
| 111 | - last = st.Reason | |
| 125 | + last = comment | |
| 112 | 126 | } |
| 113 | 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 | 231 | func titleOf(name string, u Usage) string { |
| 118 | 232 | if name == "" { |
| 119 | 233 | 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 the | 11 | // 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 the | 111 | // 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.Reason | 125 | + 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.Description | 233 | return u.Description |
modified
internal/engine/openai_compat.go +16 -0 | @@ -64,6 +64,22 @@ type openaiCompat struct { | ||
| 64 | 64 | 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 | 83 | func init() { |
| 68 | 84 | register(&openaiCompat{ |
| 69 | 85 | name: "dmr", |
| @@ -64,6 +64,22 @@ type openaiCompat struct { | |||
| 64 | unload unloadFunc | 64 | 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) { | ||
| 547 | 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 | +} | |
| @@ -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) { | ||
| 335 | 335 | top := min(max(sel-listH/2, 0), max(len(st.Options)-listH, 0)) |
| 336 | 336 | for row := 0; row < listH && top+row < len(st.Options); row++ { |
| 337 | 337 | o := st.Options[top+row] |
| 338 | - label := " " + o.Label | |
| 338 | + label := " " + optionLabel(st, o) | |
| 339 | 339 | if o.Value == st.Default { |
| 340 | 340 | label += " (default)" |
| 341 | 341 | } |
| @@ -346,7 +346,7 @@ func (w *initWin) drawQuestion(p painter) { | ||
| 346 | 346 | } |
| 347 | 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 | 350 | y := 3 + min(listH, len(st.Options)) |
| 351 | 351 | for i, line := range wrap(d, p.width()-2) { |
| 352 | 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 | 379 | func padTo(s string, width int) string { |
| 360 | 380 | if n := len([]rune(s)); n < width { |
| 361 | 381 | 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.Label | 338 | + 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) { | ||
| 99 | 99 | // steps list marks what is answered; the detail of the highlighted option |
| 100 | 100 | // is shown. |
| 101 | 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 | 105 | key(win, tcell.KeyEnter) // model: the first |
| 104 | 106 | key(win, tcell.KeyEnter) // family: detected (qwen) |
| 105 | 107 | shown := screenOf(win, screen) |
| @@ -162,3 +164,27 @@ func TestInitWindowNarrowTabShowsThePreview(t *testing.T) { | ||
| 162 | 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 | +} | |
| @@ -99,7 +99,9 @@ func TestInitWindowDefaultsAllTheWay(t *testing.T) { | |||
| 99 | // steps list marks what is answered; the detail of the highlighted option | 99 | // 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 first | 105 | 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 | +} | ||