bots-garden/mini-mepublic Fork
⑂ cbfcb19
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

Merge feature/kronk-provider into main

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
k33g committed 2026-11-11T08:58:15+01:00 Browse files
cbfcb19 parents: 74efbf2 3b7d74c
modified .tickets/epics.yaml +11 -1
@@ -31,7 +31,7 @@ epics:
3131 state: open
3232 color: "#5319e7"
3333 - title: ToDo
34- state: open
34+ state: closed
3535 color: "#c2e0c6"
3636 - title: Questions
3737 state: open
@@ -67,3 +67,13 @@ epics:
6767 state: open
6868 color: "#a2eeef"
6969 description: MCP servers used by mini-me (connection, discovery, authentication)
70+ - title: Auto improve
71+ state: open
72+ color: "#d876e3"
73+ description: mini-me improving its own source code (with Claude and local models)
74+ - title: Search
75+ state: open
76+ color: "#5319e7"
77+ - title: Agent Identity
78+ state: open
79+ color: "#006b75"
@@ -31,7 +31,7 @@ epics:
31 state: open31 state: open
32 color: "#5319e7"32 color: "#5319e7"
33 - title: ToDo33 - title: ToDo
34- state: open34+ state: closed
35 color: "#c2e0c6"35 color: "#c2e0c6"
36 - title: Questions36 - title: Questions
37 state: open37 state: open
@@ -67,3 +67,13 @@ epics:
67 state: open67 state: open
68 color: "#a2eeef"68 color: "#a2eeef"
69 description: MCP servers used by mini-me (connection, discovery, authentication)69 description: MCP servers used by mini-me (connection, discovery, authentication)
70+ - title: Auto improve
71+ state: open
72+ color: "#d876e3"
73+ description: mini-me improving its own source code (with Claude and local models)
74+ - title: Search
75+ state: open
76+ color: "#5319e7"
77+ - title: Agent Identity
78+ state: open
79+ color: "#006b75"
modified .tickets/issues/0019-try-to-auto-improve-the-source-code-of-mini-me-w.yaml +2 -2
@@ -6,12 +6,12 @@ author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-09-17T13:22:40.786Z
9-updatedAt: 2026-10-06T13:05:39.475Z
9+updatedAt: 2026-11-11T06:51:08.000Z
1010 labels:
1111 - priority::low
1212 - kind::chore
1313 - enhancement
14-epic: Debug
14+epic: Auto improve
1515 body: |
1616 - Use the sessions' logs
1717 - Create a Claude Skill for this
@@ -6,12 +6,12 @@ author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-09-17T13:22:40.786Z8 createdAt: 2026-09-17T13:22:40.786Z
9-updatedAt: 2026-10-06T13:05:39.475Z9+updatedAt: 2026-11-11T06:51:08.000Z
10 labels:10 labels:
11 - priority::low11 - priority::low
12 - kind::chore12 - kind::chore
13 - enhancement13 - enhancement
14-epic: Debug14+epic: Auto improve
15 body: |15 body: |
16 - Use the sessions' logs16 - Use the sessions' logs
17 - Create a Claude Skill for this17 - Create a Claude Skill for this
modified .tickets/issues/0058-add-more-tools-to-the-sbx-template-redis-cli-to.yaml +2 -2
@@ -1,12 +1,12 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 58
33 title: Add more tools to the sbx template (redis, cli to parse source code ...)
4-state: open
4+state: closed
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-10-01T04:39:22.452Z
9-updatedAt: 2026-10-06T13:13:18.155Z
9+updatedAt: 2026-11-11T06:53:19.908Z
1010 labels: []
1111 epic: ToDo
1212 body: ""
@@ -1,12 +1,12 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 582 id: 58
3 title: Add more tools to the sbx template (redis, cli to parse source code ...)3 title: Add more tools to the sbx template (redis, cli to parse source code ...)
4-state: open4+state: closed
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-10-01T04:39:22.452Z8 createdAt: 2026-10-01T04:39:22.452Z
9-updatedAt: 2026-10-06T13:13:18.155Z9+updatedAt: 2026-11-11T06:53:19.908Z
10 labels: []10 labels: []
11 epic: ToDo11 epic: ToDo
12 body: ""12 body: ""
renamed .tickets/issues/0069-create-mini-me-skills-to-search-some-specific-kn.yaml +4 -3
similarity index 77%
rename from .tickets/issues/0069-create-skills-to-search-some-specific-knowlege-o.yaml
rename to .tickets/issues/0069-create-mini-me-skills-to-search-some-specific-kn.yaml
@@ -1,14 +1,15 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 69
3-title: Create skills to search some specific knowlege on internet
3+title: Create mini me skills to search some specific knowlege on internet
44 state: open
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-10-02T09:26:19.729Z
9-updatedAt: 2026-10-20T07:43:15.320Z
9+updatedAt: 2026-11-11T06:55:34.384Z
1010 labels: []
11-epic: ToDo
11+milestone: v2.0.0
12+epic: Search
1213 related:
1314 - 104
1415 body: |
similarity index 77%
rename from .tickets/issues/0069-create-skills-to-search-some-specific-knowlege-o.yaml
rename to .tickets/issues/0069-create-mini-me-skills-to-search-some-specific-kn.yaml
@@ -1,14 +1,15 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 692 id: 69
3-title: Create skills to search some specific knowlege on internet3+title: Create mini me skills to search some specific knowlege on internet
4 state: open4 state: open
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-10-02T09:26:19.729Z8 createdAt: 2026-10-02T09:26:19.729Z
9-updatedAt: 2026-10-20T07:43:15.320Z9+updatedAt: 2026-11-11T06:55:34.384Z
10 labels: []10 labels: []
11-epic: ToDo11+milestone: v2.0.0
12+epic: Search
12 related:13 related:
13 - 10414 - 104
14 body: |15 body: |
modified .tickets/issues/0070-add-github-gitlab-other-identity-to-the-mini-me.yaml +2 -2
@@ -6,9 +6,9 @@ author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-10-02T14:22:29.427Z
9-updatedAt: 2026-10-06T13:11:38.195Z
9+updatedAt: 2026-11-11T06:57:56.200Z
1010 labels: []
11-epic: Questions
11+epic: Agent Identity
1212 body: ""
1313 tasks: []
1414 comments: []
@@ -6,9 +6,9 @@ author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-10-02T14:22:29.427Z8 createdAt: 2026-10-02T14:22:29.427Z
9-updatedAt: 2026-10-06T13:11:38.195Z9+updatedAt: 2026-11-11T06:57:56.200Z
10 labels: []10 labels: []
11-epic: Questions11+epic: Agent Identity
12 body: ""12 body: ""
13 tasks: []13 tasks: []
14 comments: []14 comments: []
added .tickets/issues/0075-add-kronk-provider.yaml +333 -0
new file mode 100644
@@ -0,0 +1,333 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 75
3+title: Add Kronk provider
4+state: open
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-10-06T03:58:37.611Z
9+updatedAt: 2026-11-11T07:00:31.000Z
10+labels:
11+ - kind::feature
12+ - priority::medium
13+milestone: v1.0.6
14+epic: Providers (LLM Engine)
15+related:
16+ - 62
17+body: |
18+ ## Context
19+
20+ [Kronk](https://www.kronkai.com/) ([ardanlabs/kronk](https://github.com/ardanlabs/kronk))
21+ is a local inference server written in Go by Ardan Labs. It embeds llama.cpp
22+ through yzma, without a llama-server process. `kronk server start` serves an
23+ OpenAI-compatible API (plus Anthropic `/v1/messages`) on `127.0.0.1:11435`.
24+
25+ Add it as an mm provider, `provider: kronk`. It is a new entry of the
26+ `openaiCompat` registry (`internal/engine/openai_compat.go`), like dmr,
27+ llamacpp, llmman and ollama: no new wire protocol. Only the
28+ server-specific parts are new: the probe, pull and unload.
29+
30+ ## What the Kronk manual says (to be measured, not trusted)
31+
32+ Sources: `.manual/chapter-08-model-server.md`, `chapter-09-api-endpoints.md`,
33+ `chapter-10-request-parameters.md`, `chapter-12-security-authentication.md`,
34+ `chapter-14-client-integration.md` in the Kronk repository (read on 2026-11-11, Kronk 1.33.2).
35+
36+ - **Install**: `brew install ardanlabs/kronk/kronk`, or
37+ `go install github.com/ardanlabs/kronk/cmd/kronk@latest`.
38+ - **Start**: `kronk server start`, or `--detach` (log in `~/.kronk/kronk.log`).
39+ - **Address**: `127.0.0.1:11435`, loopback only. `--api-host` or
40+ `KRONK_WEB_API_HOST` changes it. Note the port is 11435, next to Ollama's 11434.
41+ - **Base URL for mm**: `http://127.0.0.1:11435/v1`.
42+ - **Endpoints**:
43+ - `POST /v1/chat/completions`: SSE streaming, `tools` and `tool_choice`,
44+ streamed `tool_calls`, `stream_options.include_usage`;
45+ - `GET /v1/models`: installed models, not only the loaded ones;
46+ - `GET /v1/models/{model}`: 404 when absent;
47+ - `/v1/embeddings`, `/v1/rerank`, `/v1/tokenize`, `/v1/decide`.
48+ - **Model ids**: `provider/modelID`, e.g. `unsloth/Qwen3-0.6B-Q8_0`. `owner/repo:quant`
49+ and `owner/repo/file.gguf` pin a repository or a file.
50+ - **Pull**:
51+ - CLI: `kronk model pull unsloth/Qwen3-0.6B-Q8_0`;
52+ - API: `POST /v1/kronk/models/pull`, which streams progress.
53+ - **Unload**: `POST /v1/kronk/models/unload`.
54+ - **Loaded models**: `GET /v1/kronk/models/ps`.
55+ - **Model details and configuration**: `GET /v1/kronk/models/{model}`, which may
56+ give the context window. The window is also set per model in
57+ `~/.kronk/models/model_config.yaml` (`context-window`).
58+ - **Reasoning**:
59+ - streamed as `reasoning_content`, which mm already reads;
60+ - switched off with the top-level `enable_thinking: false`,
61+ `reasoning_effort`, or `chat_template_kwargs`, which is passed through
62+ to the template;
63+ - Kronk does not translate between these three switches.
64+ - **Auth**:
65+ - the default mode `open` checks no key, and any non-empty key works;
66+ - the modes `authenticated` and `full-protected` require
67+ `Authorization: Bearer <token>`;
68+ - the manual and its client examples use the variable `KRONK_TOKEN`.
69+ - **Errors**:
70+ - before the stream: JSON `{"code","message"}` with an HTTP status;
71+ - after SSE headers: reported inside the stream, with a 200 status.
72+ - **Not documented**:
73+ - whether a model that is installed but not loaded is loaded on the first request;
74+ - whether there is a llama.cpp-style `prompt_progress` (Kronk is not llama-server).
75+
76+ ## Scope
77+
78+ - Registry entry `kronk`:
79+ - `baseURL` `http://127.0.0.1:11435/v1`;
80+ - fallback `http://host.docker.internal:11435/v1`;
81+ - `apiKeyEnv: KRONK_TOKEN`, not required, with a dummy key.
82+ - `startHint`: `start it: kronk server start`. `notFoundHint`: `kronk model pull %s`.
83+ - Probe:
84+ - reachability;
85+ - model present in `/v1/models`;
86+ - context window from `/v1/kronk/models/{model}` if it gives one.
87+ - `thinkingOff`: the variant that Kronk actually obeys (measure it).
88+ - `pull` (`autoPull`) through `POST /v1/kronk/models/pull`.
89+ - `unload` through `POST /v1/kronk/models/unload`.
90+ - `progress` stays false unless a measurement shows `prompt_progress`.
91+ - Explain:
92+ - 401 or 403 names `KRONK_TOKEN`;
93+ - 404 says `kronk model pull`;
94+ - a connection refused gives the start hint.
95+ - `mm init`:
96+ - the provider is offered with its default URL;
97+ - presets if needed (`internal/agentinit/presets.yaml`).
98+ - Demo config in `demos/kronk/` with a small model that calls tools well.
99+
100+ Out of scope: the Anthropic `/v1/messages` route, `/v1/responses`, and Kronk's
101+ `/v1/decide`. Kronk's `/v1/decide` could later back mm's deciders, which would
102+ be a separate ticket.
103+
104+ ## Sandbox note
105+
106+ Kronk binds to loopback by default. From an sbx sandbox, the fallback
107+ `host.docker.internal:11435` only answers if the host started
108+ `kronk server start --api-host 0.0.0.0:11435`, and the policy allows
109+ `localhost:11435`. The how-to must say so.
110+
111+ ## Acceptance criteria
112+
113+ - `provider: kronk` with no `baseUrl` reaches a local `kronk server start`, and
114+ a turn with a tool call works end to end.
115+ - A model not installed is pulled when `autoPull` is on. Otherwise, the
116+ warning names `kronk model pull`.
117+ - The context window shown at start-up is correct, or labelled as unknown.
118+ - Compaction summary runs with thinking off.
119+ - Unit tests: a Resolve row, the probe against a fake server, pull, unload, Explain.
120+ - Docs EN and FR: a how-to, the provider reference row, `configuration.md`,
121+ the indexes, and the provider README / package diagram if touched.
122+tasks:
123+ - id: 1
124+ title: "Measure Kronk: /v1/models, model details (window), reasoning field and thinking switch, prompt_progress, 404/401 shapes, load on first request"
125+ state: closed
126+ priority: high
127+ author:
128+ name: k33g
129+ email: ph.charriere@gmail.com
130+ createdAt: 2026-11-11T06:24:34.000Z
131+ updatedAt: 2026-11-11T06:46:21.000Z
132+ closedAt: 2026-11-11T06:46:21.000Z
133+ - id: 2
134+ title: Registry entry kronk (URL, fallback, KRONK_TOKEN, hints, thinkingOff)
135+ state: closed
136+ priority: high
137+ author:
138+ name: k33g
139+ email: ph.charriere@gmail.com
140+ createdAt: 2026-11-11T06:24:34.000Z
141+ updatedAt: 2026-11-11T07:00:31.000Z
142+ closedAt: 2026-11-11T07:00:31.000Z
143+ - id: 3
144+ title: probeKronk (reachability, model installed, context window)
145+ state: closed
146+ priority: medium
147+ author:
148+ name: k33g
149+ email: ph.charriere@gmail.com
150+ createdAt: 2026-11-11T06:24:34.000Z
151+ updatedAt: 2026-11-11T07:00:31.000Z
152+ closedAt: 2026-11-11T07:00:31.000Z
153+ - id: 4
154+ title: pullKronk (POST /v1/kronk/models/pull, streamed progress) and unloadKronk (POST /v1/kronk/models/unload)
155+ state: closed
156+ priority: medium
157+ author:
158+ name: k33g
159+ email: ph.charriere@gmail.com
160+ createdAt: 2026-11-11T06:24:34.000Z
161+ updatedAt: 2026-11-11T07:00:31.000Z
162+ closedAt: 2026-11-11T07:00:31.000Z
163+ - id: 5
164+ title: "Tests: Resolve row, probe, pull, unload, Explain against a fake server"
165+ state: closed
166+ priority: high
167+ author:
168+ name: k33g
169+ email: ph.charriere@gmail.com
170+ createdAt: 2026-11-11T06:24:34.000Z
171+ updatedAt: 2026-11-11T07:00:31.000Z
172+ closedAt: 2026-11-11T07:00:31.000Z
173+ - id: 6
174+ title: mm init support (provider list, default URL comment, presets if needed)
175+ state: closed
176+ priority: low
177+ author:
178+ name: k33g
179+ email: ph.charriere@gmail.com
180+ createdAt: 2026-11-11T06:24:34.000Z
181+ updatedAt: 2026-11-11T07:00:31.000Z
182+ closedAt: 2026-11-11T07:00:31.000Z
183+ - id: 7
184+ title: Demo config demos/kronk/ with a small tool-calling model
185+ state: closed
186+ priority: low
187+ author:
188+ name: k33g
189+ email: ph.charriere@gmail.com
190+ createdAt: 2026-11-11T06:24:34.000Z
191+ updatedAt: 2026-11-11T07:00:31.000Z
192+ closedAt: 2026-11-11T07:00:31.000Z
193+ - id: 8
194+ title: Docs EN+FR (how-to incl. sandbox --api-host note, reference, configuration, indexes, diagram)
195+ state: closed
196+ priority: medium
197+ author:
198+ name: k33g
199+ email: ph.charriere@gmail.com
200+ createdAt: 2026-11-11T06:24:34.000Z
201+ updatedAt: 2026-11-11T07:00:31.000Z
202+ closedAt: 2026-11-11T07:00:31.000Z
203+ - id: 9
204+ title: Live check against Kronk on the Mac (tool call, window, compaction, pull, unload)
205+ state: open
206+ priority: high
207+ author:
208+ name: k33g
209+ email: ph.charriere@gmail.com
210+ createdAt: 2026-11-11T06:24:34.000Z
211+comments:
212+ - id: 1
213+ author:
214+ name: k33g
215+ email: ph.charriere@gmail.com
216+ createdAt: 2026-11-11T06:46:21.000Z
217+ body: |
218+ ## Measured on the Mac's Kronk from the sandbox (2026-11-11)
219+
220+ Model: `mradermacher/Qwopus3.5-4B-Coder.Q8_0`. Reached at
221+ `http://host.docker.internal:11435`; `127.0.0.1:11435` does not answer from
222+ the sandbox, so the fallback is needed.
223+
224+ **Where the manual is wrong or silent**
225+
226+ - **Error shape is OpenAI's, not the manual's**: `{"error":{"message","type","code"}}`.
227+ - Unknown model at chat: HTTP 404, `type: not_found_error`, `code: not_found`,
228+ message `... model not found: "foo/bar-Q4"`.
229+ - Id without a provider (`nope`): HTTP 400, `invalid_argument`,
230+ `must use provider/modelID or provider/modelID/profile`.
231+ - **Model ids contain a `/`, so the path must be encoded.**
232+ - `GET /v1/models/{id}` and `GET /v1/kronk/models/{id}` need `%2F`
233+ (`url.PathEscape`). A raw `/` gives 404 or 400.
234+ - **`/v1/models` also lists profiles**: `mradermacher/Qwopus3.5-4B-Coder.Q8_0/AGENT`.
235+ - **A model is loaded on the first request.** After an unload, the chat request
236+ answered 200 in 2.3 s. No pull is needed when the model is installed.
237+
238+ **Measured values for the implementation**
239+
240+ - **Context window**: `GET /v1/kronk/models/<encoded id>` gives
241+ `model_config.context-window` (131072 here).
242+ - GGUF metadata says `qwen35.context_length` 262144; the configured
243+ value is the one served.
244+ - **Loaded models**: `GET /v1/kronk/models/ps`.
245+ - Returns `[{id, status:"loaded", size, vram_total, kv_cache, ...}]`.
246+ - Returns `[]` when nothing is loaded.
247+ - **Reasoning**:
248+ - streamed as `delta.reasoning_content`;
249+ - `usage.completion_tokens_details.reasoning_tokens` is filled.
250+ - **Thinking off**:
251+ - `chat_template_kwargs: {enable_thinking: false}` works, so the
252+ existing `thinkingOffTemplate` fits;
253+ - top-level `enable_thinking: false` works too;
254+ - `reasoning_effort: "none"` is IGNORED (the model still reasons).
255+ - **`return_progress`: no `prompt_progress` chunk**, so `progress: false`.
256+ - **`stream_options.include_usage`** works: a last chunk with `choices: []`
257+ and the usage.
258+ - Extra fields in the usage: `tokens_per_second`,
259+ `time_to_first_token_ms`, `draft_*` (MTP speculative decoding).
260+ - **Streamed tool call** is standard OpenAI:
261+ - the first delta carries `id`, `name` and `arguments: ""`;
262+ - the next delta carries the arguments;
263+ - the last has `finish_reason: "tool_calls"`.
264+ - **Pull**: `POST /v1/kronk/models/pull {"model_url": "<id>"}`.
265+ - The server answers HTTP 200 with SSE `data: {status, progress?, meta?}`.
266+ - `progress` = `{src, current_bytes, total_bytes, mb_per_sec, complete}`.
267+ - **Success** = the last record has `status: "downloaded"` and `downloaded: true`.
268+ - **Failure** = the last record is only an error `status` (still HTTP 200),
269+ e.g. `download: resolve "nobody/x": ... not found at provider "nobody"`.
270+ - An already-installed model answers `-> already downloaded`, then `downloaded`.
271+ - **Unload**: `POST /v1/kronk/models/unload {"id": "<id>"}`.
272+ - Success: `{"status":"unloaded","id":...}`.
273+ - Not loaded: HTTP 404, `not_found`.
274+ - Streams still running: `failed_precondition`.
275+
276+ **Not measured**: the 401 and 403 shapes, which need an `authenticated`
277+ mode on the server.
278+
279+ Source for the pull and unload request types:
280+ `cmd/server/app/domain/toolapp/{model.go,models.go}` in ardanlabs/kronk.
281+ - id: 2
282+ author:
283+ name: k33g
284+ email: ph.charriere@gmail.com
285+ createdAt: 2026-11-11T07:00:31.000Z
286+ body: |
287+ ## Implemented on branch feature/kronk-provider
288+
289+ - `internal/engine/kronk.go`:
290+ - `probeKronk`: the key is sent; a 401 or 403 is a warning naming
291+ `$KRONK_TOKEN`; a missing model warns `kronk model pull`; the window
292+ comes from `/v1/kronk/models/<escaped id>`;
293+ - `pullKronk` and `readKronkPullStream`;
294+ - `unloadKronk`: a 404 means "nothing to unload".
295+ - The `kronk` registry entry is in `openai_compat.go`:
296+ - `thinkingOffTemplate`;
297+ - `progress: false`.
298+ - `mm init`:
299+ - `kronk` is in the list of providers and in the local servers it probes;
300+ - `kronkFacts` reads `/v1/kronk/models/<id>`: the metadata as strings,
301+ and `model_config.context-window` (the trained length when it is missing).
302+ - Tests:
303+ - `internal/engine/kronk_test.go`, against a fake Kronk replaying the
304+ measured shapes;
305+ - Resolve rows and DefaultURLs;
306+ - `TestKronkFacts`.
307+ - `go test ./...` passes.
308+ - Demo: `demos/kronk/agent.kronk.yaml` and `README.en.md` / `README.fr.md`.
309+ - Docs EN and FR:
310+ - a new page, how-to `run-with-kronk`;
311+ - updated: configuration (defaults, autoPull, unloadModels, sampling,
312+ summaryMaxTokens, firstTokenTimeout), cli, tools (`unload_model`),
313+ providers, mm-init, thinking-models, turn-reasoning-off,
314+ unload-a-model, run-in-a-docker-sandbox, the indexes.
315+
316+ ## Checked live from the sandbox (Kronk on the Mac)
317+
318+ - The banner shows `ctx: 131072 (/v1/kronk/models)`.
319+ - A turn with bash calls works.
320+ - `/unload` works twice: the second call answers "not loaded".
321+ - `mm init --provider kronk --check` works.
322+
323+ ## Still to check (task 9, on the Mac)
324+
325+ - A real download with `autoPull: true`.
326+ - A real compaction.
327+ - A protected Kronk (`KRONK_TOKEN`).
328+
329+ The quality gate (qlty) is not available in the sandbox.
330+
331+ Seen with Qwopus: a turn ended with the answer only in the reasoning
332+ (`⚠ empty answer`). That comes from the model, not from the provider.
333+
new file mode 100644
@@ -0,0 +1,333 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 75
3+title: Add Kronk provider
4+state: open
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-10-06T03:58:37.611Z
9+updatedAt: 2026-11-11T07:00:31.000Z
10+labels:
11+ - kind::feature
12+ - priority::medium
13+milestone: v1.0.6
14+epic: Providers (LLM Engine)
15+related:
16+ - 62
17+body: |
18+ ## Context
19+
20+ [Kronk](https://www.kronkai.com/) ([ardanlabs/kronk](https://github.com/ardanlabs/kronk))
21+ is a local inference server written in Go by Ardan Labs. It embeds llama.cpp
22+ through yzma, without a llama-server process. `kronk server start` serves an
23+ OpenAI-compatible API (plus Anthropic `/v1/messages`) on `127.0.0.1:11435`.
24+
25+ Add it as an mm provider, `provider: kronk`. It is a new entry of the
26+ `openaiCompat` registry (`internal/engine/openai_compat.go`), like dmr,
27+ llamacpp, llmman and ollama: no new wire protocol. Only the
28+ server-specific parts are new: the probe, pull and unload.
29+
30+ ## What the Kronk manual says (to be measured, not trusted)
31+
32+ Sources: `.manual/chapter-08-model-server.md`, `chapter-09-api-endpoints.md`,
33+ `chapter-10-request-parameters.md`, `chapter-12-security-authentication.md`,
34+ `chapter-14-client-integration.md` in the Kronk repository (read on 2026-11-11, Kronk 1.33.2).
35+
36+ - **Install**: `brew install ardanlabs/kronk/kronk`, or
37+ `go install github.com/ardanlabs/kronk/cmd/kronk@latest`.
38+ - **Start**: `kronk server start`, or `--detach` (log in `~/.kronk/kronk.log`).
39+ - **Address**: `127.0.0.1:11435`, loopback only. `--api-host` or
40+ `KRONK_WEB_API_HOST` changes it. Note the port is 11435, next to Ollama's 11434.
41+ - **Base URL for mm**: `http://127.0.0.1:11435/v1`.
42+ - **Endpoints**:
43+ - `POST /v1/chat/completions`: SSE streaming, `tools` and `tool_choice`,
44+ streamed `tool_calls`, `stream_options.include_usage`;
45+ - `GET /v1/models`: installed models, not only the loaded ones;
46+ - `GET /v1/models/{model}`: 404 when absent;
47+ - `/v1/embeddings`, `/v1/rerank`, `/v1/tokenize`, `/v1/decide`.
48+ - **Model ids**: `provider/modelID`, e.g. `unsloth/Qwen3-0.6B-Q8_0`. `owner/repo:quant`
49+ and `owner/repo/file.gguf` pin a repository or a file.
50+ - **Pull**:
51+ - CLI: `kronk model pull unsloth/Qwen3-0.6B-Q8_0`;
52+ - API: `POST /v1/kronk/models/pull`, which streams progress.
53+ - **Unload**: `POST /v1/kronk/models/unload`.
54+ - **Loaded models**: `GET /v1/kronk/models/ps`.
55+ - **Model details and configuration**: `GET /v1/kronk/models/{model}`, which may
56+ give the context window. The window is also set per model in
57+ `~/.kronk/models/model_config.yaml` (`context-window`).
58+ - **Reasoning**:
59+ - streamed as `reasoning_content`, which mm already reads;
60+ - switched off with the top-level `enable_thinking: false`,
61+ `reasoning_effort`, or `chat_template_kwargs`, which is passed through
62+ to the template;
63+ - Kronk does not translate between these three switches.
64+ - **Auth**:
65+ - the default mode `open` checks no key, and any non-empty key works;
66+ - the modes `authenticated` and `full-protected` require
67+ `Authorization: Bearer <token>`;
68+ - the manual and its client examples use the variable `KRONK_TOKEN`.
69+ - **Errors**:
70+ - before the stream: JSON `{"code","message"}` with an HTTP status;
71+ - after SSE headers: reported inside the stream, with a 200 status.
72+ - **Not documented**:
73+ - whether a model that is installed but not loaded is loaded on the first request;
74+ - whether there is a llama.cpp-style `prompt_progress` (Kronk is not llama-server).
75+
76+ ## Scope
77+
78+ - Registry entry `kronk`:
79+ - `baseURL` `http://127.0.0.1:11435/v1`;
80+ - fallback `http://host.docker.internal:11435/v1`;
81+ - `apiKeyEnv: KRONK_TOKEN`, not required, with a dummy key.
82+ - `startHint`: `start it: kronk server start`. `notFoundHint`: `kronk model pull %s`.
83+ - Probe:
84+ - reachability;
85+ - model present in `/v1/models`;
86+ - context window from `/v1/kronk/models/{model}` if it gives one.
87+ - `thinkingOff`: the variant that Kronk actually obeys (measure it).
88+ - `pull` (`autoPull`) through `POST /v1/kronk/models/pull`.
89+ - `unload` through `POST /v1/kronk/models/unload`.
90+ - `progress` stays false unless a measurement shows `prompt_progress`.
91+ - Explain:
92+ - 401 or 403 names `KRONK_TOKEN`;
93+ - 404 says `kronk model pull`;
94+ - a connection refused gives the start hint.
95+ - `mm init`:
96+ - the provider is offered with its default URL;
97+ - presets if needed (`internal/agentinit/presets.yaml`).
98+ - Demo config in `demos/kronk/` with a small model that calls tools well.
99+
100+ Out of scope: the Anthropic `/v1/messages` route, `/v1/responses`, and Kronk's
101+ `/v1/decide`. Kronk's `/v1/decide` could later back mm's deciders, which would
102+ be a separate ticket.
103+
104+ ## Sandbox note
105+
106+ Kronk binds to loopback by default. From an sbx sandbox, the fallback
107+ `host.docker.internal:11435` only answers if the host started
108+ `kronk server start --api-host 0.0.0.0:11435`, and the policy allows
109+ `localhost:11435`. The how-to must say so.
110+
111+ ## Acceptance criteria
112+
113+ - `provider: kronk` with no `baseUrl` reaches a local `kronk server start`, and
114+ a turn with a tool call works end to end.
115+ - A model not installed is pulled when `autoPull` is on. Otherwise, the
116+ warning names `kronk model pull`.
117+ - The context window shown at start-up is correct, or labelled as unknown.
118+ - Compaction summary runs with thinking off.
119+ - Unit tests: a Resolve row, the probe against a fake server, pull, unload, Explain.
120+ - Docs EN and FR: a how-to, the provider reference row, `configuration.md`,
121+ the indexes, and the provider README / package diagram if touched.
122+tasks:
123+ - id: 1
124+ title: "Measure Kronk: /v1/models, model details (window), reasoning field and thinking switch, prompt_progress, 404/401 shapes, load on first request"
125+ state: closed
126+ priority: high
127+ author:
128+ name: k33g
129+ email: ph.charriere@gmail.com
130+ createdAt: 2026-11-11T06:24:34.000Z
131+ updatedAt: 2026-11-11T06:46:21.000Z
132+ closedAt: 2026-11-11T06:46:21.000Z
133+ - id: 2
134+ title: Registry entry kronk (URL, fallback, KRONK_TOKEN, hints, thinkingOff)
135+ state: closed
136+ priority: high
137+ author:
138+ name: k33g
139+ email: ph.charriere@gmail.com
140+ createdAt: 2026-11-11T06:24:34.000Z
141+ updatedAt: 2026-11-11T07:00:31.000Z
142+ closedAt: 2026-11-11T07:00:31.000Z
143+ - id: 3
144+ title: probeKronk (reachability, model installed, context window)
145+ state: closed
146+ priority: medium
147+ author:
148+ name: k33g
149+ email: ph.charriere@gmail.com
150+ createdAt: 2026-11-11T06:24:34.000Z
151+ updatedAt: 2026-11-11T07:00:31.000Z
152+ closedAt: 2026-11-11T07:00:31.000Z
153+ - id: 4
154+ title: pullKronk (POST /v1/kronk/models/pull, streamed progress) and unloadKronk (POST /v1/kronk/models/unload)
155+ state: closed
156+ priority: medium
157+ author:
158+ name: k33g
159+ email: ph.charriere@gmail.com
160+ createdAt: 2026-11-11T06:24:34.000Z
161+ updatedAt: 2026-11-11T07:00:31.000Z
162+ closedAt: 2026-11-11T07:00:31.000Z
163+ - id: 5
164+ title: "Tests: Resolve row, probe, pull, unload, Explain against a fake server"
165+ state: closed
166+ priority: high
167+ author:
168+ name: k33g
169+ email: ph.charriere@gmail.com
170+ createdAt: 2026-11-11T06:24:34.000Z
171+ updatedAt: 2026-11-11T07:00:31.000Z
172+ closedAt: 2026-11-11T07:00:31.000Z
173+ - id: 6
174+ title: mm init support (provider list, default URL comment, presets if needed)
175+ state: closed
176+ priority: low
177+ author:
178+ name: k33g
179+ email: ph.charriere@gmail.com
180+ createdAt: 2026-11-11T06:24:34.000Z
181+ updatedAt: 2026-11-11T07:00:31.000Z
182+ closedAt: 2026-11-11T07:00:31.000Z
183+ - id: 7
184+ title: Demo config demos/kronk/ with a small tool-calling model
185+ state: closed
186+ priority: low
187+ author:
188+ name: k33g
189+ email: ph.charriere@gmail.com
190+ createdAt: 2026-11-11T06:24:34.000Z
191+ updatedAt: 2026-11-11T07:00:31.000Z
192+ closedAt: 2026-11-11T07:00:31.000Z
193+ - id: 8
194+ title: Docs EN+FR (how-to incl. sandbox --api-host note, reference, configuration, indexes, diagram)
195+ state: closed
196+ priority: medium
197+ author:
198+ name: k33g
199+ email: ph.charriere@gmail.com
200+ createdAt: 2026-11-11T06:24:34.000Z
201+ updatedAt: 2026-11-11T07:00:31.000Z
202+ closedAt: 2026-11-11T07:00:31.000Z
203+ - id: 9
204+ title: Live check against Kronk on the Mac (tool call, window, compaction, pull, unload)
205+ state: open
206+ priority: high
207+ author:
208+ name: k33g
209+ email: ph.charriere@gmail.com
210+ createdAt: 2026-11-11T06:24:34.000Z
211+comments:
212+ - id: 1
213+ author:
214+ name: k33g
215+ email: ph.charriere@gmail.com
216+ createdAt: 2026-11-11T06:46:21.000Z
217+ body: |
218+ ## Measured on the Mac's Kronk from the sandbox (2026-11-11)
219+
220+ Model: `mradermacher/Qwopus3.5-4B-Coder.Q8_0`. Reached at
221+ `http://host.docker.internal:11435`; `127.0.0.1:11435` does not answer from
222+ the sandbox, so the fallback is needed.
223+
224+ **Where the manual is wrong or silent**
225+
226+ - **Error shape is OpenAI's, not the manual's**: `{"error":{"message","type","code"}}`.
227+ - Unknown model at chat: HTTP 404, `type: not_found_error`, `code: not_found`,
228+ message `... model not found: "foo/bar-Q4"`.
229+ - Id without a provider (`nope`): HTTP 400, `invalid_argument`,
230+ `must use provider/modelID or provider/modelID/profile`.
231+ - **Model ids contain a `/`, so the path must be encoded.**
232+ - `GET /v1/models/{id}` and `GET /v1/kronk/models/{id}` need `%2F`
233+ (`url.PathEscape`). A raw `/` gives 404 or 400.
234+ - **`/v1/models` also lists profiles**: `mradermacher/Qwopus3.5-4B-Coder.Q8_0/AGENT`.
235+ - **A model is loaded on the first request.** After an unload, the chat request
236+ answered 200 in 2.3 s. No pull is needed when the model is installed.
237+
238+ **Measured values for the implementation**
239+
240+ - **Context window**: `GET /v1/kronk/models/<encoded id>` gives
241+ `model_config.context-window` (131072 here).
242+ - GGUF metadata says `qwen35.context_length` 262144; the configured
243+ value is the one served.
244+ - **Loaded models**: `GET /v1/kronk/models/ps`.
245+ - Returns `[{id, status:"loaded", size, vram_total, kv_cache, ...}]`.
246+ - Returns `[]` when nothing is loaded.
247+ - **Reasoning**:
248+ - streamed as `delta.reasoning_content`;
249+ - `usage.completion_tokens_details.reasoning_tokens` is filled.
250+ - **Thinking off**:
251+ - `chat_template_kwargs: {enable_thinking: false}` works, so the
252+ existing `thinkingOffTemplate` fits;
253+ - top-level `enable_thinking: false` works too;
254+ - `reasoning_effort: "none"` is IGNORED (the model still reasons).
255+ - **`return_progress`: no `prompt_progress` chunk**, so `progress: false`.
256+ - **`stream_options.include_usage`** works: a last chunk with `choices: []`
257+ and the usage.
258+ - Extra fields in the usage: `tokens_per_second`,
259+ `time_to_first_token_ms`, `draft_*` (MTP speculative decoding).
260+ - **Streamed tool call** is standard OpenAI:
261+ - the first delta carries `id`, `name` and `arguments: ""`;
262+ - the next delta carries the arguments;
263+ - the last has `finish_reason: "tool_calls"`.
264+ - **Pull**: `POST /v1/kronk/models/pull {"model_url": "<id>"}`.
265+ - The server answers HTTP 200 with SSE `data: {status, progress?, meta?}`.
266+ - `progress` = `{src, current_bytes, total_bytes, mb_per_sec, complete}`.
267+ - **Success** = the last record has `status: "downloaded"` and `downloaded: true`.
268+ - **Failure** = the last record is only an error `status` (still HTTP 200),
269+ e.g. `download: resolve "nobody/x": ... not found at provider "nobody"`.
270+ - An already-installed model answers `-> already downloaded`, then `downloaded`.
271+ - **Unload**: `POST /v1/kronk/models/unload {"id": "<id>"}`.
272+ - Success: `{"status":"unloaded","id":...}`.
273+ - Not loaded: HTTP 404, `not_found`.
274+ - Streams still running: `failed_precondition`.
275+
276+ **Not measured**: the 401 and 403 shapes, which need an `authenticated`
277+ mode on the server.
278+
279+ Source for the pull and unload request types:
280+ `cmd/server/app/domain/toolapp/{model.go,models.go}` in ardanlabs/kronk.
281+ - id: 2
282+ author:
283+ name: k33g
284+ email: ph.charriere@gmail.com
285+ createdAt: 2026-11-11T07:00:31.000Z
286+ body: |
287+ ## Implemented on branch feature/kronk-provider
288+
289+ - `internal/engine/kronk.go`:
290+ - `probeKronk`: the key is sent; a 401 or 403 is a warning naming
291+ `$KRONK_TOKEN`; a missing model warns `kronk model pull`; the window
292+ comes from `/v1/kronk/models/<escaped id>`;
293+ - `pullKronk` and `readKronkPullStream`;
294+ - `unloadKronk`: a 404 means "nothing to unload".
295+ - The `kronk` registry entry is in `openai_compat.go`:
296+ - `thinkingOffTemplate`;
297+ - `progress: false`.
298+ - `mm init`:
299+ - `kronk` is in the list of providers and in the local servers it probes;
300+ - `kronkFacts` reads `/v1/kronk/models/<id>`: the metadata as strings,
301+ and `model_config.context-window` (the trained length when it is missing).
302+ - Tests:
303+ - `internal/engine/kronk_test.go`, against a fake Kronk replaying the
304+ measured shapes;
305+ - Resolve rows and DefaultURLs;
306+ - `TestKronkFacts`.
307+ - `go test ./...` passes.
308+ - Demo: `demos/kronk/agent.kronk.yaml` and `README.en.md` / `README.fr.md`.
309+ - Docs EN and FR:
310+ - a new page, how-to `run-with-kronk`;
311+ - updated: configuration (defaults, autoPull, unloadModels, sampling,
312+ summaryMaxTokens, firstTokenTimeout), cli, tools (`unload_model`),
313+ providers, mm-init, thinking-models, turn-reasoning-off,
314+ unload-a-model, run-in-a-docker-sandbox, the indexes.
315+
316+ ## Checked live from the sandbox (Kronk on the Mac)
317+
318+ - The banner shows `ctx: 131072 (/v1/kronk/models)`.
319+ - A turn with bash calls works.
320+ - `/unload` works twice: the second call answers "not loaded".
321+ - `mm init --provider kronk --check` works.
322+
323+ ## Still to check (task 9, on the Mac)
324+
325+ - A real download with `autoPull: true`.
326+ - A real compaction.
327+ - A protected Kronk (`KRONK_TOKEN`).
328+
329+ The quality gate (qlty) is not available in the sandbox.
330+
331+ Seen with Qwopus: a turn ended with the answer only in the reasoning
332+ (`⚠ empty answer`). That comes from the model, not from the provider.
333+
modified .tickets/issues/0090-agent-identity.yaml +2 -2
@@ -6,9 +6,9 @@ author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-10-16T15:37:32.062Z
9-updatedAt: 2026-11-01T20:44:45.000Z
9+updatedAt: 2026-11-11T06:57:30.799Z
1010 labels: []
11-epic: Questions
11+epic: Agent Identity
1212 body: |
1313 Des idées pour gérer cela ?
1414 avec l'existant (est ce que un seul useur peut avoir plusieurs identités sur GitHub par exemple)
@@ -6,9 +6,9 @@ author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-10-16T15:37:32.062Z8 createdAt: 2026-10-16T15:37:32.062Z
9-updatedAt: 2026-11-01T20:44:45.000Z9+updatedAt: 2026-11-11T06:57:30.799Z
10 labels: []10 labels: []
11-epic: Questions11+epic: Agent Identity
12 body: |12 body: |
13 Des idées pour gérer cela ?13 Des idées pour gérer cela ?
14 avec l'existant (est ce que un seul useur peut avoir plusieurs identités sur GitHub par exemple)14 avec l'existant (est ce que un seul useur peut avoir plusieurs identités sur GitHub par exemple)
modified .tickets/issues/0111-using-the-settings-guide-create-a-claude-skill.yaml +2 -2
@@ -1,12 +1,12 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 111
33 title: Using the settings guide create a Claude skill
4-state: open
4+state: closed
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-10-21T04:27:19.433Z
9-updatedAt: 2026-11-01T20:44:45.000Z
9+updatedAt: 2026-11-11T06:52:11.123Z
1010 labels: []
1111 epic: Models
1212 body: |
@@ -1,12 +1,12 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 1112 id: 111
3 title: Using the settings guide create a Claude skill3 title: Using the settings guide create a Claude skill
4-state: open4+state: closed
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-10-21T04:27:19.433Z8 createdAt: 2026-10-21T04:27:19.433Z
9-updatedAt: 2026-11-01T20:44:45.000Z9+updatedAt: 2026-11-11T06:52:11.123Z
10 labels: []10 labels: []
11 epic: Models11 epic: Models
12 body: |12 body: |
modified .tickets/issues/0113-mini-me-as-a-service-turn-mm-into-a-dedicated-ap.yaml +11 -3
@@ -1,13 +1,14 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 113
33 title: "Mini Me as a service: turn mm into a dedicated app, or expose it as services — ideas"
4-state: open
4+state: closed
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-10-21T13:00:00.000Z
9-updatedAt: 2026-10-22T17:09:28.961Z
10-labels: []
9+updatedAt: 2026-11-11T07:02:05.377Z
10+labels:
11+ - wontfix
1112 epic: Mini Me as a service
1213 related:
1314 - 71
@@ -172,3 +173,10 @@ comments:
172173 user's prompt (like mm -p); structured output; streaming and
173174 non-streaming; a cancel API. The OpenAI-compatible HTTP façade
174175 (direction B.2) is the candidate: specified in ticket 114.
176+ - id: 2
177+ author:
178+ name: k33g
179+ email: ph.charriere@gmail.com
180+ createdAt: 2026-11-11T07:02:01.934Z
181+ body: |
182+ Use ACP instead
@@ -1,13 +1,14 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 1132 id: 113
3 title: "Mini Me as a service: turn mm into a dedicated app, or expose it as services — ideas"3 title: "Mini Me as a service: turn mm into a dedicated app, or expose it as services — ideas"
4-state: open4+state: closed
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-10-21T13:00:00.000Z8 createdAt: 2026-10-21T13:00:00.000Z
9-updatedAt: 2026-10-22T17:09:28.961Z9+updatedAt: 2026-11-11T07:02:05.377Z
10-labels: []10+labels:
11+ - wontfix
11 epic: Mini Me as a service12 epic: Mini Me as a service
12 related:13 related:
13 - 7114 - 71
@@ -172,3 +173,10 @@ comments:
172 user's prompt (like mm -p); structured output; streaming and173 user's prompt (like mm -p); structured output; streaming and
173 non-streaming; a cancel API. The OpenAI-compatible HTTP façade174 non-streaming; a cancel API. The OpenAI-compatible HTTP façade
174 (direction B.2) is the candidate: specified in ticket 114.175 (direction B.2) is the candidate: specified in ticket 114.
176+ - id: 2
177+ author:
178+ name: k33g
179+ email: ph.charriere@gmail.com
180+ createdAt: 2026-11-11T07:02:01.934Z
181+ body: |
182+ Use ACP instead
modified .tickets/issues/0140-rephrase-the-question.yaml +2 -2
@@ -1,12 +1,12 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 140
33 title: Rephrase the question
4-state: open
4+state: closed
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-10-24T04:11:57.315Z
9-updatedAt: 2026-10-24T04:15:54.392Z
9+updatedAt: 2026-11-11T06:49:35.456Z
1010 labels:
1111 - hooks
1212 epic: Demos
@@ -1,12 +1,12 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 1402 id: 140
3 title: Rephrase the question3 title: Rephrase the question
4-state: open4+state: closed
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-10-24T04:11:57.315Z8 createdAt: 2026-10-24T04:11:57.315Z
9-updatedAt: 2026-10-24T04:15:54.392Z9+updatedAt: 2026-11-11T06:49:35.456Z
10 labels:10 labels:
11 - hooks11 - hooks
12 epic: Demos12 epic: Demos
modified .tickets/issues/0151-blog-post-to-explain-how-to-create-a-wasm-declar.yaml +2 -2
@@ -1,12 +1,12 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 151
33 title: Blog post to explain how to create a wasm declared tool
4-state: open
4+state: closed
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-10-25T04:23:41.209Z
9-updatedAt: 2026-10-25T04:23:41.209Z
9+updatedAt: 2026-11-11T07:03:15.692Z
1010 labels: []
1111 epic: WASM Module
1212 body: ""
@@ -1,12 +1,12 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 1512 id: 151
3 title: Blog post to explain how to create a wasm declared tool3 title: Blog post to explain how to create a wasm declared tool
4-state: open4+state: closed
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-10-25T04:23:41.209Z8 createdAt: 2026-10-25T04:23:41.209Z
9-updatedAt: 2026-10-25T04:23:41.209Z9+updatedAt: 2026-11-11T07:03:15.692Z
10 labels: []10 labels: []
11 epic: WASM Module11 epic: WASM Module
12 body: ""12 body: ""
modified .tickets/issues/0152-dev-team.yaml +6 -7
@@ -1,12 +1,12 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 152
33 title: Dev team
4-state: open
4+state: closed
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-10-25T04:44:30.351Z
9-updatedAt: 2026-10-25T12:30:00.000Z
9+updatedAt: 2026-11-11T06:49:12.500Z
1010 labels: []
1111 epic: Demos
1212 body: |+
@@ -64,7 +64,7 @@ tasks:
6464 createdAt: 2026-10-25T07:30:00.000Z
6565 updatedAt: 2026-10-25T07:30:00.000Z
6666 - id: 2
67- title: "Mesurer chaque agent seul (mm -p) sur des exemples réels, ajuster prompts et réglages"
67+ title: Mesurer chaque agent seul (mm -p) sur des exemples réels, ajuster prompts et réglages
6868 state: closed
6969 priority: none
7070 author:
@@ -73,7 +73,7 @@ tasks:
7373 createdAt: 2026-10-25T07:30:00.000Z
7474 updatedAt: 2026-10-25T07:30:00.000Z
7575 - id: 3
76- title: "Mesurer l'équipe de bout en bout via le coordinateur, vérifier le déchargement des modèles (/api/ps)"
76+ title: Mesurer l'équipe de bout en bout via le coordinateur, vérifier le déchargement des modèles (/api/ps)
7777 state: closed
7878 priority: none
7979 author:
@@ -82,7 +82,7 @@ tasks:
8282 createdAt: 2026-10-25T07:30:00.000Z
8383 updatedAt: 2026-10-25T07:30:00.000Z
8484 - id: 4
85- title: "Répondre à la question du modèle de décision (mesure du routage du coordinateur)"
85+ title: Répondre à la question du modèle de décision (mesure du routage du coordinateur)
8686 state: closed
8787 priority: none
8888 author:
@@ -91,7 +91,7 @@ tasks:
9191 createdAt: 2026-10-25T07:30:00.000Z
9292 updatedAt: 2026-10-25T07:30:00.000Z
9393 - id: 5
94- title: "README EN + FR avec diagrammes fancy et exemples d'utilisation ; ligne dans le README racine"
94+ title: README EN + FR avec diagrammes fancy et exemples d'utilisation ; ligne dans le README racine
9595 state: closed
9696 priority: none
9797 author:
@@ -473,4 +473,3 @@ comments:
473473
474474 Remarque : dev.team/go.mod n'était plus là (supprimé de ton côté ?) —
475475 je ne l'ai pas remis.
476-
@@ -1,12 +1,12 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 1522 id: 152
3 title: Dev team3 title: Dev team
4-state: open4+state: closed
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-10-25T04:44:30.351Z8 createdAt: 2026-10-25T04:44:30.351Z
9-updatedAt: 2026-10-25T12:30:00.000Z9+updatedAt: 2026-11-11T06:49:12.500Z
10 labels: []10 labels: []
11 epic: Demos11 epic: Demos
12 body: |+12 body: |+
@@ -64,7 +64,7 @@ tasks:
64 createdAt: 2026-10-25T07:30:00.000Z64 createdAt: 2026-10-25T07:30:00.000Z
65 updatedAt: 2026-10-25T07:30:00.000Z65 updatedAt: 2026-10-25T07:30:00.000Z
66 - id: 266 - id: 2
67- title: "Mesurer chaque agent seul (mm -p) sur des exemples réels, ajuster prompts et réglages"67+ title: Mesurer chaque agent seul (mm -p) sur des exemples réels, ajuster prompts et réglages
68 state: closed68 state: closed
69 priority: none69 priority: none
70 author:70 author:
@@ -73,7 +73,7 @@ tasks:
73 createdAt: 2026-10-25T07:30:00.000Z73 createdAt: 2026-10-25T07:30:00.000Z
74 updatedAt: 2026-10-25T07:30:00.000Z74 updatedAt: 2026-10-25T07:30:00.000Z
75 - id: 375 - id: 3
76- title: "Mesurer l'équipe de bout en bout via le coordinateur, vérifier le déchargement des modèles (/api/ps)"76+ title: Mesurer l'équipe de bout en bout via le coordinateur, vérifier le déchargement des modèles (/api/ps)
77 state: closed77 state: closed
78 priority: none78 priority: none
79 author:79 author:
@@ -82,7 +82,7 @@ tasks:
82 createdAt: 2026-10-25T07:30:00.000Z82 createdAt: 2026-10-25T07:30:00.000Z
83 updatedAt: 2026-10-25T07:30:00.000Z83 updatedAt: 2026-10-25T07:30:00.000Z
84 - id: 484 - id: 4
85- title: "Répondre à la question du modèle de décision (mesure du routage du coordinateur)"85+ title: Répondre à la question du modèle de décision (mesure du routage du coordinateur)
86 state: closed86 state: closed
87 priority: none87 priority: none
88 author:88 author:
@@ -91,7 +91,7 @@ tasks:
91 createdAt: 2026-10-25T07:30:00.000Z91 createdAt: 2026-10-25T07:30:00.000Z
92 updatedAt: 2026-10-25T07:30:00.000Z92 updatedAt: 2026-10-25T07:30:00.000Z
93 - id: 593 - id: 5
94- title: "README EN + FR avec diagrammes fancy et exemples d'utilisation ; ligne dans le README racine"94+ title: README EN + FR avec diagrammes fancy et exemples d'utilisation ; ligne dans le README racine
95 state: closed95 state: closed
96 priority: none96 priority: none
97 author:97 author:
@@ -473,4 +473,3 @@ comments:
473 473
474 Remarque : dev.team/go.mod n'était plus là (supprimé de ton côté ?) —474 Remarque : dev.team/go.mod n'était plus là (supprimé de ton côté ?) —
475 je ne l'ai pas remis.475 je ne l'ai pas remis.
476-
renamed .tickets/issues/0172-nushell-script-client-over-acp-mm-from-a-shell-s.yaml +6 -5
similarity index 91%
rename from .tickets/issues/0172-nushell-script-client-over-acp.yaml
rename to .tickets/issues/0172-nushell-script-client-over-acp-mm-from-a-shell-s.yaml
@@ -6,9 +6,10 @@ author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-10-26T06:30:00.000Z
9-updatedAt: 2026-10-26T06:30:00.000Z
9+updatedAt: 2026-11-11T07:00:55.349Z
1010 labels:
1111 - kind::feature
12+milestone: v2.0.0
1213 epic: SDK
1314 related:
1415 - 167
@@ -42,7 +43,7 @@ tasks:
4243 email: ph.charriere@gmail.com
4344 createdAt: 2026-10-26T06:30:00.000Z
4445 - id: 2
45- title: "Get Nushell (sandbox or the user's machine)"
46+ title: Get Nushell (sandbox or the user's machine)
4647 state: open
4748 priority: none
4849 author:
@@ -58,7 +59,7 @@ tasks:
5859 email: ph.charriere@gmail.com
5960 createdAt: 2026-10-26T06:30:00.000Z
6061 - id: 4
61- title: "Permission callback (session/request_permission), refuse by default"
62+ title: Permission callback (session/request_permission), refuse by default
6263 state: open
6364 priority: none
6465 author:
@@ -66,7 +67,7 @@ tasks:
6667 email: ph.charriere@gmail.com
6768 createdAt: 2026-10-26T06:30:00.000Z
6869 - id: 5
69- title: "Examples (not streamed, streamed, cancelled, permission) run on llmman"
70+ title: Examples (not streamed, streamed, cancelled, permission) run on llmman
7071 state: open
7172 priority: none
7273 author:
@@ -74,7 +75,7 @@ tasks:
7475 email: ph.charriere@gmail.com
7576 createdAt: 2026-10-26T06:30:00.000Z
7677 - id: 6
77- title: "README EN + FR with measured results and limits"
78+ title: README EN + FR with measured results and limits
7879 state: open
7980 priority: none
8081 author:
similarity index 91%
rename from .tickets/issues/0172-nushell-script-client-over-acp.yaml
rename to .tickets/issues/0172-nushell-script-client-over-acp-mm-from-a-shell-s.yaml
@@ -6,9 +6,10 @@ author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-10-26T06:30:00.000Z8 createdAt: 2026-10-26T06:30:00.000Z
9-updatedAt: 2026-10-26T06:30:00.000Z9+updatedAt: 2026-11-11T07:00:55.349Z
10 labels:10 labels:
11 - kind::feature11 - kind::feature
12+milestone: v2.0.0
12 epic: SDK13 epic: SDK
13 related:14 related:
14 - 16715 - 167
@@ -42,7 +43,7 @@ tasks:
42 email: ph.charriere@gmail.com43 email: ph.charriere@gmail.com
43 createdAt: 2026-10-26T06:30:00.000Z44 createdAt: 2026-10-26T06:30:00.000Z
44 - id: 245 - id: 2
45- title: "Get Nushell (sandbox or the user's machine)"46+ title: Get Nushell (sandbox or the user's machine)
46 state: open47 state: open
47 priority: none48 priority: none
48 author:49 author:
@@ -58,7 +59,7 @@ tasks:
58 email: ph.charriere@gmail.com59 email: ph.charriere@gmail.com
59 createdAt: 2026-10-26T06:30:00.000Z60 createdAt: 2026-10-26T06:30:00.000Z
60 - id: 461 - id: 4
61- title: "Permission callback (session/request_permission), refuse by default"62+ title: Permission callback (session/request_permission), refuse by default
62 state: open63 state: open
63 priority: none64 priority: none
64 author:65 author:
@@ -66,7 +67,7 @@ tasks:
66 email: ph.charriere@gmail.com67 email: ph.charriere@gmail.com
67 createdAt: 2026-10-26T06:30:00.000Z68 createdAt: 2026-10-26T06:30:00.000Z
68 - id: 569 - id: 5
69- title: "Examples (not streamed, streamed, cancelled, permission) run on llmman"70+ title: Examples (not streamed, streamed, cancelled, permission) run on llmman
70 state: open71 state: open
71 priority: none72 priority: none
72 author:73 author:
@@ -74,7 +75,7 @@ tasks:
74 email: ph.charriere@gmail.com75 email: ph.charriere@gmail.com
75 createdAt: 2026-10-26T06:30:00.000Z76 createdAt: 2026-10-26T06:30:00.000Z
76 - id: 677 - id: 6
77- title: "README EN + FR with measured results and limits"78+ title: README EN + FR with measured results and limits
78 state: open79 state: open
79 priority: none80 priority: none
80 author:81 author:
renamed .tickets/issues/0178-support-openai-s-responses-api-to-keep-reasoning.yaml +7 -6
similarity index 90%
rename from .tickets/issues/0178-support-openai-responses-api.yaml
rename to .tickets/issues/0178-support-openai-s-responses-api-to-keep-reasoning.yaml
@@ -1,14 +1,15 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 178
3-title: "Support OpenAI's Responses API, to keep reasoning and tools together (gpt-6-luna)"
3+title: Support OpenAI's Responses API, to keep reasoning and tools together (gpt-6-luna)
44 state: open
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-10-26T19:30:00.000Z
9-updatedAt: 2026-10-26T19:30:00.000Z
9+updatedAt: 2026-11-11T06:48:27.885Z
1010 labels:
1111 - kind::feature
12+milestone: v2.0.0
1213 epic: Providers (LLM Engine)
1314 related:
1415 - 176
@@ -60,7 +61,7 @@ tasks:
6061 email: ph.charriere@gmail.com
6162 createdAt: 2026-10-26T19:30:00.000Z
6263 - id: 2
63- title: "Check which OpenAI-compatible servers serve /v1/responses (vLLM, Ollama, llama-server…)"
64+ title: Check which OpenAI-compatible servers serve /v1/responses (vLLM, Ollama, llama-server…)
6465 state: open
6566 priority: none
6667 author:
@@ -76,7 +77,7 @@ tasks:
7677 email: ph.charriere@gmail.com
7778 createdAt: 2026-10-26T19:30:00.000Z
7879 - id: 4
79- title: "Reasoning items carried between turns; thoughts, usage, structuredOutput, compaction, max_output_tokens"
80+ title: Reasoning items carried between turns; thoughts, usage, structuredOutput, compaction, max_output_tokens
8081 state: open
8182 priority: none
8283 author:
@@ -84,7 +85,7 @@ tasks:
8485 email: ph.charriere@gmail.com
8586 createdAt: 2026-10-26T19:30:00.000Z
8687 - id: 5
87- title: "Tests against a fake /v1/responses server; a live check with a reasoning model"
88+ title: Tests against a fake /v1/responses server; a live check with a reasoning model
8889 state: open
8990 priority: none
9091 author:
@@ -92,7 +93,7 @@ tasks:
9293 email: ph.charriere@gmail.com
9394 createdAt: 2026-10-26T19:30:00.000Z
9495 - id: 6
95- title: "mm init and docs EN + FR"
96+ title: mm init and docs EN + FR
9697 state: open
9798 priority: none
9899 author:
similarity index 90%
rename from .tickets/issues/0178-support-openai-responses-api.yaml
rename to .tickets/issues/0178-support-openai-s-responses-api-to-keep-reasoning.yaml
@@ -1,14 +1,15 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 1782 id: 178
3-title: "Support OpenAI's Responses API, to keep reasoning and tools together (gpt-6-luna)"3+title: Support OpenAI's Responses API, to keep reasoning and tools together (gpt-6-luna)
4 state: open4 state: open
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-10-26T19:30:00.000Z8 createdAt: 2026-10-26T19:30:00.000Z
9-updatedAt: 2026-10-26T19:30:00.000Z9+updatedAt: 2026-11-11T06:48:27.885Z
10 labels:10 labels:
11 - kind::feature11 - kind::feature
12+milestone: v2.0.0
12 epic: Providers (LLM Engine)13 epic: Providers (LLM Engine)
13 related:14 related:
14 - 17615 - 176
@@ -60,7 +61,7 @@ tasks:
60 email: ph.charriere@gmail.com61 email: ph.charriere@gmail.com
61 createdAt: 2026-10-26T19:30:00.000Z62 createdAt: 2026-10-26T19:30:00.000Z
62 - id: 263 - id: 2
63- title: "Check which OpenAI-compatible servers serve /v1/responses (vLLM, Ollama, llama-server…)"64+ title: Check which OpenAI-compatible servers serve /v1/responses (vLLM, Ollama, llama-server…)
64 state: open65 state: open
65 priority: none66 priority: none
66 author:67 author:
@@ -76,7 +77,7 @@ tasks:
76 email: ph.charriere@gmail.com77 email: ph.charriere@gmail.com
77 createdAt: 2026-10-26T19:30:00.000Z78 createdAt: 2026-10-26T19:30:00.000Z
78 - id: 479 - id: 4
79- title: "Reasoning items carried between turns; thoughts, usage, structuredOutput, compaction, max_output_tokens"80+ title: Reasoning items carried between turns; thoughts, usage, structuredOutput, compaction, max_output_tokens
80 state: open81 state: open
81 priority: none82 priority: none
82 author:83 author:
@@ -84,7 +85,7 @@ tasks:
84 email: ph.charriere@gmail.com85 email: ph.charriere@gmail.com
85 createdAt: 2026-10-26T19:30:00.000Z86 createdAt: 2026-10-26T19:30:00.000Z
86 - id: 587 - id: 5
87- title: "Tests against a fake /v1/responses server; a live check with a reasoning model"88+ title: Tests against a fake /v1/responses server; a live check with a reasoning model
88 state: open89 state: open
89 priority: none90 priority: none
90 author:91 author:
@@ -92,7 +93,7 @@ tasks:
92 email: ph.charriere@gmail.com93 email: ph.charriere@gmail.com
93 createdAt: 2026-10-26T19:30:00.000Z94 createdAt: 2026-10-26T19:30:00.000Z
94 - id: 695 - id: 6
95- title: "mm init and docs EN + FR"96+ title: mm init and docs EN + FR
96 state: open97 state: open
97 priority: none98 priority: none
98 author:99 author:
modified .tickets/issues/0183-acp-use-the-client-s-fs-and-terminal-services-wh.yaml +2 -2
@@ -6,12 +6,12 @@ author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-10-31T16:39:23.000Z
9-updatedAt: 2026-11-01T20:42:22.000Z
9+updatedAt: 2026-11-11T00:28:50.000Z
1010 labels:
1111 - enhancement
1212 - kind::feature
1313 - tools
14-milestone: v2.0.0
14+milestone: v1.0.6
1515 epic: ACP integration
1616 body: |
1717 ## Context
@@ -6,12 +6,12 @@ author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-10-31T16:39:23.000Z8 createdAt: 2026-10-31T16:39:23.000Z
9-updatedAt: 2026-11-01T20:42:22.000Z9+updatedAt: 2026-11-11T00:28:50.000Z
10 labels:10 labels:
11 - enhancement11 - enhancement
12 - kind::feature12 - kind::feature
13 - tools13 - tools
14-milestone: v2.0.014+milestone: v1.0.6
15 epic: ACP integration15 epic: ACP integration
16 body: |16 body: |
17 ## Context17 ## Context
modified .tickets/issues/0194-better-understanding-of-the-code.yaml +2 -1
@@ -6,9 +6,10 @@ author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-11-07T04:16:22.000Z
9-updatedAt: 2026-11-07T04:16:44.000Z
9+updatedAt: 2026-11-11T00:29:35.282Z
1010 labels: []
1111 milestone: v1.0.6
12+epic: Documentation
1213 body: ""
1314 tasks:
1415 - id: 1
@@ -6,9 +6,10 @@ author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-11-07T04:16:22.000Z8 createdAt: 2026-11-07T04:16:22.000Z
9-updatedAt: 2026-11-07T04:16:44.000Z9+updatedAt: 2026-11-11T00:29:35.282Z
10 labels: []10 labels: []
11 milestone: v1.0.611 milestone: v1.0.6
12+epic: Documentation
12 body: ""13 body: ""
13 tasks:14 tasks:
14 - id: 115 - id: 1
modified .tickets/issues/0197-how-to-use-mini-me-to-help-making-suggestions-wh.yaml +2 -1
@@ -6,9 +6,10 @@ author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-11-07T08:02:50.274Z
9-updatedAt: 2026-11-07T08:02:50.274Z
9+updatedAt: 2026-11-11T00:30:34.700Z
1010 labels: []
1111 milestone: v1.0.6
12+epic: Documentation
1213 body: ""
1314 tasks: []
1415 comments: []
@@ -6,9 +6,10 @@ author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-11-07T08:02:50.274Z8 createdAt: 2026-11-07T08:02:50.274Z
9-updatedAt: 2026-11-07T08:02:50.274Z9+updatedAt: 2026-11-11T00:30:34.700Z
10 labels: []10 labels: []
11 milestone: v1.0.611 milestone: v1.0.6
12+epic: Documentation
12 body: ""13 body: ""
13 tasks: []14 tasks: []
14 comments: []15 comments: []
modified .tickets/issues/0202-acp-custom-extensions-exposed-by-the-client-as-a.yaml +2 -1
@@ -6,11 +6,12 @@ author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-11-10T21:56:01.000Z
9-updatedAt: 2026-11-10T23:30:00.000Z
9+updatedAt: 2026-11-11T00:30:03.268Z
1010 labels:
1111 - kind::feature
1212 - tools
1313 milestone: v1.0.6
14+epic: ACP integration
1415 body: |
1516 In ACP mode (`mm -acp`), the agent must **discover** the custom extensions
1617 the ACP client exposes, and **call them like any other tool**. Discovery
@@ -6,11 +6,12 @@ author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-11-10T21:56:01.000Z8 createdAt: 2026-11-10T21:56:01.000Z
9-updatedAt: 2026-11-10T23:30:00.000Z9+updatedAt: 2026-11-11T00:30:03.268Z
10 labels:10 labels:
11 - kind::feature11 - kind::feature
12 - tools12 - tools
13 milestone: v1.0.613 milestone: v1.0.6
14+epic: ACP integration
14 body: |15 body: |
15 In ACP mode (`mm -acp`), the agent must **discover** the custom extensions16 In ACP mode (`mm -acp`), the agent must **discover** the custom extensions
16 the ACP client exposes, and **call them like any other tool**. Discovery17 the ACP client exposes, and **call them like any other tool**. Discovery
renamed .tickets/issues/0203-how-to-distribute-the-sdks.yaml +7 -6
similarity index 55%
rename from .tickets/issues/0075-add-gronk-provider.yaml
rename to .tickets/issues/0203-how-to-distribute-the-sdks.yaml
@@ -1,15 +1,16 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2-id: 75
3-title: Add Gronk provider
2+id: 203
3+title: How to distribute the SDKs?
44 state: open
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
8-createdAt: 2026-10-06T03:58:37.611Z
9-updatedAt: 2026-11-01T21:05:08.000Z
10-labels: []
8+createdAt: 2026-11-11T06:59:37.515Z
9+updatedAt: 2026-11-11T07:00:02.502Z
10+labels:
11+ - priority::high
1112 milestone: v2.0.0
12-epic: Providers (LLM Engine)
13+epic: SDK
1314 body: ""
1415 tasks: []
1516 comments: []
similarity index 55%
rename from .tickets/issues/0075-add-gronk-provider.yaml
rename to .tickets/issues/0203-how-to-distribute-the-sdks.yaml
@@ -1,15 +1,16 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2-id: 752+id: 203
3-title: Add Gronk provider3+title: How to distribute the SDKs?
4 state: open4 state: open
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8-createdAt: 2026-10-06T03:58:37.611Z8+createdAt: 2026-11-11T06:59:37.515Z
9-updatedAt: 2026-11-01T21:05:08.000Z9+updatedAt: 2026-11-11T07:00:02.502Z
10-labels: []10+labels:
11+ - priority::high
11 milestone: v2.0.012 milestone: v2.0.0
12-epic: Providers (LLM Engine)13+epic: SDK
13 body: ""14 body: ""
14 tasks: []15 tasks: []
15 comments: []16 comments: []
added demos/kronk/README.en.md +37 -0
new file mode 100644
@@ -0,0 +1,37 @@
1+# mini-me on Kronk
2+
3+`agent.kronk.yaml` runs mini-me on [Kronk](https://www.kronkai.com), the Go inference server of Ardan Labs, with `mradermacher/Qwopus3.5-4B-Coder.Q8_0`, a Qwen3.5 4B coder that thinks before it answers. Version française : [README.fr.md](README.fr.md).
4+
5+## Run it
6+
7+```bash
8+kronk model pull mradermacher/Qwopus3.5-4B-Coder.Q8_0
9+kronk server start # 127.0.0.1:11435
10+cd demos/kronk
11+../../mm -config agent.kronk.yaml
12+```
13+
14+The banner should say `provider: kronk` and `ctx: 131072 (/v1/kronk/models)`.
15+
16+A smaller model also works, Qwen3.5 2B. Kronk serves it under another id than the one it was pulled with:
17+
18+```bash
19+kronk model pull unsloth/Qwen3.5-2B-GGUF:Q4_K_M --local # with the server stopped
20+```
21+
22+```yaml
23+model: unsloth/Qwen3.5-2B-Q4_K_M # the id GET /v1/models gives
24+```
25+
26+From a Docker sandbox, start Kronk on the host with `kronk server start --api-host 0.0.0.0:11435` and allow `localhost:11435` in the sandbox policy: `mm` falls back to `host.docker.internal:11435` by itself.
27+
28+## What was checked (2026-11-11, Kronk 1.33.2, from an sbx sandbox)
29+
30+- A turn with tool calls: the agent ran `wc -l`, created a file with `bash`, then read it back.
31+- `/unload`: `unloaded …`, then `… was not loaded: nothing to unload`.
32+- `mm init --provider kronk --check` wrote a file with the window (131072), the GGUF's sampling and the template's reasoning switch read from Kronk, and the model answered.
33+- Seen once: the model ended a turn with its answer in its reasoning and an empty text (`⚠ empty answer`). This comes from the model, not from Kronk.
34+
35+Not checked live: a real download through `autoPull` (the stream was read on an installed model only), a compaction, and a protected Kronk (`KRONK_TOKEN`).
36+
37+See [How to run mini-me on Kronk](../../docs/en/how-to/run-with-kronk.md).
new file mode 100644
@@ -0,0 +1,37 @@
1+# mini-me on Kronk
2+
3+`agent.kronk.yaml` runs mini-me on [Kronk](https://www.kronkai.com), the Go inference server of Ardan Labs, with `mradermacher/Qwopus3.5-4B-Coder.Q8_0`, a Qwen3.5 4B coder that thinks before it answers. Version française : [README.fr.md](README.fr.md).
4+
5+## Run it
6+
7+```bash
8+kronk model pull mradermacher/Qwopus3.5-4B-Coder.Q8_0
9+kronk server start # 127.0.0.1:11435
10+cd demos/kronk
11+../../mm -config agent.kronk.yaml
12+```
13+
14+The banner should say `provider: kronk` and `ctx: 131072 (/v1/kronk/models)`.
15+
16+A smaller model also works, Qwen3.5 2B. Kronk serves it under another id than the one it was pulled with:
17+
18+```bash
19+kronk model pull unsloth/Qwen3.5-2B-GGUF:Q4_K_M --local # with the server stopped
20+```
21+
22+```yaml
23+model: unsloth/Qwen3.5-2B-Q4_K_M # the id GET /v1/models gives
24+```
25+
26+From a Docker sandbox, start Kronk on the host with `kronk server start --api-host 0.0.0.0:11435` and allow `localhost:11435` in the sandbox policy: `mm` falls back to `host.docker.internal:11435` by itself.
27+
28+## What was checked (2026-11-11, Kronk 1.33.2, from an sbx sandbox)
29+
30+- A turn with tool calls: the agent ran `wc -l`, created a file with `bash`, then read it back.
31+- `/unload`: `unloaded …`, then `… was not loaded: nothing to unload`.
32+- `mm init --provider kronk --check` wrote a file with the window (131072), the GGUF's sampling and the template's reasoning switch read from Kronk, and the model answered.
33+- Seen once: the model ended a turn with its answer in its reasoning and an empty text (`⚠ empty answer`). This comes from the model, not from Kronk.
34+
35+Not checked live: a real download through `autoPull` (the stream was read on an installed model only), a compaction, and a protected Kronk (`KRONK_TOKEN`).
36+
37+See [How to run mini-me on Kronk](../../docs/en/how-to/run-with-kronk.md).
added demos/kronk/README.fr.md +37 -0
new file mode 100644
@@ -0,0 +1,37 @@
1+# mini-me sur Kronk
2+
3+`agent.kronk.yaml` fait tourner mini-me sur [Kronk](https://www.kronkai.com), le serveur d'inférence Go d'Ardan Labs, avec `mradermacher/Qwopus3.5-4B-Coder.Q8_0`, un Qwen3.5 4B orienté code qui réfléchit avant de répondre. English version: [README.en.md](README.en.md).
4+
5+## Lancer la démo
6+
7+```bash
8+kronk model pull mradermacher/Qwopus3.5-4B-Coder.Q8_0
9+kronk server start # 127.0.0.1:11435
10+cd demos/kronk
11+../../mm -config agent.kronk.yaml
12+```
13+
14+La bannière doit afficher `provider: kronk` et `ctx: 131072 (/v1/kronk/models)`.
15+
16+Un modèle plus petit marche aussi, Qwen3.5 2B. Kronk le sert sous un autre id que celui du téléchargement :
17+
18+```bash
19+kronk model pull unsloth/Qwen3.5-2B-GGUF:Q4_K_M --local # serveur arrêté
20+```
21+
22+```yaml
23+model: unsloth/Qwen3.5-2B-Q4_K_M # l'id que donne GET /v1/models
24+```
25+
26+Depuis une sandbox Docker, lancez Kronk sur l'hôte avec `kronk server start --api-host 0.0.0.0:11435` et autorisez `localhost:11435` dans la politique de la sandbox : `mm` se replie tout seul sur `host.docker.internal:11435`.
27+
28+## Ce qui a été vérifié (2026-11-11, Kronk 1.33.2, depuis une sandbox sbx)
29+
30+- Un tour avec des appels d'outils : l'agent a lancé `wc -l`, créé un fichier avec `bash`, puis l'a relu.
31+- `/unload` : `unloaded …`, puis `… was not loaded: nothing to unload`.
32+- `mm init --provider kronk --check` a écrit un fichier avec la fenêtre (131072), l'échantillonnage du GGUF et l'interrupteur de raisonnement du template lus auprès de Kronk, et le modèle a répondu.
33+- Vu une fois : le modèle a terminé un tour avec sa réponse dans son raisonnement et un texte vide (`⚠ empty answer`). Cela vient du modèle, pas de Kronk.
34+
35+Pas vérifié en direct : un vrai téléchargement par `autoPull` (le flux n'a été lu que sur un modèle déjà installé), une compaction, et un Kronk protégé (`KRONK_TOKEN`).
36+
37+Voir [Faire tourner mini-me sur Kronk](../../docs/fr/how-to/run-with-kronk.md).
new file mode 100644
@@ -0,0 +1,37 @@
1+# mini-me sur Kronk
2+
3+`agent.kronk.yaml` fait tourner mini-me sur [Kronk](https://www.kronkai.com), le serveur d'inférence Go d'Ardan Labs, avec `mradermacher/Qwopus3.5-4B-Coder.Q8_0`, un Qwen3.5 4B orienté code qui réfléchit avant de répondre. English version: [README.en.md](README.en.md).
4+
5+## Lancer la démo
6+
7+```bash
8+kronk model pull mradermacher/Qwopus3.5-4B-Coder.Q8_0
9+kronk server start # 127.0.0.1:11435
10+cd demos/kronk
11+../../mm -config agent.kronk.yaml
12+```
13+
14+La bannière doit afficher `provider: kronk` et `ctx: 131072 (/v1/kronk/models)`.
15+
16+Un modèle plus petit marche aussi, Qwen3.5 2B. Kronk le sert sous un autre id que celui du téléchargement :
17+
18+```bash
19+kronk model pull unsloth/Qwen3.5-2B-GGUF:Q4_K_M --local # serveur arrêté
20+```
21+
22+```yaml
23+model: unsloth/Qwen3.5-2B-Q4_K_M # l'id que donne GET /v1/models
24+```
25+
26+Depuis une sandbox Docker, lancez Kronk sur l'hôte avec `kronk server start --api-host 0.0.0.0:11435` et autorisez `localhost:11435` dans la politique de la sandbox : `mm` se replie tout seul sur `host.docker.internal:11435`.
27+
28+## Ce qui a été vérifié (2026-11-11, Kronk 1.33.2, depuis une sandbox sbx)
29+
30+- Un tour avec des appels d'outils : l'agent a lancé `wc -l`, créé un fichier avec `bash`, puis l'a relu.
31+- `/unload` : `unloaded …`, puis `… was not loaded: nothing to unload`.
32+- `mm init --provider kronk --check` a écrit un fichier avec la fenêtre (131072), l'échantillonnage du GGUF et l'interrupteur de raisonnement du template lus auprès de Kronk, et le modèle a répondu.
33+- Vu une fois : le modèle a terminé un tour avec sa réponse dans son raisonnement et un texte vide (`⚠ empty answer`). Cela vient du modèle, pas de Kronk.
34+
35+Pas vérifié en direct : un vrai téléchargement par `autoPull` (le flux n'a été lu que sur un modèle déjà installé), une compaction, et un Kronk protégé (`KRONK_TOKEN`).
36+
37+Voir [Faire tourner mini-me sur Kronk](../../docs/fr/how-to/run-with-kronk.md).
added demos/kronk/agent.kronk.yaml +218 -0
new file mode 100644
@@ -0,0 +1,218 @@
1+# Agent settings — mini-me served by Kronk (https://www.kronkai.com), the Go
2+# inference server of Ardan Labs, through its OpenAI-compatible endpoint
3+# (/v1/chat/completions).
4+#
5+# brew install ardanlabs/kronk/kronk
6+# kronk model pull mradermacher/Qwopus3.5-4B-Coder.Q8_0
7+# kronk server start # 127.0.0.1:11435 — loopback only
8+#
9+# Kronk loads an installed model on the first request. A model that is not
10+# installed is a "model not found" error: pull it first, or set autoPull: true
11+# and mm pulls it at start-up.
12+provider: kronk
13+
14+# The model id exactly as Kronk lists it on GET /v1/models:
15+# provider/modelID.
16+model: mradermacher/Qwopus3.5-4B-Coder.Q8_0
17+
18+# `kronk server start`'s default; the OpenAI routes sit under /v1.
19+baseUrl: http://127.0.0.1:11435/v1
20+
21+# From a Docker sandbox, Kronk runs on the host. It answers there only when
22+# started with `kronk server start --api-host 0.0.0.0:11435` (and the sandbox
23+# policy allows localhost:11435).
24+fallback: http://host.docker.internal:11435/v1
25+
26+# Kronk's default `open` mode checks no key. In its `authenticated` or
27+# `full-protected` mode, export a token (never put it here):
28+# export KRONK_TOKEN=$(cat ~/.kronk/keys/master.jwt)
29+# apiKeyEnv: KRONK_TOKEN
30+
31+# Yolo mode. `true` (default): the tools run without asking.
32+# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change
33+# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the
34+# terminal, the window and the editor; MCP tools ask too.
35+# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs),
36+# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false.
37+yolo: true
38+
39+# The `bash` tool.
40+# `false` removes it: the model can still read and edit files (editTools) and load skills,
41+# but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool".
42+bashTool: true
43+
44+# The built-in file tools: read_file, write_file, edit_file.
45+editTools: true
46+
47+# The built-in plan tool for managing multi-step plans.
48+# Set to false to disable plan creation and tracking via the `plan` tool.
49+planTool: true
50+
51+# Language servers for the `lsp` tool (definition, references, hover, symbols, diagnostics, rename).
52+# The file extension picks the server. A server missing from the PATH is skipped with a warning.
53+# No server listed = no `lsp` tool. Examples (uncomment what you have installed):
54+# lsp:
55+# servers:
56+# - name: gopls
57+# command: gopls
58+# extensions: [".go"]
59+# - name: typescript
60+# command: typescript-language-server
61+# args: ["--stdio"]
62+# extensions: [".ts", ".tsx", ".js", ".jsx"]
63+# - name: python
64+# command: pyright-langserver
65+# args: ["--stdio"]
66+# extensions: [".py"]
67+# - name: rust
68+# command: rust-analyzer
69+# extensions: [".rs"]
70+
71+# How many tokens the server actually serves.
72+# 0 = ask Kronk: GET /v1/kronk/models/<id> gives the window the model is
73+# configured with (`context-window` in ~/.kronk/models/model_config.yaml),
74+# loaded or not, so it is known from the start.
75+contextWindow: 0
76+
77+# Max number of characters a tool returns to the model.
78+# Context safeguard: beyond that, the output is truncated (beginning + end kept).
79+maxOutput: 16000
80+
81+# Max number of model <-> tools round trips for a single question.
82+# Raise it for tasks that need long chains of commands.
83+maxTurns: 40
84+
85+# Where the `read_skill` tool looks for its markdown procedures.
86+# The path is relative to the CURRENT DIRECTORY: run the agent from demos/kronk/.
87+skillsDir: ../../skills
88+
89+# Persistent sessions, per project.
90+# When enabled, every conversation is saved as one JSON file under `dir`.
91+# `/sessions` lists them, `/resume` continues one; in an editor, "Reload Agent" replays the thread.
92+sessions:
93+ enabled: true
94+ dir: .mm/sessions
95+
96+# How many lines of a command's output are echoed to the terminal.
97+# 0 disables it.
98+previewLines: 20
99+
100+displayCommands: true
101+# Show what a thinking model reasons about before it answers (dim italic in the
102+# terminal, its own style in the window, a thought chunk in an editor).
103+# The reasoning is always READ — it is what keeps the watchdog alive while the
104+# model thinks — this only says whether to show it.
105+showThinking: true
106+
107+
108+# The system prompt: what the agent is, and what it is allowed to do.
109+# This is the knob to play with — it is the shortest path to changing behaviour.
110+system: |
111+ Your name is Kronk-Bob.
112+ You are a coding agent working in a terminal.
113+ You have a "bash" tool to run shell commands.
114+ Use it to explore files, run tests, inspect the repository, etc.
115+ Chain several commands if needed, then answer clearly in English.
116+
117+ A request often mixes things you answer from yourself ("say hello") with
118+ things only a command can answer ("list the files"). Handle every part, in
119+ the order asked, and run a command for each part that needs one.
120+ Never state the contents of a file, the output of a command, or the state of
121+ the repository unless a command in THIS answer returned it. What you did not
122+ read, you do not know: run the command instead of recalling it.
123+
124+ SKILLS
125+ You have a second tool, `read_skill`. Its description lists the procedures
126+ available for this project — one per kind of task.
127+
128+ Any request to DO something to a Go project is a skill, not a shell command
129+ you invent. Match the request against that list, call `read_skill` FIRST,
130+ before any bash command, and then follow what it says step by step.
131+
132+ FILE EDITING
133+ You have three tools for files: `read_file`, `edit_file` and `write_file`.
134+ They are how a file gets read and changed here: each change is exact,
135+ checked before it is written, and comes back as a diff with line numbers.
136+ bash is for running things — building, testing, listing, searching.
137+
138+ - Read before you write: call `read_file` on the file (numbered=true when
139+ you need line numbers). You cannot target text you have not seen; never
140+ rely on what you think you remember about a file.
141+ - To change an existing file, call `edit_file` with one or more {old, new}
142+ pairs. `old` is copied from the file character for character — same
143+ spaces, same indentation, same line breaks — and appears exactly once:
144+ add the surrounding lines until it is unique. Several pairs are applied
145+ together, against the original file. An empty `new` deletes the text.
146+ - Call `write_file` only to create a file, or to rewrite one entirely and
147+ on purpose. On an existing file it replaces everything, including what
148+ you did not intend to touch.
149+ - Read the diff the tool returns: it says exactly what changed and on which
150+ line. If `edit_file` refuses — text not found, ambiguous, overlapping
151+ edits — read the file again and fix `old`. Do not fall back to
152+ `write_file` to force the change through.
153+ - After editing code, run the narrowest check with bash: the formatter, the
154+ compiler, or the test covering that file.
155+
156+ RULES
157+ - Keep everything the file already does, unless the user asked to remove it.
158+ - Touch only the files the request is about. Do not add tests, files or
159+ features that were not asked for.
160+ - Never run a git command unless the user says git, commit or push.
161+ - Never move, rename or delete a file unless the user asked for it.
162+ - Then answer in English, in a few lines.
163+ - If you don't know how to use a <cli>, run `<cli> --help` (or `<cli> help`)
164+ to understand the options, then run the command.
165+
166+ BACKGROUND JOBS
167+ Never let a command block the answer. Anything that serves, watches or runs
168+ long goes to the background, with BOTH streams redirected and its pid kept:
169+
170+ nohup <command> > /tmp/<job>.log 2>&1 & echo $! > /tmp/<job>.pid
171+
172+ Redirecting only stdout still blocks until the process exits. Read the
173+ `bg-jobs` skill before you wait on, inspect or stop such a job — each has a
174+ rule you cannot guess. Stop every job you started before you finish, and say
175+ which ones you left running.
176+
177+# Generation settings (OpenAI API keys). The model's GGUF recommends
178+# temperature 0.6, top_p 0.95, top_k 20 (mm init reads them from Kronk).
179+# The model thinks by default; the compaction summary turns thinking off by
180+# itself (chat_template_kwargs, which Kronk obeys).
181+sampling:
182+ temperature: 0.6
183+ top_p: 0.95
184+ top_k: 20
185+ parallel_tool_calls: false
186+ max_tokens: 16384
187+
188+# Silence allowed between two tokens once the answer has started.
189+watchdogTimeout: 30s
190+
191+# Silence allowed while the model reads the prompt, before the first token.
192+# Kronk sends no progress while it reads (no "Reading the prompt" line), so
193+# this one wait has to cover the whole prompt — and the model's loading on the
194+# first question.
195+firstTokenTimeout: 3m
196+
197+# Context compression.
198+# ON here: the agent learns the window from Kronk, so `threshold` has a number to apply to.
199+context:
200+ enabled: true
201+
202+ # Compress when the history reaches this share of the window.
203+ threshold: 75
204+
205+ # Fallback on the message count, for when the window is unknown.
206+ # One command costs 2 messages (call + response), so 80 is roughly 30 commands of history.
207+ maxMessages: 80
208+
209+ # How many recent questions (with their commands and answers) are kept as-is instead of being summarised.
210+ # The model works on the last few turns, so summarising them would make it redo what it just did.
211+ keepLastTurns: 3
212+
213+ # max_tokens of the summary request. Reasoning is turned off for it on this
214+ # provider (reasoning_effort: "none"), or a thinking model like Qwen3.5 would
215+ # spend the budget thinking and write nothing.
216+ summaryMaxTokens: 1200
217+
218+ showStats: true
new file mode 100644
@@ -0,0 +1,218 @@
1+# Agent settings — mini-me served by Kronk (https://www.kronkai.com), the Go
2+# inference server of Ardan Labs, through its OpenAI-compatible endpoint
3+# (/v1/chat/completions).
4+#
5+# brew install ardanlabs/kronk/kronk
6+# kronk model pull mradermacher/Qwopus3.5-4B-Coder.Q8_0
7+# kronk server start # 127.0.0.1:11435 — loopback only
8+#
9+# Kronk loads an installed model on the first request. A model that is not
10+# installed is a "model not found" error: pull it first, or set autoPull: true
11+# and mm pulls it at start-up.
12+provider: kronk
13+
14+# The model id exactly as Kronk lists it on GET /v1/models:
15+# provider/modelID.
16+model: mradermacher/Qwopus3.5-4B-Coder.Q8_0
17+
18+# `kronk server start`'s default; the OpenAI routes sit under /v1.
19+baseUrl: http://127.0.0.1:11435/v1
20+
21+# From a Docker sandbox, Kronk runs on the host. It answers there only when
22+# started with `kronk server start --api-host 0.0.0.0:11435` (and the sandbox
23+# policy allows localhost:11435).
24+fallback: http://host.docker.internal:11435/v1
25+
26+# Kronk's default `open` mode checks no key. In its `authenticated` or
27+# `full-protected` mode, export a token (never put it here):
28+# export KRONK_TOKEN=$(cat ~/.kronk/keys/master.jwt)
29+# apiKeyEnv: KRONK_TOKEN
30+
31+# Yolo mode. `true` (default): the tools run without asking.
32+# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change
33+# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the
34+# terminal, the window and the editor; MCP tools ask too.
35+# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs),
36+# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false.
37+yolo: true
38+
39+# The `bash` tool.
40+# `false` removes it: the model can still read and edit files (editTools) and load skills,
41+# but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool".
42+bashTool: true
43+
44+# The built-in file tools: read_file, write_file, edit_file.
45+editTools: true
46+
47+# The built-in plan tool for managing multi-step plans.
48+# Set to false to disable plan creation and tracking via the `plan` tool.
49+planTool: true
50+
51+# Language servers for the `lsp` tool (definition, references, hover, symbols, diagnostics, rename).
52+# The file extension picks the server. A server missing from the PATH is skipped with a warning.
53+# No server listed = no `lsp` tool. Examples (uncomment what you have installed):
54+# lsp:
55+# servers:
56+# - name: gopls
57+# command: gopls
58+# extensions: [".go"]
59+# - name: typescript
60+# command: typescript-language-server
61+# args: ["--stdio"]
62+# extensions: [".ts", ".tsx", ".js", ".jsx"]
63+# - name: python
64+# command: pyright-langserver
65+# args: ["--stdio"]
66+# extensions: [".py"]
67+# - name: rust
68+# command: rust-analyzer
69+# extensions: [".rs"]
70+
71+# How many tokens the server actually serves.
72+# 0 = ask Kronk: GET /v1/kronk/models/<id> gives the window the model is
73+# configured with (`context-window` in ~/.kronk/models/model_config.yaml),
74+# loaded or not, so it is known from the start.
75+contextWindow: 0
76+
77+# Max number of characters a tool returns to the model.
78+# Context safeguard: beyond that, the output is truncated (beginning + end kept).
79+maxOutput: 16000
80+
81+# Max number of model <-> tools round trips for a single question.
82+# Raise it for tasks that need long chains of commands.
83+maxTurns: 40
84+
85+# Where the `read_skill` tool looks for its markdown procedures.
86+# The path is relative to the CURRENT DIRECTORY: run the agent from demos/kronk/.
87+skillsDir: ../../skills
88+
89+# Persistent sessions, per project.
90+# When enabled, every conversation is saved as one JSON file under `dir`.
91+# `/sessions` lists them, `/resume` continues one; in an editor, "Reload Agent" replays the thread.
92+sessions:
93+ enabled: true
94+ dir: .mm/sessions
95+
96+# How many lines of a command's output are echoed to the terminal.
97+# 0 disables it.
98+previewLines: 20
99+
100+displayCommands: true
101+# Show what a thinking model reasons about before it answers (dim italic in the
102+# terminal, its own style in the window, a thought chunk in an editor).
103+# The reasoning is always READ — it is what keeps the watchdog alive while the
104+# model thinks — this only says whether to show it.
105+showThinking: true
106+
107+
108+# The system prompt: what the agent is, and what it is allowed to do.
109+# This is the knob to play with — it is the shortest path to changing behaviour.
110+system: |
111+ Your name is Kronk-Bob.
112+ You are a coding agent working in a terminal.
113+ You have a "bash" tool to run shell commands.
114+ Use it to explore files, run tests, inspect the repository, etc.
115+ Chain several commands if needed, then answer clearly in English.
116+
117+ A request often mixes things you answer from yourself ("say hello") with
118+ things only a command can answer ("list the files"). Handle every part, in
119+ the order asked, and run a command for each part that needs one.
120+ Never state the contents of a file, the output of a command, or the state of
121+ the repository unless a command in THIS answer returned it. What you did not
122+ read, you do not know: run the command instead of recalling it.
123+
124+ SKILLS
125+ You have a second tool, `read_skill`. Its description lists the procedures
126+ available for this project — one per kind of task.
127+
128+ Any request to DO something to a Go project is a skill, not a shell command
129+ you invent. Match the request against that list, call `read_skill` FIRST,
130+ before any bash command, and then follow what it says step by step.
131+
132+ FILE EDITING
133+ You have three tools for files: `read_file`, `edit_file` and `write_file`.
134+ They are how a file gets read and changed here: each change is exact,
135+ checked before it is written, and comes back as a diff with line numbers.
136+ bash is for running things — building, testing, listing, searching.
137+
138+ - Read before you write: call `read_file` on the file (numbered=true when
139+ you need line numbers). You cannot target text you have not seen; never
140+ rely on what you think you remember about a file.
141+ - To change an existing file, call `edit_file` with one or more {old, new}
142+ pairs. `old` is copied from the file character for character — same
143+ spaces, same indentation, same line breaks — and appears exactly once:
144+ add the surrounding lines until it is unique. Several pairs are applied
145+ together, against the original file. An empty `new` deletes the text.
146+ - Call `write_file` only to create a file, or to rewrite one entirely and
147+ on purpose. On an existing file it replaces everything, including what
148+ you did not intend to touch.
149+ - Read the diff the tool returns: it says exactly what changed and on which
150+ line. If `edit_file` refuses — text not found, ambiguous, overlapping
151+ edits — read the file again and fix `old`. Do not fall back to
152+ `write_file` to force the change through.
153+ - After editing code, run the narrowest check with bash: the formatter, the
154+ compiler, or the test covering that file.
155+
156+ RULES
157+ - Keep everything the file already does, unless the user asked to remove it.
158+ - Touch only the files the request is about. Do not add tests, files or
159+ features that were not asked for.
160+ - Never run a git command unless the user says git, commit or push.
161+ - Never move, rename or delete a file unless the user asked for it.
162+ - Then answer in English, in a few lines.
163+ - If you don't know how to use a <cli>, run `<cli> --help` (or `<cli> help`)
164+ to understand the options, then run the command.
165+
166+ BACKGROUND JOBS
167+ Never let a command block the answer. Anything that serves, watches or runs
168+ long goes to the background, with BOTH streams redirected and its pid kept:
169+
170+ nohup <command> > /tmp/<job>.log 2>&1 & echo $! > /tmp/<job>.pid
171+
172+ Redirecting only stdout still blocks until the process exits. Read the
173+ `bg-jobs` skill before you wait on, inspect or stop such a job — each has a
174+ rule you cannot guess. Stop every job you started before you finish, and say
175+ which ones you left running.
176+
177+# Generation settings (OpenAI API keys). The model's GGUF recommends
178+# temperature 0.6, top_p 0.95, top_k 20 (mm init reads them from Kronk).
179+# The model thinks by default; the compaction summary turns thinking off by
180+# itself (chat_template_kwargs, which Kronk obeys).
181+sampling:
182+ temperature: 0.6
183+ top_p: 0.95
184+ top_k: 20
185+ parallel_tool_calls: false
186+ max_tokens: 16384
187+
188+# Silence allowed between two tokens once the answer has started.
189+watchdogTimeout: 30s
190+
191+# Silence allowed while the model reads the prompt, before the first token.
192+# Kronk sends no progress while it reads (no "Reading the prompt" line), so
193+# this one wait has to cover the whole prompt — and the model's loading on the
194+# first question.
195+firstTokenTimeout: 3m
196+
197+# Context compression.
198+# ON here: the agent learns the window from Kronk, so `threshold` has a number to apply to.
199+context:
200+ enabled: true
201+
202+ # Compress when the history reaches this share of the window.
203+ threshold: 75
204+
205+ # Fallback on the message count, for when the window is unknown.
206+ # One command costs 2 messages (call + response), so 80 is roughly 30 commands of history.
207+ maxMessages: 80
208+
209+ # How many recent questions (with their commands and answers) are kept as-is instead of being summarised.
210+ # The model works on the last few turns, so summarising them would make it redo what it just did.
211+ keepLastTurns: 3
212+
213+ # max_tokens of the summary request. Reasoning is turned off for it on this
214+ # provider (reasoning_effort: "none"), or a thinking model like Qwen3.5 would
215+ # spend the budget thinking and write nothing.
216+ summaryMaxTokens: 1200
217+
218+ showStats: true
added demos/kronk/agent.yaml +98 -0
new file mode 100644
@@ -0,0 +1,98 @@
1+# developer — a coding agent: reads and changes the project, runs commands.
2+#
3+# Written by `mm init`. Every value says why it has that value; change
4+# them as you watch the agent work:
5+# https://rickub.com/bots-garden/mini-me/blob/main/guides/agent-settings-for-small-models.md
6+#
7+# model family: qwen
8+# usage: developer
9+# machine: small — a laptop, models up to about 8B
10+# model facts: read from kronk: native window 131072 tokens; reasoning switch enable_thinking (default off)
11+#
12+# Where the numbers come from (the tag before their comment):
13+# [card] from the model card or the GGUF, not measured — re-read the new card
14+# [guide] the settings guide's row for this machine
15+# [rule] a rule of thumb, not specific to this model
16+#
17+# mm -config <this file> or mm -tui <this file>
18+
19+provider: kronk
20+# The model. Chosen for it (family qwen): sampling (temperature, top_p, top_k,
21+# max_tokens) and sampling.chat_template_kwargs (enable_thinking). Change the
22+# model and redo them together — running mm init again is the quick way.
23+model: "unsloth/Qwen3.5-2B-Q4_K_M"
24+# No baseUrl: kronk's defaults apply. For the record:
25+# baseUrl: "http://127.0.0.1:11435/v1"
26+# fallback: "http://host.docker.internal:11435/v1" # tried when baseUrl does not answer
27+
28+# 0 = mm asks the server for the window it serves (the model's native window
29+# is 131072 tokens; the server may serve less)
30+contextWindow: 0
31+# your answer: every command and file change waits for your answer
32+yolo: false
33+# usage developer
34+bashTool: true
35+editTools: true
36+planTool: true
37+askUserTool: true
38+# no skills: add a skills/ folder and set skillsDir to use some (see
39+# https://rickub.com/bots-garden/mini-me/blob/main/docs/en/how-to/add-a-skill.md)
40+skillsDir: no-skills
41+# usage developer
42+maxTurns: 40
43+# [guide] settings guide, row "Small model (≤ 8B), short tasks"
44+maxOutput: 8000
45+firstTokenTimeout: 3m
46+# [guide] aligned on bashTimeout, as the settings guide says when bash is on
47+# (section 3)
48+watchdogTimeout: 30s
49+# [guide] settings guide, row "Small model (≤ 8B), short tasks"
50+bashTimeout: 30s
51+
52+sampling:
53+ # [card] family qwen: Qwen model cards (thinking 0.6 / 0.95 / 20,
54+ # non-thinking 0.7 / 0.8 / 20); precise 0.0 measured with Qwen3.5 4B,
55+ # thinking off (demos/docs-search, memory-search, plans)
56+ temperature: 0.6
57+ top_p: 0.95
58+ top_k: 20
59+ # [rule] reasoning on: room for the thoughts plus a whole tool call
60+ max_tokens: 16384
61+ # one tool call per reply: small models run a check before the change it
62+ # checks has landed otherwise
63+ parallel_tool_calls: false
64+ chat_template_kwargs:
65+ # reasoning on: code and multi-step work; the template's default is off
66+ # (read from the chat template)
67+ enable_thinking: true
68+
69+context:
70+ # compress the history before the window is full (settings guide, every row)
71+ enabled: true
72+ # mm's default: 75 % of the window the server serves, a quarter left for the
73+ # next question and its tool outputs
74+ threshold: 75
75+
76+agentsMd:
77+ # An AGENTS.md in the project directory is added to the system prompt.
78+ enabled: true
79+
80+acp:
81+ name: developer
82+ title: "Developer (developer)"
83+
84+system: |
85+ Your name is developer.
86+ You are a coding agent working in a terminal, in the user's project.
87+ You have a bash tool, file tools (read_file, write_file, edit_file) and a
88+ plan tool.
89+ Read before you change: look at the files and run the commands that tell
90+ you how the project works. Make the smallest change that does the job,
91+ then run the tests or the build to check it.
92+ Never state the output of a command you did not run in this answer.
93+ When a task has several steps, make a plan and update it as you go.
94+
95+# Next: skills https://rickub.com/bots-garden/mini-me/blob/main/docs/en/how-to/add-a-skill.md
96+# MCP servers https://rickub.com/bots-garden/mini-me/blob/main/docs/en/tutorials/mcp-getting-started.md
97+# hooks https://rickub.com/bots-garden/mini-me/blob/main/docs/en/how-to/guard-the-agent-with-hooks.md
98+# sessions https://rickub.com/bots-garden/mini-me/blob/main/docs/en/how-to/keep-sessions.md
new file mode 100644
@@ -0,0 +1,98 @@
1+# developer — a coding agent: reads and changes the project, runs commands.
2+#
3+# Written by `mm init`. Every value says why it has that value; change
4+# them as you watch the agent work:
5+# https://rickub.com/bots-garden/mini-me/blob/main/guides/agent-settings-for-small-models.md
6+#
7+# model family: qwen
8+# usage: developer
9+# machine: small — a laptop, models up to about 8B
10+# model facts: read from kronk: native window 131072 tokens; reasoning switch enable_thinking (default off)
11+#
12+# Where the numbers come from (the tag before their comment):
13+# [card] from the model card or the GGUF, not measured — re-read the new card
14+# [guide] the settings guide's row for this machine
15+# [rule] a rule of thumb, not specific to this model
16+#
17+# mm -config <this file> or mm -tui <this file>
18+
19+provider: kronk
20+# The model. Chosen for it (family qwen): sampling (temperature, top_p, top_k,
21+# max_tokens) and sampling.chat_template_kwargs (enable_thinking). Change the
22+# model and redo them together — running mm init again is the quick way.
23+model: "unsloth/Qwen3.5-2B-Q4_K_M"
24+# No baseUrl: kronk's defaults apply. For the record:
25+# baseUrl: "http://127.0.0.1:11435/v1"
26+# fallback: "http://host.docker.internal:11435/v1" # tried when baseUrl does not answer
27+
28+# 0 = mm asks the server for the window it serves (the model's native window
29+# is 131072 tokens; the server may serve less)
30+contextWindow: 0
31+# your answer: every command and file change waits for your answer
32+yolo: false
33+# usage developer
34+bashTool: true
35+editTools: true
36+planTool: true
37+askUserTool: true
38+# no skills: add a skills/ folder and set skillsDir to use some (see
39+# https://rickub.com/bots-garden/mini-me/blob/main/docs/en/how-to/add-a-skill.md)
40+skillsDir: no-skills
41+# usage developer
42+maxTurns: 40
43+# [guide] settings guide, row "Small model (≤ 8B), short tasks"
44+maxOutput: 8000
45+firstTokenTimeout: 3m
46+# [guide] aligned on bashTimeout, as the settings guide says when bash is on
47+# (section 3)
48+watchdogTimeout: 30s
49+# [guide] settings guide, row "Small model (≤ 8B), short tasks"
50+bashTimeout: 30s
51+
52+sampling:
53+ # [card] family qwen: Qwen model cards (thinking 0.6 / 0.95 / 20,
54+ # non-thinking 0.7 / 0.8 / 20); precise 0.0 measured with Qwen3.5 4B,
55+ # thinking off (demos/docs-search, memory-search, plans)
56+ temperature: 0.6
57+ top_p: 0.95
58+ top_k: 20
59+ # [rule] reasoning on: room for the thoughts plus a whole tool call
60+ max_tokens: 16384
61+ # one tool call per reply: small models run a check before the change it
62+ # checks has landed otherwise
63+ parallel_tool_calls: false
64+ chat_template_kwargs:
65+ # reasoning on: code and multi-step work; the template's default is off
66+ # (read from the chat template)
67+ enable_thinking: true
68+
69+context:
70+ # compress the history before the window is full (settings guide, every row)
71+ enabled: true
72+ # mm's default: 75 % of the window the server serves, a quarter left for the
73+ # next question and its tool outputs
74+ threshold: 75
75+
76+agentsMd:
77+ # An AGENTS.md in the project directory is added to the system prompt.
78+ enabled: true
79+
80+acp:
81+ name: developer
82+ title: "Developer (developer)"
83+
84+system: |
85+ Your name is developer.
86+ You are a coding agent working in a terminal, in the user's project.
87+ You have a bash tool, file tools (read_file, write_file, edit_file) and a
88+ plan tool.
89+ Read before you change: look at the files and run the commands that tell
90+ you how the project works. Make the smallest change that does the job,
91+ then run the tests or the build to check it.
92+ Never state the output of a command you did not run in this answer.
93+ When a task has several steps, make a plan and update it as you go.
94+
95+# Next: skills https://rickub.com/bots-garden/mini-me/blob/main/docs/en/how-to/add-a-skill.md
96+# MCP servers https://rickub.com/bots-garden/mini-me/blob/main/docs/en/tutorials/mcp-getting-started.md
97+# hooks https://rickub.com/bots-garden/mini-me/blob/main/docs/en/how-to/guard-the-agent-with-hooks.md
98+# sessions https://rickub.com/bots-garden/mini-me/blob/main/docs/en/how-to/keep-sessions.md
modified docs/en/README.md +2 -1
@@ -1,6 +1,6 @@
11 # mini-me — documentation (English)
22
3-`mm` is a coding agent that runs in your terminal — line by line, or in a single window with `mm -tui` — or inside an editor through the Agent Client Protocol. It talks to a local model server (Docker Model Runner, llama.cpp's `llama-server`, llmman or Ollama) and works with a handful of built-in tools: `bash`, `read_skill`, `read_file`, `write_file`, `edit_file` — plus, when turned on, `lsp` and `fetch_content`.
3+`mm` is a coding agent that runs in your terminal — line by line, or in a single window with `mm -tui` — or inside an editor through the Agent Client Protocol. It talks to a local model server (Docker Model Runner, llama.cpp's `llama-server`, llmman, Ollama or Kronk) and works with a handful of built-in tools: `bash`, `read_skill`, `read_file`, `write_file`, `edit_file` — plus, when turned on, `lsp` and `fetch_content`.
44
55 This documentation is organised along the four [Diátaxis](https://diataxis.fr) quadrants. Pick the one that matches what you need right now.
66
@@ -15,6 +15,7 @@ This documentation is organised along the four [Diátaxis](https://diataxis.fr)
1515 - [Run mini-me on llama.cpp instead of Docker Model Runner](how-to/run-with-llama-cpp.md)
1616 - [Run mini-me on llmman](how-to/run-with-llmman.md) — `provider: llmman`, the demo config in `demos/llmman/`
1717 - [Run mini-me on Ollama](how-to/run-with-ollama.md) — `provider: ollama`, the demo config in `demos/ollama/`
18+- [Run mini-me on Kronk](how-to/run-with-kronk.md) — `provider: kronk`, the demo config in `demos/kronk/`
1819 - [Use any OpenAI-compatible server](how-to/run-with-an-openai-compatible-server.md) — `provider: openai`: the OpenAI API, vLLM, LM Studio, a gateway
1920 - [Ask another mm agent from your session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<name>`, `/ask-with-context-<name>`
2021 - [See the JSON sent to the model and its answer](how-to/inspect-model-traffic.md) — `trace`, `AGENT_TRACE`, reading the files with jq
@@ -1,6 +1,6 @@
1 # mini-me — documentation (English)1 # mini-me — documentation (English)
2 2
3-`mm` is a coding agent that runs in your terminal — line by line, or in a single window with `mm -tui` — or inside an editor through the Agent Client Protocol. It talks to a local model server (Docker Model Runner, llama.cpp's `llama-server`, llmman or Ollama) and works with a handful of built-in tools: `bash`, `read_skill`, `read_file`, `write_file`, `edit_file` — plus, when turned on, `lsp` and `fetch_content`.3+`mm` is a coding agent that runs in your terminal — line by line, or in a single window with `mm -tui` — or inside an editor through the Agent Client Protocol. It talks to a local model server (Docker Model Runner, llama.cpp's `llama-server`, llmman, Ollama or Kronk) and works with a handful of built-in tools: `bash`, `read_skill`, `read_file`, `write_file`, `edit_file` — plus, when turned on, `lsp` and `fetch_content`.
4 4
5 This documentation is organised along the four [Diátaxis](https://diataxis.fr) quadrants. Pick the one that matches what you need right now.5 This documentation is organised along the four [Diátaxis](https://diataxis.fr) quadrants. Pick the one that matches what you need right now.
6 6
@@ -15,6 +15,7 @@ This documentation is organised along the four [Diátaxis](https://diataxis.fr)
15 - [Run mini-me on llama.cpp instead of Docker Model Runner](how-to/run-with-llama-cpp.md)15 - [Run mini-me on llama.cpp instead of Docker Model Runner](how-to/run-with-llama-cpp.md)
16 - [Run mini-me on llmman](how-to/run-with-llmman.md) — `provider: llmman`, the demo config in `demos/llmman/`16 - [Run mini-me on llmman](how-to/run-with-llmman.md) — `provider: llmman`, the demo config in `demos/llmman/`
17 - [Run mini-me on Ollama](how-to/run-with-ollama.md) — `provider: ollama`, the demo config in `demos/ollama/`17 - [Run mini-me on Ollama](how-to/run-with-ollama.md) — `provider: ollama`, the demo config in `demos/ollama/`
18+- [Run mini-me on Kronk](how-to/run-with-kronk.md) — `provider: kronk`, the demo config in `demos/kronk/`
18 - [Use any OpenAI-compatible server](how-to/run-with-an-openai-compatible-server.md) — `provider: openai`: the OpenAI API, vLLM, LM Studio, a gateway19 - [Use any OpenAI-compatible server](how-to/run-with-an-openai-compatible-server.md) — `provider: openai`: the OpenAI API, vLLM, LM Studio, a gateway
19 - [Ask another mm agent from your session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<name>`, `/ask-with-context-<name>`20 - [Ask another mm agent from your session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<name>`, `/ask-with-context-<name>`
20 - [See the JSON sent to the model and its answer](how-to/inspect-model-traffic.md) — `trace`, `AGENT_TRACE`, reading the files with jq21 - [See the JSON sent to the model and its answer](how-to/inspect-model-traffic.md) — `trace`, `AGENT_TRACE`, reading the files with jq
modified docs/en/explanation/mm-init.md +2 -1
@@ -16,6 +16,7 @@ The server knows the exact model that is loaded, so it is the most specific sour
1616 |---|---|---|
1717 | `llmman`, `ollama` | `/api/tags`, then `/api/show` | the model list, the family the server reports (`details.family`), the native context window, the sampling written in the GGUF (`temperature`, `top_p`, `top_k`, `min_p`), the chat template |
1818 | `llamacpp` | `/props` | the window and the chat template |
19+| `kronk` | `/models`, then `/v1/kronk/models/<id>` | the model list, the architecture, the window the model is configured with (the trained one when none), the sampling written in the GGUF, the chat template; for a profile, its own sampling, which wins over every other source: `mm` sends its sampling with every request and a request's value replaces a profile's, so the profile's values would otherwise never apply |
1920 | `dmr` | `/models` | the model list only |
2021 | `openai` | `/models` (with the key) | the model list only |
2122
@@ -106,7 +107,7 @@ The wizard overwrites an existing file only if you agree, and never with `--yes`
106107
107108 ## Known limits
108109
109-- Live checks were run against llmman only (Qwen3.5 4B, gemma-4-E4B). Ollama, Docker Model Runner and llama-server were tested against fake servers recorded in the tests. The OpenAI API itself was not tried; its constraints come from its reference.
110+- Live checks were run against llmman (Qwen3.5 4B, gemma-4-E4B) and Kronk 1.33.2 (`mm init --provider kronk … --check`, Qwopus3.5 4B Coder, 2026-11-11). Ollama, Docker Model Runner and llama-server were tested against fake servers recorded in the tests. The OpenAI API itself was not tried; its constraints come from its reference.
110111 - A `[card]` number was copied, not measured in this repository.
111112 - The families are matched on words in the model id: a model renamed by its publisher may land in `other`.
112113
@@ -16,6 +16,7 @@ The server knows the exact model that is loaded, so it is the most specific sour
16 |---|---|---|16 |---|---|---|
17 | `llmman`, `ollama` | `/api/tags`, then `/api/show` | the model list, the family the server reports (`details.family`), the native context window, the sampling written in the GGUF (`temperature`, `top_p`, `top_k`, `min_p`), the chat template |17 | `llmman`, `ollama` | `/api/tags`, then `/api/show` | the model list, the family the server reports (`details.family`), the native context window, the sampling written in the GGUF (`temperature`, `top_p`, `top_k`, `min_p`), the chat template |
18 | `llamacpp` | `/props` | the window and the chat template |18 | `llamacpp` | `/props` | the window and the chat template |
19+| `kronk` | `/models`, then `/v1/kronk/models/<id>` | the model list, the architecture, the window the model is configured with (the trained one when none), the sampling written in the GGUF, the chat template; for a profile, its own sampling, which wins over every other source: `mm` sends its sampling with every request and a request's value replaces a profile's, so the profile's values would otherwise never apply |
19 | `dmr` | `/models` | the model list only |20 | `dmr` | `/models` | the model list only |
20 | `openai` | `/models` (with the key) | the model list only |21 | `openai` | `/models` (with the key) | the model list only |
21 22
@@ -106,7 +107,7 @@ The wizard overwrites an existing file only if you agree, and never with `--yes`
106 107
107 ## Known limits108 ## Known limits
108 109
109-- Live checks were run against llmman only (Qwen3.5 4B, gemma-4-E4B). Ollama, Docker Model Runner and llama-server were tested against fake servers recorded in the tests. The OpenAI API itself was not tried; its constraints come from its reference.110+- Live checks were run against llmman (Qwen3.5 4B, gemma-4-E4B) and Kronk 1.33.2 (`mm init --provider kronk … --check`, Qwopus3.5 4B Coder, 2026-11-11). Ollama, Docker Model Runner and llama-server were tested against fake servers recorded in the tests. The OpenAI API itself was not tried; its constraints come from its reference.
110 - A `[card]` number was copied, not measured in this repository.111 - A `[card]` number was copied, not measured in this repository.
111 - The families are matched on words in the model id: a model renamed by its publisher may land in `other`.112 - The families are matched on words in the model id: a model renamed by its publisher may land in `other`.
112 113
modified docs/en/explanation/providers.md +4 -2
@@ -2,7 +2,7 @@
22
33 ## What is this about?
44
5-`mm` talks to a local model server through the OpenAI chat-completions API. Four servers are supported out of the box: Docker Model Runner, llama.cpp's `llama-server`, llmman and Ollama, under the names `dmr`, `llamacpp`, `llmman` and `ollama`. A fifth entry, `openai` (ticket 118), takes any other server that speaks the protocol — the OpenAI API by default, vLLM, LM Studio, a gateway — and assumes nothing beyond it: it is the same data-only entry with every server-specific field left empty, and a probe that sends the key. The engine package used to be tied to Docker Model Runner; the provider abstraction is what generalised it.
5+`mm` talks to a local model server through the OpenAI chat-completions API. Five servers are supported out of the box: Docker Model Runner, llama.cpp's `llama-server`, llmman, Ollama and Kronk, under the names `dmr`, `llamacpp`, `llmman`, `ollama` and `kronk`. Another entry, `openai` (ticket 118), takes any other server that speaks the protocol — the OpenAI API by default, vLLM, LM Studio, a gateway — and assumes nothing beyond it: it is the same data-only entry with every server-specific field left empty, and a probe that sends the key. The engine package used to be tied to Docker Model Runner; the provider abstraction is what generalised it.
66
77 ## Why it is designed this way
88
@@ -20,6 +20,8 @@
2020
2121 **Ollama is a fourth entry, and shows why each server keeps its own.** Ollama's `/v1` looks like the others, but measured on Ollama 0.35.0 it differs in three ways. Reasoning streams as `delta.reasoning`, which the tap already reads. No prompt progress comes back, so `return_progress` is not sent. And `chat_template_kwargs` is ignored: the summary of a compaction reasoned until its token limit and came back empty; `reasoning_effort: "none"` is what Ollama obeys, and with it the same compaction succeeded. That is why how to turn reasoning off is a per-provider field, not something implied by "is llama.cpp". `mm` uses it for summaries only; for every turn, the user writes the same server-specific key in `sampling`, which forwards keys the OpenAI API does not know since Genkit 1.12 (see [How to turn a model's reasoning off](../how-to/turn-reasoning-off.md)). Like llmman, Ollama reports the served window on `/api/ps` once the model is loaded (the two share that reading); unlike llmman, it does not pull a missing model, so the probe's warning says `ollama pull`.
2222
23+**Kronk is a fifth entry, with its own routes beside the OpenAI ones.** [Kronk](https://www.kronkai.com) (`kronk server start`, port 11435, ticket 75) is a Go server that embeds llama.cpp directly, without a `llama-server` process. Measured on Kronk 1.33.2, it behaves like llama.cpp where it matters to `mm`: reasoning streams as `reasoning_content`, and `chat_template_kwargs` turns it off, while `reasoning_effort: "none"` is ignored. Like Ollama, it sends no prompt progress. The window, the pull and the unload have their own routes under `/v1/kronk/…`. `GET /v1/kronk/models/<id>` reports the window the model is configured with, loaded or not, so it is known at start-up. The id contains a `/` (`provider/modelID`), so it travels escaped: a raw `/` splits the path. The pull, `POST /v1/kronk/models/pull`, answers 200 even when it fails, so only the stream's last record (`"downloaded": true`, or the reason) says how it ended. An installed model loads on the first request; a missing one is a 404, hence the probe's `kronk model pull` warning. Kronk listens on the loopback only, so the sandbox fallback works only once the host starts it with `--api-host 0.0.0.0:11435`. Kronk also lists configuration profiles (`provider/modelID/PROFILE`, the same weights with their own `model_config.yaml` settings); `mm` treats them as models, and the probe reads the profile's own window. A profile's sampling only fills in what the request leaves out, so `mm`'s own `sampling` wins: measured with `temperature` and `max_tokens`.
24+
2325 **Errors are rewritten in the server's own words.** A stack of wrapped messages is noise; "tool calls need llama-server started with --jinja" is the whole diagnosis. The provider recognises connection refusals, the missing `--jinja` flag, 401/403, 404 with a pull or alias hint, 429 and 5xx. Unknown errors come back unchanged: a raw message beats a wrong hint.
2426
2527 **A stream the server breaks is retried, and its words are kept.** llama.cpp reports a failure in the middle of a streamed answer as an SSE `error:` event (the 200 status is already sent). openai-go only reads `data:` lines, so the message used to vanish behind `unexpected end of JSON input`. The tap that reads the stream for reasoning also picks up those events, and the model call that broke is retried once — that call only: the tools that ran earlier in the turn are not replayed, and text the broken attempt already streamed is not taken back. If it breaks again, the error shows the server's own message (`the model server reported an error while generating: Failed to parse input at pos …`), or says the connection broke. Either way the history is kept: asking to continue picks the work up.
@@ -38,5 +40,5 @@
3840
3941 - The two configuration files that parametrise each provider are listed in the [configuration reference](../reference/configuration.md).
4042 - The window the probe learns is what [context compression](context-compression.md) measures against.
41-- Switching servers for one run is covered in [how to run on llama.cpp](../how-to/run-with-llama-cpp.md) [how to run on llmman](../how-to/run-with-llmman.md) and [how to run on Ollama](../how-to/run-with-ollama.md).
43+- Switching servers for one run is covered in [how to run on llama.cpp](../how-to/run-with-llama-cpp.md) [how to run on llmman](../how-to/run-with-llmman.md), [how to run on Ollama](../how-to/run-with-ollama.md) and [how to run on Kronk](../how-to/run-with-kronk.md).
4244 - Checking the tool order and measuring the cache gain on your own server: [how to check that the tools go out in a stable order](../how-to/verify-tool-order.md).
@@ -2,7 +2,7 @@
2 2
3 ## What is this about?3 ## What is this about?
4 4
5-`mm` talks to a local model server through the OpenAI chat-completions API. Four servers are supported out of the box: Docker Model Runner, llama.cpp's `llama-server`, llmman and Ollama, under the names `dmr`, `llamacpp`, `llmman` and `ollama`. A fifth entry, `openai` (ticket 118), takes any other server that speaks the protocol — the OpenAI API by default, vLLM, LM Studio, a gateway — and assumes nothing beyond it: it is the same data-only entry with every server-specific field left empty, and a probe that sends the key. The engine package used to be tied to Docker Model Runner; the provider abstraction is what generalised it.5+`mm` talks to a local model server through the OpenAI chat-completions API. Five servers are supported out of the box: Docker Model Runner, llama.cpp's `llama-server`, llmman, Ollama and Kronk, under the names `dmr`, `llamacpp`, `llmman`, `ollama` and `kronk`. Another entry, `openai` (ticket 118), takes any other server that speaks the protocol — the OpenAI API by default, vLLM, LM Studio, a gateway — and assumes nothing beyond it: it is the same data-only entry with every server-specific field left empty, and a probe that sends the key. The engine package used to be tied to Docker Model Runner; the provider abstraction is what generalised it.
6 6
7 ## Why it is designed this way7 ## Why it is designed this way
8 8
@@ -20,6 +20,8 @@
20 20
21 **Ollama is a fourth entry, and shows why each server keeps its own.** Ollama's `/v1` looks like the others, but measured on Ollama 0.35.0 it differs in three ways. Reasoning streams as `delta.reasoning`, which the tap already reads. No prompt progress comes back, so `return_progress` is not sent. And `chat_template_kwargs` is ignored: the summary of a compaction reasoned until its token limit and came back empty; `reasoning_effort: "none"` is what Ollama obeys, and with it the same compaction succeeded. That is why how to turn reasoning off is a per-provider field, not something implied by "is llama.cpp". `mm` uses it for summaries only; for every turn, the user writes the same server-specific key in `sampling`, which forwards keys the OpenAI API does not know since Genkit 1.12 (see [How to turn a model's reasoning off](../how-to/turn-reasoning-off.md)). Like llmman, Ollama reports the served window on `/api/ps` once the model is loaded (the two share that reading); unlike llmman, it does not pull a missing model, so the probe's warning says `ollama pull`.21 **Ollama is a fourth entry, and shows why each server keeps its own.** Ollama's `/v1` looks like the others, but measured on Ollama 0.35.0 it differs in three ways. Reasoning streams as `delta.reasoning`, which the tap already reads. No prompt progress comes back, so `return_progress` is not sent. And `chat_template_kwargs` is ignored: the summary of a compaction reasoned until its token limit and came back empty; `reasoning_effort: "none"` is what Ollama obeys, and with it the same compaction succeeded. That is why how to turn reasoning off is a per-provider field, not something implied by "is llama.cpp". `mm` uses it for summaries only; for every turn, the user writes the same server-specific key in `sampling`, which forwards keys the OpenAI API does not know since Genkit 1.12 (see [How to turn a model's reasoning off](../how-to/turn-reasoning-off.md)). Like llmman, Ollama reports the served window on `/api/ps` once the model is loaded (the two share that reading); unlike llmman, it does not pull a missing model, so the probe's warning says `ollama pull`.
22 22
23+**Kronk is a fifth entry, with its own routes beside the OpenAI ones.** [Kronk](https://www.kronkai.com) (`kronk server start`, port 11435, ticket 75) is a Go server that embeds llama.cpp directly, without a `llama-server` process. Measured on Kronk 1.33.2, it behaves like llama.cpp where it matters to `mm`: reasoning streams as `reasoning_content`, and `chat_template_kwargs` turns it off, while `reasoning_effort: "none"` is ignored. Like Ollama, it sends no prompt progress. The window, the pull and the unload have their own routes under `/v1/kronk/…`. `GET /v1/kronk/models/<id>` reports the window the model is configured with, loaded or not, so it is known at start-up. The id contains a `/` (`provider/modelID`), so it travels escaped: a raw `/` splits the path. The pull, `POST /v1/kronk/models/pull`, answers 200 even when it fails, so only the stream's last record (`"downloaded": true`, or the reason) says how it ended. An installed model loads on the first request; a missing one is a 404, hence the probe's `kronk model pull` warning. Kronk listens on the loopback only, so the sandbox fallback works only once the host starts it with `--api-host 0.0.0.0:11435`. Kronk also lists configuration profiles (`provider/modelID/PROFILE`, the same weights with their own `model_config.yaml` settings); `mm` treats them as models, and the probe reads the profile's own window. A profile's sampling only fills in what the request leaves out, so `mm`'s own `sampling` wins: measured with `temperature` and `max_tokens`.
24+
23 **Errors are rewritten in the server's own words.** A stack of wrapped messages is noise; "tool calls need llama-server started with --jinja" is the whole diagnosis. The provider recognises connection refusals, the missing `--jinja` flag, 401/403, 404 with a pull or alias hint, 429 and 5xx. Unknown errors come back unchanged: a raw message beats a wrong hint.25 **Errors are rewritten in the server's own words.** A stack of wrapped messages is noise; "tool calls need llama-server started with --jinja" is the whole diagnosis. The provider recognises connection refusals, the missing `--jinja` flag, 401/403, 404 with a pull or alias hint, 429 and 5xx. Unknown errors come back unchanged: a raw message beats a wrong hint.
24 26
25 **A stream the server breaks is retried, and its words are kept.** llama.cpp reports a failure in the middle of a streamed answer as an SSE `error:` event (the 200 status is already sent). openai-go only reads `data:` lines, so the message used to vanish behind `unexpected end of JSON input`. The tap that reads the stream for reasoning also picks up those events, and the model call that broke is retried once — that call only: the tools that ran earlier in the turn are not replayed, and text the broken attempt already streamed is not taken back. If it breaks again, the error shows the server's own message (`the model server reported an error while generating: Failed to parse input at pos …`), or says the connection broke. Either way the history is kept: asking to continue picks the work up.27 **A stream the server breaks is retried, and its words are kept.** llama.cpp reports a failure in the middle of a streamed answer as an SSE `error:` event (the 200 status is already sent). openai-go only reads `data:` lines, so the message used to vanish behind `unexpected end of JSON input`. The tap that reads the stream for reasoning also picks up those events, and the model call that broke is retried once — that call only: the tools that ran earlier in the turn are not replayed, and text the broken attempt already streamed is not taken back. If it breaks again, the error shows the server's own message (`the model server reported an error while generating: Failed to parse input at pos …`), or says the connection broke. Either way the history is kept: asking to continue picks the work up.
@@ -38,5 +40,5 @@
38 40
39 - The two configuration files that parametrise each provider are listed in the [configuration reference](../reference/configuration.md).41 - The two configuration files that parametrise each provider are listed in the [configuration reference](../reference/configuration.md).
40 - The window the probe learns is what [context compression](context-compression.md) measures against.42 - The window the probe learns is what [context compression](context-compression.md) measures against.
41-- Switching servers for one run is covered in [how to run on llama.cpp](../how-to/run-with-llama-cpp.md) [how to run on llmman](../how-to/run-with-llmman.md) and [how to run on Ollama](../how-to/run-with-ollama.md).43+- Switching servers for one run is covered in [how to run on llama.cpp](../how-to/run-with-llama-cpp.md) [how to run on llmman](../how-to/run-with-llmman.md), [how to run on Ollama](../how-to/run-with-ollama.md) and [how to run on Kronk](../how-to/run-with-kronk.md).
42 - Checking the tool order and measuring the cache gain on your own server: [how to check that the tools go out in a stable order](../how-to/verify-tool-order.md).44 - Checking the tool order and measuring the cache gain on your own server: [how to check that the tools go out in a stable order](../how-to/verify-tool-order.md).
modified docs/en/explanation/thinking-models.md +1 -1
@@ -48,6 +48,6 @@ Three properties hold everywhere:
4848
4949 ## What this is not
5050
51-It is not a "reasoning effort" setting: `mm` has no key of its own for it, and shows what arrives. The server can still be asked not to reason, through `sampling`, whose keys unknown to the OpenAI API are sent as they are (since Genkit 1.12): `chat_template_kwargs: {enable_thinking: false}` for a llama.cpp server (`dmr`, `llamacpp`, `llmman`), `reasoning_effort: none` for Ollama. Measured with Qwen3.5 4B on Docker Model Runner: the same answer in under a second, without the four seconds of reasoning. `mm` does this by itself, per server, for the summary of a compaction only. See [How to turn a model's reasoning off](../how-to/turn-reasoning-off.md).
51+It is not a "reasoning effort" setting: `mm` has no key of its own for it, and shows what arrives. The server can still be asked not to reason, through `sampling`, whose keys unknown to the OpenAI API are sent as they are (since Genkit 1.12): `chat_template_kwargs: {enable_thinking: false}` for a llama.cpp server (`dmr`, `llamacpp`, `llmman`) and for Kronk, `reasoning_effort: none` for Ollama. Measured with Qwen3.5 4B on Docker Model Runner: the same answer in under a second, without the four seconds of reasoning. `mm` does this by itself, per server, for the summary of a compaction only. See [How to turn a model's reasoning off](../how-to/turn-reasoning-off.md).
5252
5353 It is not a parser. Some models instead wrap their reasoning in `<think>` tags inside `content`. Those arrive as ordinary answer text and `mm` shows them as such — the two shapes would need different treatment, and only the field is handled today.
@@ -48,6 +48,6 @@ Three properties hold everywhere:
48 48
49 ## What this is not49 ## What this is not
50 50
51-It is not a "reasoning effort" setting: `mm` has no key of its own for it, and shows what arrives. The server can still be asked not to reason, through `sampling`, whose keys unknown to the OpenAI API are sent as they are (since Genkit 1.12): `chat_template_kwargs: {enable_thinking: false}` for a llama.cpp server (`dmr`, `llamacpp`, `llmman`), `reasoning_effort: none` for Ollama. Measured with Qwen3.5 4B on Docker Model Runner: the same answer in under a second, without the four seconds of reasoning. `mm` does this by itself, per server, for the summary of a compaction only. See [How to turn a model's reasoning off](../how-to/turn-reasoning-off.md).51+It is not a "reasoning effort" setting: `mm` has no key of its own for it, and shows what arrives. The server can still be asked not to reason, through `sampling`, whose keys unknown to the OpenAI API are sent as they are (since Genkit 1.12): `chat_template_kwargs: {enable_thinking: false}` for a llama.cpp server (`dmr`, `llamacpp`, `llmman`) and for Kronk, `reasoning_effort: none` for Ollama. Measured with Qwen3.5 4B on Docker Model Runner: the same answer in under a second, without the four seconds of reasoning. `mm` does this by itself, per server, for the summary of a compaction only. See [How to turn a model's reasoning off](../how-to/turn-reasoning-off.md).
52 52
53 It is not a parser. Some models instead wrap their reasoning in `<think>` tags inside `content`. Those arrive as ordinary answer text and `mm` shows them as such — the two shapes would need different treatment, and only the field is handled today.53 It is not a parser. Some models instead wrap their reasoning in `<think>` tags inside `content`. Those arrive as ordinary answer text and `mm` shows them as such — the two shapes would need different treatment, and only the field is handled today.
modified docs/en/how-to/run-in-a-docker-sandbox.md +1 -0
@@ -165,6 +165,7 @@ sbx env rm --force --env-arg PROJECT_DIR="$PWD"
165165 Inside a sandbox `localhost` is the sandbox itself. Your model server keeps running on your machine and is reached through `host.docker.internal`, which the sandbox proxy maps to your host.
166166
167167 - The `mini-me` kit's network policy allows **`host.docker.internal:12434`** (Docker Model Runner), **`:11434`** (Ollama) and **`:17434`** (llmman). `agent.dmr.yaml` already falls back to `http://host.docker.internal:12434/engines/v1`; the configurations shipped in `demos/use-with-sbx/` use llmman (`fallback: http://host.docker.internal:17434/v1`).
168+- Kronk (`:11435`) is not in that list: allow `localhost:11435` in the policy, and start Kronk on the host with `--api-host 0.0.0.0:11435` — it listens on the loopback only otherwise. See [How to run mini-me on Kronk](run-with-kronk.md).
168169 - For another port — llama-server's `8080`, for instance — add it to the kit's `network-policy` capability, or allow it for one sandbox:
169170
170171 ```bash
@@ -165,6 +165,7 @@ sbx env rm --force --env-arg PROJECT_DIR="$PWD"
165 Inside a sandbox `localhost` is the sandbox itself. Your model server keeps running on your machine and is reached through `host.docker.internal`, which the sandbox proxy maps to your host.165 Inside a sandbox `localhost` is the sandbox itself. Your model server keeps running on your machine and is reached through `host.docker.internal`, which the sandbox proxy maps to your host.
166 166
167 - The `mini-me` kit's network policy allows **`host.docker.internal:12434`** (Docker Model Runner), **`:11434`** (Ollama) and **`:17434`** (llmman). `agent.dmr.yaml` already falls back to `http://host.docker.internal:12434/engines/v1`; the configurations shipped in `demos/use-with-sbx/` use llmman (`fallback: http://host.docker.internal:17434/v1`).167 - The `mini-me` kit's network policy allows **`host.docker.internal:12434`** (Docker Model Runner), **`:11434`** (Ollama) and **`:17434`** (llmman). `agent.dmr.yaml` already falls back to `http://host.docker.internal:12434/engines/v1`; the configurations shipped in `demos/use-with-sbx/` use llmman (`fallback: http://host.docker.internal:17434/v1`).
168+- Kronk (`:11435`) is not in that list: allow `localhost:11435` in the policy, and start Kronk on the host with `--api-host 0.0.0.0:11435` — it listens on the loopback only otherwise. See [How to run mini-me on Kronk](run-with-kronk.md).
168 - For another port — llama-server's `8080`, for instance — add it to the kit's `network-policy` capability, or allow it for one sandbox:169 - For another port — llama-server's `8080`, for instance — add it to the kit's `network-policy` capability, or allow it for one sandbox:
169 170
170 ```bash171 ```bash
added docs/en/how-to/run-with-kronk.md +88 -0
new file mode 100644
@@ -0,0 +1,88 @@
1+# How to run mini-me on Kronk
2+
3+This guide shows how to point `mm` at [Kronk](https://www.kronkai.com), the Go inference server of Ardan Labs, through its OpenAI-compatible endpoint. It assumes Kronk is installed (`brew install ardanlabs/kronk/kronk`, or `go install github.com/ardanlabs/kronk/cmd/kronk@latest`) and that you know how to build `mm`.
4+
5+## Steps
6+
7+1. Pull the model. Kronk loads an installed model on the first request, but a model that is not installed is a `model not found` error:
8+
9+ ```bash
10+ kronk model pull mradermacher/Qwopus3.5-4B-Coder.Q8_0
11+ ```
12+
13+2. Start the server. It listens on `127.0.0.1:11435`:
14+
15+ ```bash
16+ kronk server start
17+ ```
18+
19+3. Run the demo configuration from its directory (its `skillsDir` is `../../skills`):
20+
21+ ```bash
22+ cd demos/kronk
23+ ../../mm -config agent.kronk.yaml
24+ ```
25+
26+4. Check the banner. `provider: kronk` confirms the provider, and `ctx: 131072 (/v1/kronk/models)` is the window Kronk serves the model with, read at start-up whether the model is loaded or not.
27+
28+## Variants
29+
30+- **Another model.** Set `model:` to the id exactly as Kronk lists it on `GET /v1/models`: `provider/modelID`. An id without its provider is refused by Kronk with `must use provider/modelID`. **The id you pull with is not always the id Kronk serves.** For example, Qwen3.5 2B, a smaller model:
31+
32+ ```bash
33+ kronk model pull unsloth/Qwen3.5-2B-GGUF:Q4_K_M --local # --local: with the server stopped
34+ curl -s localhost:11435/v1/models # lists it as unsloth/Qwen3.5-2B-Q4_K_M
35+ ```
36+
37+ ```yaml
38+ model: unsloth/Qwen3.5-2B-Q4_K_M # not unsloth/Qwen3.5-2B-GGUF:Q4_K_M, which is a 404
39+ ```
40+
41+ Measured on Kronk 1.33.2: `ctx: 131072 (/v1/kronk/models)`, and tool calls work.
42+- **A Kronk profile.** Kronk also lists configuration profiles, `provider/modelID/PROFILE`, for example `mradermacher/Qwopus3.5-4B-Coder.Q8_0/AGENT`. A profile is the same GGUF file, served with the settings of that key in `~/.kronk/models/model_config.yaml`. Nothing is downloaded twice. The `AGENT` profiles come with Kronk's default `model_config.yaml`; you can add your own there, then restart Kronk. The one for Qwopus reads:
43+
44+ ```yaml
45+ mradermacher/Qwopus3.5-4B-Coder.Q8_0/AGENT:
46+ context-window: 65536
47+ sampling-parameters:
48+ max_tokens: 8192
49+ temperature: 1
50+ top_k: 20
51+ top_p: 0.95
52+ ```
53+
54+ A profile is a valid `model:`. What it changes for `mm`:
55+
56+ - **The window: yes.** It is the profile's own, here 65536 instead of the 131072 Kronk picks for the plain model. The banner shows it (`ctx: 65536 (/v1/kronk/models)`), and compaction triggers at 75 % of it. A smaller window also takes less memory on the server.
57+ - **Sampling: only for the keys `mm` does not send.** A value in the request wins over the profile's: measured on Kronk 1.33.2, `temperature: 0` in the request gave the same answer three times in a row under the profile's `temperature: 1`, and `max_tokens: 5` cut the answer at 5 tokens under the profile's 8192. `mm` always sends `temperature`, `top_p` and `max_tokens`: its defaults, merged with the file's `sampling` (see [`sampling`](../reference/configuration.md)). So the profile's `temperature`, `top_p` and `max_tokens` never apply, and its other keys apply only when `sampling` leaves them out, here `top_k: 20`. This holds even for a file with no `sampling:` block: `mm` then sends `temperature: 0`, `top_p: 0.9`, `max_tokens: 4096` (checked in a trace). To get a profile's values, write them in `sampling`; `mm init` does it for you (below).
58+ - **Reasoning: no.** `enable_thinking` and `chat_template_kwargs` in `sampling` are per request too.
59+
60+ `mm init` lists profiles with a line that says what they are, and the window shows them as `Qwopus3.5-4B-Coder.Q8…/AGENT`. When you pick a profile, `mm init` reads its sampling from Kronk and copies it into the file's `sampling`. The copied values are `temperature`, `top_p`, `top_k`, `min_p`, and `max_tokens` when the profile caps it. They replace the family table's and the GGUF's values, each marked `[server] Kronk profile AGENT`:
61+
62+ ```yaml
63+ sampling:
64+ # [server] Kronk profile AGENT (model_config.yaml), read from the server:
65+ # mm's values replace a profile's, so they are copied here
66+ temperature: 1.0
67+ top_p: 0.95
68+ top_k: 20
69+ min_p: 0.0
70+ max_tokens: 8192
71+ ```
72+- **Write the file with the wizard.** `mm init --provider kronk` lists Kronk's models and reads the chosen one's window, sampling and chat template from Kronk. See [How to create an agent](create-an-agent.md).
73+- **Let mm pull the model.** With `autoPull: true`, mm pulls a model Kronk does not list at start-up, through `POST /v1/kronk/models/pull`, and shows the progress. See [`autoPull`](../reference/configuration.md).
74+- **From a Docker sandbox.** `kronk server start` binds the loopback only, which a sandbox cannot reach. On the host, start it with `kronk server start --api-host 0.0.0.0:11435`, and allow `localhost:11435` in the sandbox's network policy. With no `baseUrl` answering, `mm` then falls back to `http://host.docker.internal:11435/v1` by itself.
75+- **A larger or smaller window.** The window is Kronk's, per model: set `context-window` for the model in `~/.kronk/models/model_config.yaml` and restart Kronk. `mm` reads the new value at start-up.
76+- **A protected Kronk.** In the `authenticated` or `full-protected` modes, export a token in `KRONK_TOKEN` (or name another variable under `apiKeyEnv`). A refused token is a start-up warning naming the variable.
77+- **Free the memory.** `/unload` and the `unload_model` tool unload the model through `POST /v1/kronk/models/unload`. See [How to free a model's memory](unload-a-model.md).
78+- **Keep your own `agent.yaml`.** `AGENT_PROVIDER=kronk ./mm` works as long as the file leaves `baseUrl` empty.
79+
80+## What differs from the other servers
81+
82+- No `Reading the prompt …` line: Kronk sends no progress while it reads, so `firstTokenTimeout` has to cover the whole prompt and the model's loading.
83+- Reasoning is turned off with `chat_template_kwargs: {enable_thinking: false}`, as on llama.cpp; Kronk ignores `reasoning_effort: none`. The summary of a compaction does it by itself. See [How to turn a model's reasoning off](turn-reasoning-off.md).
84+
85+## See also
86+
87+- [Providers: one implementation per protocol](../explanation/providers.md) — what was measured on Kronk.
88+- [Configuration reference](../reference/configuration.md) — provider defaults.
new file mode 100644
@@ -0,0 +1,88 @@
1+# How to run mini-me on Kronk
2+
3+This guide shows how to point `mm` at [Kronk](https://www.kronkai.com), the Go inference server of Ardan Labs, through its OpenAI-compatible endpoint. It assumes Kronk is installed (`brew install ardanlabs/kronk/kronk`, or `go install github.com/ardanlabs/kronk/cmd/kronk@latest`) and that you know how to build `mm`.
4+
5+## Steps
6+
7+1. Pull the model. Kronk loads an installed model on the first request, but a model that is not installed is a `model not found` error:
8+
9+ ```bash
10+ kronk model pull mradermacher/Qwopus3.5-4B-Coder.Q8_0
11+ ```
12+
13+2. Start the server. It listens on `127.0.0.1:11435`:
14+
15+ ```bash
16+ kronk server start
17+ ```
18+
19+3. Run the demo configuration from its directory (its `skillsDir` is `../../skills`):
20+
21+ ```bash
22+ cd demos/kronk
23+ ../../mm -config agent.kronk.yaml
24+ ```
25+
26+4. Check the banner. `provider: kronk` confirms the provider, and `ctx: 131072 (/v1/kronk/models)` is the window Kronk serves the model with, read at start-up whether the model is loaded or not.
27+
28+## Variants
29+
30+- **Another model.** Set `model:` to the id exactly as Kronk lists it on `GET /v1/models`: `provider/modelID`. An id without its provider is refused by Kronk with `must use provider/modelID`. **The id you pull with is not always the id Kronk serves.** For example, Qwen3.5 2B, a smaller model:
31+
32+ ```bash
33+ kronk model pull unsloth/Qwen3.5-2B-GGUF:Q4_K_M --local # --local: with the server stopped
34+ curl -s localhost:11435/v1/models # lists it as unsloth/Qwen3.5-2B-Q4_K_M
35+ ```
36+
37+ ```yaml
38+ model: unsloth/Qwen3.5-2B-Q4_K_M # not unsloth/Qwen3.5-2B-GGUF:Q4_K_M, which is a 404
39+ ```
40+
41+ Measured on Kronk 1.33.2: `ctx: 131072 (/v1/kronk/models)`, and tool calls work.
42+- **A Kronk profile.** Kronk also lists configuration profiles, `provider/modelID/PROFILE`, for example `mradermacher/Qwopus3.5-4B-Coder.Q8_0/AGENT`. A profile is the same GGUF file, served with the settings of that key in `~/.kronk/models/model_config.yaml`. Nothing is downloaded twice. The `AGENT` profiles come with Kronk's default `model_config.yaml`; you can add your own there, then restart Kronk. The one for Qwopus reads:
43+
44+ ```yaml
45+ mradermacher/Qwopus3.5-4B-Coder.Q8_0/AGENT:
46+ context-window: 65536
47+ sampling-parameters:
48+ max_tokens: 8192
49+ temperature: 1
50+ top_k: 20
51+ top_p: 0.95
52+ ```
53+
54+ A profile is a valid `model:`. What it changes for `mm`:
55+
56+ - **The window: yes.** It is the profile's own, here 65536 instead of the 131072 Kronk picks for the plain model. The banner shows it (`ctx: 65536 (/v1/kronk/models)`), and compaction triggers at 75 % of it. A smaller window also takes less memory on the server.
57+ - **Sampling: only for the keys `mm` does not send.** A value in the request wins over the profile's: measured on Kronk 1.33.2, `temperature: 0` in the request gave the same answer three times in a row under the profile's `temperature: 1`, and `max_tokens: 5` cut the answer at 5 tokens under the profile's 8192. `mm` always sends `temperature`, `top_p` and `max_tokens`: its defaults, merged with the file's `sampling` (see [`sampling`](../reference/configuration.md)). So the profile's `temperature`, `top_p` and `max_tokens` never apply, and its other keys apply only when `sampling` leaves them out, here `top_k: 20`. This holds even for a file with no `sampling:` block: `mm` then sends `temperature: 0`, `top_p: 0.9`, `max_tokens: 4096` (checked in a trace). To get a profile's values, write them in `sampling`; `mm init` does it for you (below).
58+ - **Reasoning: no.** `enable_thinking` and `chat_template_kwargs` in `sampling` are per request too.
59+
60+ `mm init` lists profiles with a line that says what they are, and the window shows them as `Qwopus3.5-4B-Coder.Q8…/AGENT`. When you pick a profile, `mm init` reads its sampling from Kronk and copies it into the file's `sampling`. The copied values are `temperature`, `top_p`, `top_k`, `min_p`, and `max_tokens` when the profile caps it. They replace the family table's and the GGUF's values, each marked `[server] Kronk profile AGENT`:
61+
62+ ```yaml
63+ sampling:
64+ # [server] Kronk profile AGENT (model_config.yaml), read from the server:
65+ # mm's values replace a profile's, so they are copied here
66+ temperature: 1.0
67+ top_p: 0.95
68+ top_k: 20
69+ min_p: 0.0
70+ max_tokens: 8192
71+ ```
72+- **Write the file with the wizard.** `mm init --provider kronk` lists Kronk's models and reads the chosen one's window, sampling and chat template from Kronk. See [How to create an agent](create-an-agent.md).
73+- **Let mm pull the model.** With `autoPull: true`, mm pulls a model Kronk does not list at start-up, through `POST /v1/kronk/models/pull`, and shows the progress. See [`autoPull`](../reference/configuration.md).
74+- **From a Docker sandbox.** `kronk server start` binds the loopback only, which a sandbox cannot reach. On the host, start it with `kronk server start --api-host 0.0.0.0:11435`, and allow `localhost:11435` in the sandbox's network policy. With no `baseUrl` answering, `mm` then falls back to `http://host.docker.internal:11435/v1` by itself.
75+- **A larger or smaller window.** The window is Kronk's, per model: set `context-window` for the model in `~/.kronk/models/model_config.yaml` and restart Kronk. `mm` reads the new value at start-up.
76+- **A protected Kronk.** In the `authenticated` or `full-protected` modes, export a token in `KRONK_TOKEN` (or name another variable under `apiKeyEnv`). A refused token is a start-up warning naming the variable.
77+- **Free the memory.** `/unload` and the `unload_model` tool unload the model through `POST /v1/kronk/models/unload`. See [How to free a model's memory](unload-a-model.md).
78+- **Keep your own `agent.yaml`.** `AGENT_PROVIDER=kronk ./mm` works as long as the file leaves `baseUrl` empty.
79+
80+## What differs from the other servers
81+
82+- No `Reading the prompt …` line: Kronk sends no progress while it reads, so `firstTokenTimeout` has to cover the whole prompt and the model's loading.
83+- Reasoning is turned off with `chat_template_kwargs: {enable_thinking: false}`, as on llama.cpp; Kronk ignores `reasoning_effort: none`. The summary of a compaction does it by itself. See [How to turn a model's reasoning off](turn-reasoning-off.md).
84+
85+## See also
86+
87+- [Providers: one implementation per protocol](../explanation/providers.md) — what was measured on Kronk.
88+- [Configuration reference](../reference/configuration.md) — provider defaults.
modified docs/en/how-to/turn-reasoning-off.md +2 -2
@@ -8,7 +8,7 @@ A thinking model (Qwen3.5, Qwen3.x, gpt-oss…) reasons before it answers. On a
88
99 2. Add the server's own switch under `sampling`.
1010
11- For `dmr`, `llamacpp` or `llmman` — all llama.cpp underneath:
11+ For `dmr`, `llamacpp`, `llmman` or `kronk` — all llama.cpp underneath:
1212
1313 ```yaml
1414 sampling:
@@ -61,7 +61,7 @@ A thinking model (Qwen3.5, Qwen3.x, gpt-oss…) reasons before it answers. On a
6161 - On `ollama`, `reasoning_effort` goes at the top level of `sampling`, not under `chat_template_kwargs`; only `none` has been measured there.
6262 - Compaction summaries do not follow it — see below.
6363 - **Turn it off for one run.** Keep two configuration files, or start the server itself without reasoning (for llama-server, see its `--reasoning-budget` option).
64-- **Compaction summaries.** Nothing to do: `mm` turns reasoning off for the summary of `/compact` and of the automatic compression, whatever `sampling` says. On `dmr`, `llamacpp` and `llmman` it sets `enable_thinking: false` in `chat_template_kwargs`, keeps your other template variables, and lowers a `reasoning_effort` you set there to `low` — gpt-oss's template has no "off", so a summary from that model still reasons a little and that counts against `context.summaryMaxTokens`. On `ollama` it sends `reasoning_effort: none`.
64+- **Compaction summaries.** Nothing to do: `mm` turns reasoning off for the summary of `/compact` and of the automatic compression, whatever `sampling` says. On `dmr`, `llamacpp`, `llmman` and `kronk` it sets `enable_thinking: false` in `chat_template_kwargs`, keeps your other template variables, and lowers a `reasoning_effort` you set there to `low` — gpt-oss's template has no "off", so a summary from that model still reasons a little and that counts against `context.summaryMaxTokens`. On `ollama` it sends `reasoning_effort: none`.
6565
6666 ## See also
6767
@@ -8,7 +8,7 @@ A thinking model (Qwen3.5, Qwen3.x, gpt-oss…) reasons before it answers. On a
8 8
9 2. Add the server's own switch under `sampling`.9 2. Add the server's own switch under `sampling`.
10 10
11- For `dmr`, `llamacpp` or `llmman` — all llama.cpp underneath:11+ For `dmr`, `llamacpp`, `llmman` or `kronk` — all llama.cpp underneath:
12 12
13 ```yaml13 ```yaml
14 sampling:14 sampling:
@@ -61,7 +61,7 @@ A thinking model (Qwen3.5, Qwen3.x, gpt-oss…) reasons before it answers. On a
61 - On `ollama`, `reasoning_effort` goes at the top level of `sampling`, not under `chat_template_kwargs`; only `none` has been measured there.61 - On `ollama`, `reasoning_effort` goes at the top level of `sampling`, not under `chat_template_kwargs`; only `none` has been measured there.
62 - Compaction summaries do not follow it — see below.62 - Compaction summaries do not follow it — see below.
63 - **Turn it off for one run.** Keep two configuration files, or start the server itself without reasoning (for llama-server, see its `--reasoning-budget` option).63 - **Turn it off for one run.** Keep two configuration files, or start the server itself without reasoning (for llama-server, see its `--reasoning-budget` option).
64-- **Compaction summaries.** Nothing to do: `mm` turns reasoning off for the summary of `/compact` and of the automatic compression, whatever `sampling` says. On `dmr`, `llamacpp` and `llmman` it sets `enable_thinking: false` in `chat_template_kwargs`, keeps your other template variables, and lowers a `reasoning_effort` you set there to `low` — gpt-oss's template has no "off", so a summary from that model still reasons a little and that counts against `context.summaryMaxTokens`. On `ollama` it sends `reasoning_effort: none`.64+- **Compaction summaries.** Nothing to do: `mm` turns reasoning off for the summary of `/compact` and of the automatic compression, whatever `sampling` says. On `dmr`, `llamacpp`, `llmman` and `kronk` it sets `enable_thinking: false` in `chat_template_kwargs`, keeps your other template variables, and lowers a `reasoning_effort` you set there to `low` — gpt-oss's template has no "off", so a summary from that model still reasons a little and that counts against `context.summaryMaxTokens`. On `ollama` it sends `reasoning_effort: none`.
65 65
66 ## See also66 ## See also
67 67
modified docs/en/how-to/unload-a-model.md +2 -2
@@ -1,6 +1,6 @@
11 # How to free a model's memory
22
3-This guide shows how to unload a model from the model server's memory without stopping the server: by hand with `/unload`, or by the agent itself with its `unload_model` tool — typically to free the memory of another agent's model once its answer is in. It assumes mm runs on llmman, Ollama, or llama-server in router mode. The model reloads by itself the next time something uses it.
3+This guide shows how to unload a model from the model server's memory without stopping the server: by hand with `/unload`, or by the agent itself with its `unload_model` tool — typically to free the memory of another agent's model once its answer is in. It assumes mm runs on llmman, Ollama, Kronk, or llama-server in router mode. The model reloads by itself the next time something uses it.
44
55 ## Unload by hand
66
@@ -22,7 +22,7 @@ Name another model served by the same server to unload that one instead:
2222 /unload hf.co/unsloth/Qwen3.5-9B-GGUF:Q4_K_M
2323 ```
2424
25-A model that is not loaded gives `🧹 <model> was not loaded: nothing to unload`; nothing is sent to the server, so nothing is loaded either. Check what is loaded with `llmman ps`, `ollama ps`, or `curl <server>/api/ps`.
25+A model that is not loaded gives `🧹 <model> was not loaded: nothing to unload`, and nothing gets loaded: llmman and Ollama are not even asked, Kronk answers that the model is not loaded. Check what is loaded with `llmman ps`, `ollama ps`, `curl <server>/api/ps`, or, on Kronk, `curl <server>/v1/kronk/models/ps`.
2626
2727 ## Let the agent unload a model
2828
@@ -1,6 +1,6 @@
1 # How to free a model's memory1 # How to free a model's memory
2 2
3-This guide shows how to unload a model from the model server's memory without stopping the server: by hand with `/unload`, or by the agent itself with its `unload_model` tool — typically to free the memory of another agent's model once its answer is in. It assumes mm runs on llmman, Ollama, or llama-server in router mode. The model reloads by itself the next time something uses it.3+This guide shows how to unload a model from the model server's memory without stopping the server: by hand with `/unload`, or by the agent itself with its `unload_model` tool — typically to free the memory of another agent's model once its answer is in. It assumes mm runs on llmman, Ollama, Kronk, or llama-server in router mode. The model reloads by itself the next time something uses it.
4 4
5 ## Unload by hand5 ## Unload by hand
6 6
@@ -22,7 +22,7 @@ Name another model served by the same server to unload that one instead:
22 /unload hf.co/unsloth/Qwen3.5-9B-GGUF:Q4_K_M22 /unload hf.co/unsloth/Qwen3.5-9B-GGUF:Q4_K_M
23 ```23 ```
24 24
25-A model that is not loaded gives `🧹 <model> was not loaded: nothing to unload`; nothing is sent to the server, so nothing is loaded either. Check what is loaded with `llmman ps`, `ollama ps`, or `curl <server>/api/ps`.25+A model that is not loaded gives `🧹 <model> was not loaded: nothing to unload`, and nothing gets loaded: llmman and Ollama are not even asked, Kronk answers that the model is not loaded. Check what is loaded with `llmman ps`, `ollama ps`, `curl <server>/api/ps`, or, on Kronk, `curl <server>/v1/kronk/models/ps`.
26 26
27 ## Let the agent unload a model27 ## Let the agent unload a model
28 28
modified docs/en/reference/cli.md +3 -3
@@ -71,7 +71,7 @@ The first existing source wins.
7171 | `MM_BIN` | Set by `mm` in its own environment, so every process it starts inherits it — the commands its `bash` tool runs, its hooks: the path of the running `mm` binary. |
7272 | `MM_CONFIG` | Set by `mm` in its own environment, like `MM_BIN`, when a configuration file was read: the absolute path of its configuration file. Read by `--ask` / `--ask-with-context` when no `-config` and no positional path are given. |
7373 | `MM_ASK_DEPTH` | How many `/ask` levels deep this agent runs (unset = 0). Set by `/ask`; at 2, `/ask` is refused. |
74-| Variable named by `apiKeyEnv` | Holds the API key. Default name: none for `dmr`, `LLAMA_API_KEY` for `llamacpp`, `LLMMAN_API_KEY` for `llmman`, `OLLAMA_API_KEY` for `ollama`. |
74+| Variable named by `apiKeyEnv` | Holds the API key. Default name: none for `dmr`, `LLAMA_API_KEY` for `llamacpp`, `LLMMAN_API_KEY` for `llmman`, `OLLAMA_API_KEY` for `ollama`, `KRONK_TOKEN` for `kronk`. |
7575
7676 ## Start-up output
7777
@@ -222,7 +222,7 @@ An agent file outside the workspace only gives a warning: see [when the agent fi
222222
223223 | Flag | Question when absent | Default |
224224 |------|----------------------|---------|
225-| `--provider` | Model server: `llmman`, `ollama`, `dmr`, `llamacpp`, `openai` | the first local one whose server answers, else `llmman` (`openai` is never probed: its default address is the OpenAI API) |
225+| `--provider` | Model server: `llmman`, `ollama`, `dmr`, `llamacpp`, `kronk`, `openai` | the first local one whose server answers, else `llmman` (`openai` is never probed: its default address is the OpenAI API) |
226226 | `--base-url` | `openai` only: Endpoint — the service's OpenAI-compatible address (OpenAI, Cerebras, Hugging Face…); for the other providers, not asked | `https://api.openai.com/v1` for `openai`; otherwise the provider's address, or its fallback when it does not answer. Written as `baseUrl` unless it is the default |
227227 | `--api-key-env` | `openai` only: Environment variable that holds the API key | `OPENAI_API_KEY`. Written as `apiKeyEnv` unless it is the default |
228228 | `--model` | Model: a list of the server's models (`/api/tags` for llmman and Ollama, `/models` for the others; 5 s for a local server, 20 s for `openai`), or a name to type when there is no list — the question says why (`<VARIABLE> is not set: no list`, or the server's error). For `openai` the list is asked with the key read from the variable named above, and not asked at all when that variable is empty | the first of the list |
@@ -240,7 +240,7 @@ An agent file outside the workspace only gives a warning: see [when the agent fi
240240 |--------|-----------|
241241 | 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, `←` `→` move the cursor in a text answer (`Home` `End` or `Ctrl-A` `Ctrl-E` to its ends; typing inserts at the cursor, `Backspace` deletes before it, `Delete` after it), `Ctrl-U` clears it, `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. |
242242 | 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. |
243-| 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`. |
243+| 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. Kronk: `GET /v1/kronk/models/<id>` (the id escaped) — the window the model is configured with (`model_config.context-window`; the GGUF's trained length when none), the GGUF's sampling, the chat template; for a profile (`provider/modelID/PROFILE`), the sampling Kronk applies for it (`temperature`, `top_p`, `top_k`, `min_p`, `max_tokens` when capped), copied into `sampling` over every other source. Docker Model Runner and `openai`: none — the family table decides; `openai` lists its models with `OPENAI_API_KEY`. |
244244 | 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`). |
245245 | 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. |
246246 | 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. |
@@ -71,7 +71,7 @@ The first existing source wins.
71 | `MM_BIN` | Set by `mm` in its own environment, so every process it starts inherits it — the commands its `bash` tool runs, its hooks: the path of the running `mm` binary. |71 | `MM_BIN` | Set by `mm` in its own environment, so every process it starts inherits it — the commands its `bash` tool runs, its hooks: the path of the running `mm` binary. |
72 | `MM_CONFIG` | Set by `mm` in its own environment, like `MM_BIN`, when a configuration file was read: the absolute path of its configuration file. Read by `--ask` / `--ask-with-context` when no `-config` and no positional path are given. |72 | `MM_CONFIG` | Set by `mm` in its own environment, like `MM_BIN`, when a configuration file was read: the absolute path of its configuration file. Read by `--ask` / `--ask-with-context` when no `-config` and no positional path are given. |
73 | `MM_ASK_DEPTH` | How many `/ask` levels deep this agent runs (unset = 0). Set by `/ask`; at 2, `/ask` is refused. |73 | `MM_ASK_DEPTH` | How many `/ask` levels deep this agent runs (unset = 0). Set by `/ask`; at 2, `/ask` is refused. |
74-| Variable named by `apiKeyEnv` | Holds the API key. Default name: none for `dmr`, `LLAMA_API_KEY` for `llamacpp`, `LLMMAN_API_KEY` for `llmman`, `OLLAMA_API_KEY` for `ollama`. |74+| Variable named by `apiKeyEnv` | Holds the API key. Default name: none for `dmr`, `LLAMA_API_KEY` for `llamacpp`, `LLMMAN_API_KEY` for `llmman`, `OLLAMA_API_KEY` for `ollama`, `KRONK_TOKEN` for `kronk`. |
75 75
76 ## Start-up output76 ## Start-up output
77 77
@@ -222,7 +222,7 @@ An agent file outside the workspace only gives a warning: see [when the agent fi
222 222
223 | Flag | Question when absent | Default |223 | Flag | Question when absent | Default |
224 |------|----------------------|---------|224 |------|----------------------|---------|
225-| `--provider` | Model server: `llmman`, `ollama`, `dmr`, `llamacpp`, `openai` | the first local one whose server answers, else `llmman` (`openai` is never probed: its default address is the OpenAI API) |225+| `--provider` | Model server: `llmman`, `ollama`, `dmr`, `llamacpp`, `kronk`, `openai` | the first local one whose server answers, else `llmman` (`openai` is never probed: its default address is the OpenAI API) |
226 | `--base-url` | `openai` only: Endpoint — the service's OpenAI-compatible address (OpenAI, Cerebras, Hugging Face…); for the other providers, not asked | `https://api.openai.com/v1` for `openai`; otherwise the provider's address, or its fallback when it does not answer. Written as `baseUrl` unless it is the default |226 | `--base-url` | `openai` only: Endpoint — the service's OpenAI-compatible address (OpenAI, Cerebras, Hugging Face…); for the other providers, not asked | `https://api.openai.com/v1` for `openai`; otherwise the provider's address, or its fallback when it does not answer. Written as `baseUrl` unless it is the default |
227 | `--api-key-env` | `openai` only: Environment variable that holds the API key | `OPENAI_API_KEY`. Written as `apiKeyEnv` unless it is the default |227 | `--api-key-env` | `openai` only: Environment variable that holds the API key | `OPENAI_API_KEY`. Written as `apiKeyEnv` unless it is the default |
228 | `--model` | Model: a list of the server's models (`/api/tags` for llmman and Ollama, `/models` for the others; 5 s for a local server, 20 s for `openai`), or a name to type when there is no list — the question says why (`<VARIABLE> is not set: no list`, or the server's error). For `openai` the list is asked with the key read from the variable named above, and not asked at all when that variable is empty | the first of the list |228 | `--model` | Model: a list of the server's models (`/api/tags` for llmman and Ollama, `/models` for the others; 5 s for a local server, 20 s for `openai`), or a name to type when there is no list — the question says why (`<VARIABLE> is not set: no list`, or the server's error). For `openai` the list is asked with the key read from the variable named above, and not asked at all when that variable is empty | the first of the list |
@@ -240,7 +240,7 @@ An agent file outside the workspace only gives a warning: see [when the agent fi
240 |--------|-----------|240 |--------|-----------|
241 | 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, `←` `→` move the cursor in a text answer (`Home` `End` or `Ctrl-A` `Ctrl-E` to its ends; typing inserts at the cursor, `Backspace` deletes before it, `Delete` after it), `Ctrl-U` clears it, `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. |241 | 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, `←` `→` move the cursor in a text answer (`Home` `End` or `Ctrl-A` `Ctrl-E` to its ends; typing inserts at the cursor, `Backspace` deletes before it, `Delete` after it), `Ctrl-U` clears it, `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. |
242 | 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. |242 | 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. |
243-| 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`. |243+| 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. Kronk: `GET /v1/kronk/models/<id>` (the id escaped) — the window the model is configured with (`model_config.context-window`; the GGUF's trained length when none), the GGUF's sampling, the chat template; for a profile (`provider/modelID/PROFILE`), the sampling Kronk applies for it (`temperature`, `top_p`, `top_k`, `min_p`, `max_tokens` when capped), copied into `sampling` over every other source. Docker Model Runner and `openai`: none — the family table decides; `openai` lists its models with `OPENAI_API_KEY`. |
244 | 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`). |244 | 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`). |
245 | 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. |245 | 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. |
246 | 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. |246 | 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. |
modified docs/en/reference/configuration.md +8 -7
@@ -6,7 +6,7 @@
66
77 | Key | Type | Default | Description |
88 |-----|------|---------|-------------|
9-| `provider` | string | `dmr` | Registry key of the LLM server: `dmr`, `llamacpp`, `llmman`, `ollama`, or `openai` for any other OpenAI-compatible server (see [How to use any OpenAI-compatible server](../how-to/run-with-an-openai-compatible-server.md)). Overridden by `AGENT_PROVIDER`. |
9+| `provider` | string | `dmr` | Registry key of the LLM server: `dmr`, `llamacpp`, `llmman`, `ollama`, `kronk`, or `openai` for any other OpenAI-compatible server (see [How to use any OpenAI-compatible server](../how-to/run-with-an-openai-compatible-server.md)). Overridden by `AGENT_PROVIDER`. |
1010 | `model` | string | `huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M` | Model identifier in the provider's naming. Overridden by `AGENT_MODEL`. |
1111 | `baseUrl` | string | `""` (provider default) | OpenAI-compatible endpoint tried first. Overridden by `AGENT_BASE_URL`. |
1212 | `fallback` | string or absent | absent (provider default) | Endpoint used when `baseUrl` does not answer `/models` within 2 s. Absent = provider default; `""` = no fallback. |
@@ -31,11 +31,11 @@
3131 | `planTool` | bool | `true` | Declare `plan`, through which the model keeps a plan of tasks during multi-step work. `false` removes it. See [tools](tools.md#plan) and [Manage plans and tasks](../how-to/manage-plans-and-tasks.md). |
3232 | `askUserTool` | bool | `true` | Declare `ask_user`, through which the model asks the user questions (pick one, pick several, type a text, yes or no) and waits for the answers. `false` removes it: the model cannot ask. Never declared beside a `structuredOutput`. Overridden by `AGENT_ASK_USER`; for an agent asked by another one, also by the asker's `mmConfigs.<name>.askUser`. See [tools](tools.md#ask_user). |
3333 | `saveAnswerTool` | bool | `false` | Declare `save_answer`, which writes an answer the model already gave to a new file: the model sends a file name, not the text again. Off by default. The `/save` command exists either way. See [tools](tools.md#save_answer). |
34-| `unloadModels` | bool | `true` | The `/unload [model]` command (terminal, window, editor) and the `unload_model` tool, which free a model from the server's memory: llmman and Ollama, llama-server in router mode. `false` removes both. The tool is never declared beside a `structuredOutput` or a `decision` block. See [tools](tools.md#unload_model) and [How to free a model's memory](../how-to/unload-a-model.md). |
35-| `autoPull` | bool | `false` | At start-up, download the models this agent will use and the server does not list: its own, its `mmConfigs` agents', its `skillRouting` / `toolRouting` deciders'. The progress goes to stderr — one line rewritten in place on a terminal (`⬇ pulling <model> — 52 MB / 145 MB (35 %)`), a line per tenth elsewhere — then `⬇ pulled <model> in 0.8s`. The agent's own model failing stops `mm` with the server's message (`[engine error: autoPull: <model>: the server says: …]`); another model failing, a server that does not answer, or a provider that cannot pull is a `[warning: autoPull: …]`. An agent started by `/ask` or `ask_agent` pulls nothing; `mm --ask` pulls nothing either. A model the server already lists is never pulled (a name without a tag also matches its `:latest` entry). By provider: `llmman` and `ollama` — `POST /api/pull`; `dmr` — `POST /models/create` on the runner (the base URL without `/engines/…`), the pull counting as done once the runner lists the model; `llamacpp` — no download over the API: the warning says to start llama-server with `-hf <repo>:<quant>`, or to put the GGUF in the router's models directory; `openai` — a warning. |
36-| `sampling` | map | `temperature: 0.0`, `top_p: 0.9`, `max_tokens: 4096` | Generation settings, OpenAI API key names, passed to the server. The file's block is merged with these defaults: a default the file does not name is still sent. **Except with `provider: openai`**: the request carries exactly the file's block, nothing added (no block: no sampling key, the server's defaults apply) — the hosted OpenAI reasoning models refuse `max_tokens` and accept only the default temperature, so a default left in would be a 400. A key the OpenAI API does not declare is sent as it is, at the top level of the request — for example `chat_template_kwargs: {enable_thinking: false}`, which tells a llama.cpp server (`dmr`, `llamacpp`, `llmman`) not to reason; `reasoning_effort: none` does the same on Ollama. `messages`, `tools`, `tool_choice` (and the legacy `functions`, `function_call`) are ignored: Genkit builds them from the conversation. The shipped `agent.yaml` adds `parallel_tool_calls: false`. `max_tokens` also bounds one tool call: a `write_file` of a long document (or a thinking model's reasoning plus the call) can hit it, and the call is then cut off. mm hands it back to the model twice, asking for smaller pieces, then stops with `the model hit max_tokens (N) while writing a tool call`; raise the value if that happens. A config that names the limit `max_completion_tokens` (the OpenAI API's name for its newer models) is read the same way: that name is used in this message and for the summary request of the context compression. With streaming, mm also asks the server for token counts (`stream_options.include_usage`). |
34+| `unloadModels` | bool | `true` | The `/unload [model]` command (terminal, window, editor) and the `unload_model` tool, which free a model from the server's memory: llmman and Ollama, Kronk, llama-server in router mode. `false` removes both. The tool is never declared beside a `structuredOutput` or a `decision` block. See [tools](tools.md#unload_model) and [How to free a model's memory](../how-to/unload-a-model.md). |
35+| `autoPull` | bool | `false` | At start-up, download the models this agent will use and the server does not list: its own, its `mmConfigs` agents', its `skillRouting` / `toolRouting` deciders'. The progress goes to stderr — one line rewritten in place on a terminal (`⬇ pulling <model> — 52 MB / 145 MB (35 %)`), a line per tenth elsewhere — then `⬇ pulled <model> in 0.8s`. The agent's own model failing stops `mm` with the server's message (`[engine error: autoPull: <model>: the server says: …]`); another model failing, a server that does not answer, or a provider that cannot pull is a `[warning: autoPull: …]`. An agent started by `/ask` or `ask_agent` pulls nothing; `mm --ask` pulls nothing either. A model the server already lists is never pulled (a name without a tag also matches its `:latest` entry). By provider: `llmman` and `ollama` — `POST /api/pull`; `kronk` — `POST /v1/kronk/models/pull`, the pull counting as done only when Kronk's stream ends with `"downloaded": true` (Kronk answers 200 to a failed pull; its last status is then the message); `dmr` — `POST /models/create` on the runner (the base URL without `/engines/…`), the pull counting as done once the runner lists the model; `llamacpp` — no download over the API: the warning says to start llama-server with `-hf <repo>:<quant>`, or to put the GGUF in the router's models directory; `openai` — a warning. |
36+| `sampling` | map | `temperature: 0.0`, `top_p: 0.9`, `max_tokens: 4096` | Generation settings, OpenAI API key names, passed to the server. The file's block is merged with these defaults: a default the file does not name is still sent. **Except with `provider: openai`**: the request carries exactly the file's block, nothing added (no block: no sampling key, the server's defaults apply) — the hosted OpenAI reasoning models refuse `max_tokens` and accept only the default temperature, so a default left in would be a 400. A key the OpenAI API does not declare is sent as it is, at the top level of the request — for example `chat_template_kwargs: {enable_thinking: false}`, which tells a llama.cpp server (`dmr`, `llamacpp`, `llmman`) or Kronk not to reason; `reasoning_effort: none` does the same on Ollama. `messages`, `tools`, `tool_choice` (and the legacy `functions`, `function_call`) are ignored: Genkit builds them from the conversation. The shipped `agent.yaml` adds `parallel_tool_calls: false`. `max_tokens` also bounds one tool call: a `write_file` of a long document (or a thinking model's reasoning plus the call) can hit it, and the call is then cut off. mm hands it back to the model twice, asking for smaller pieces, then stops with `the model hit max_tokens (N) while writing a tool call`; raise the value if that happens. A config that names the limit `max_completion_tokens` (the OpenAI API's name for its newer models) is read the same way: that name is used in this message and for the summary request of the context compression. With streaming, mm also asks the server for token counts (`stream_options.include_usage`). |
3737 | `watchdogTimeout` | duration | `20s` | Silence tolerated between two streamed tokens before the connection is declared hung. Doubled for summary requests. |
38-| `firstTokenTimeout` | duration | `3m` | Silence tolerated before a model call's first token, while the server reads the prompt (every round trip of a turn has one). Never shorter than `watchdogTimeout`; must be >= 0. With a llama.cpp server (`dmr`, `llamacpp`, `llmman` — whose local GGUF models run in llama-server) mm asks for `return_progress`: each progress report restarts this clock, and the spinner shows `Reading the prompt 12.0k/25.3k tokens`. Ollama sends no progress: this one wait covers the whole prompt (and the model's loading). On a timeout the watchdog says `no first token from the server after …` (before the first token) or `connection hang detected after …` (after). |
38+| `firstTokenTimeout` | duration | `3m` | Silence tolerated before a model call's first token, while the server reads the prompt (every round trip of a turn has one). Never shorter than `watchdogTimeout`; must be >= 0. With a llama.cpp server (`dmr`, `llamacpp`, `llmman` — whose local GGUF models run in llama-server) mm asks for `return_progress`: each progress report restarts this clock, and the spinner shows `Reading the prompt 12.0k/25.3k tokens`. Ollama and Kronk send no progress: this one wait covers the whole prompt (and the model's loading). On a timeout the watchdog says `no first token from the server after …` (before the first token) or `connection hang detected after …` (after). |
3939 | `bashTimeout` | duration | `30s` | How long one `bash` command may run before it is killed; the tool then answers `[error: <bashTimeout> timeout exceeded]`. Must be > 0. The watchdog keeps counting while a command runs (ticket 57): a command longer than `watchdogTimeout` ends the turn, so raise both together. |
4040 | `context` | map | see below | Automatic history compression. |
4141 | `sessions` | map | see below | Persistent, per-project sessions. |
@@ -59,7 +59,7 @@
5959 | `threshold` | int | `75` | Percentage of the context window beyond which the history is compressed before the next question. Between 1 and 100. |
6060 | `maxMessages` | int | `80` | Message count that triggers a compression when the window is unknown or the estimate is low. `0` disables. Must be ≥ 0. |
6161 | `keepLastTurns` | int | `3` | Question turns kept raw at the end of the history. Must be ≥ 1. |
62-| `summaryMaxTokens` | int | `1200` | `max_tokens` of the summary request. Must be > 0. A thinking model's reasoning counts against it: on `dmr`, `llamacpp` and `llmman` the summary request turns reasoning off (`chat_template_kwargs.enable_thinking: false`), on `ollama` too (`reasoning_effort: "none"`, the one switch Ollama obeys) — whatever `sampling` says; other `chat_template_kwargs` are kept, and a `reasoning_effort` set there is lowered to `low` (gpt-oss has no "off"); elsewhere, if the error says the model "reasoned until the limit", raise it. |
62+| `summaryMaxTokens` | int | `1200` | `max_tokens` of the summary request. Must be > 0. A thinking model's reasoning counts against it: on `dmr`, `llamacpp`, `llmman` and `kronk` the summary request turns reasoning off (`chat_template_kwargs.enable_thinking: false`), on `ollama` too (`reasoning_effort: "none"`, the one switch Ollama obeys) — whatever `sampling` says; other `chat_template_kwargs` are kept, and a `reasoning_effort` set there is lowered to `low` (gpt-oss has no "off"); elsewhere, if the error says the model "reasoned until the limit", raise it. |
6363 | `prompt` | string | `""` | Replaces the built-in summary prompt when not empty. |
6464 | `showStats` | bool | `true` | Print the one-line `🗜️` report after each compression. |
6565
@@ -197,6 +197,7 @@ tools:
197197 | `llamacpp` | `http://127.0.0.1:8080/v1` | none | `LLAMA_API_KEY` | no |
198198 | `llmman` | `http://127.0.0.1:17434/v1` | `http://host.docker.internal:17434/v1` | `LLMMAN_API_KEY` | no |
199199 | `ollama` | `http://localhost:11434/v1` | `http://host.docker.internal:11434/v1` | `OLLAMA_API_KEY` | no |
200+| `kronk` | `http://127.0.0.1:11435/v1` | `http://host.docker.internal:11435/v1` | `KRONK_TOKEN` | no — only Kronk's `authenticated` and `full-protected` modes check it |
200201 | `openai` | `https://api.openai.com/v1` | none | `OPENAI_API_KEY` | no — a server that needs one answers 401, and the start-up warning names the variable |
201202
202203 The `openai` provider sends only standard fields: no `return_progress` (so no prompt-reading progress while a long prompt is read), no reasoning switch on a summary request, no `/unload`. It cannot ask the server for its window: set `contextWindow`, or compression counts messages (`context.maxMessages`). Its start-up probe sends the key to `GET /models`; a 401 or 403 is reported as a refused key, not as a server that does not answer.
@@ -464,7 +465,7 @@ Reported on stderr as `[config error: <path>: <message>]`, exit code 1 — in ev
464465 | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | a name starting with `with-context` |
465466 | `mmConfigs.<name>: the path must not be empty` | `<name>: ""`, or a long form without `path` |
466467 | `hooks: unknown event "x" (…)`, `hooks.<event>[N]: …` | an invalid `hooks:` block — see [hooks](hooks.md#validation-errors) |
467-| `unknown provider "x" (known: dmr, llamacpp, llmman, ollama)` | Reported by the engine, as `[engine error: …]`. |
468+| `unknown provider "x" (known: dmr, kronk, llamacpp, llmman, ollama, openai)` | Reported by the engine, as `[engine error: …]`. |
468469
469470 A start-up warning, not an error, is printed when `context.enabled` is `true`, the window is unknown and `context.maxMessages` is `0`, and when a declared tool has the name of an MCP tool (`tools: <name> skipped — an MCP tool already has that name`; the tool is not declared), and when the file sets the deprecated `permissions` key (`permissions: ask is deprecated — use yolo: false instead …`).
470471
@@ -6,7 +6,7 @@
6 6
7 | Key | Type | Default | Description |7 | Key | Type | Default | Description |
8 |-----|------|---------|-------------|8 |-----|------|---------|-------------|
9-| `provider` | string | `dmr` | Registry key of the LLM server: `dmr`, `llamacpp`, `llmman`, `ollama`, or `openai` for any other OpenAI-compatible server (see [How to use any OpenAI-compatible server](../how-to/run-with-an-openai-compatible-server.md)). Overridden by `AGENT_PROVIDER`. |9+| `provider` | string | `dmr` | Registry key of the LLM server: `dmr`, `llamacpp`, `llmman`, `ollama`, `kronk`, or `openai` for any other OpenAI-compatible server (see [How to use any OpenAI-compatible server](../how-to/run-with-an-openai-compatible-server.md)). Overridden by `AGENT_PROVIDER`. |
10 | `model` | string | `huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M` | Model identifier in the provider's naming. Overridden by `AGENT_MODEL`. |10 | `model` | string | `huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M` | Model identifier in the provider's naming. Overridden by `AGENT_MODEL`. |
11 | `baseUrl` | string | `""` (provider default) | OpenAI-compatible endpoint tried first. Overridden by `AGENT_BASE_URL`. |11 | `baseUrl` | string | `""` (provider default) | OpenAI-compatible endpoint tried first. Overridden by `AGENT_BASE_URL`. |
12 | `fallback` | string or absent | absent (provider default) | Endpoint used when `baseUrl` does not answer `/models` within 2 s. Absent = provider default; `""` = no fallback. |12 | `fallback` | string or absent | absent (provider default) | Endpoint used when `baseUrl` does not answer `/models` within 2 s. Absent = provider default; `""` = no fallback. |
@@ -31,11 +31,11 @@
31 | `planTool` | bool | `true` | Declare `plan`, through which the model keeps a plan of tasks during multi-step work. `false` removes it. See [tools](tools.md#plan) and [Manage plans and tasks](../how-to/manage-plans-and-tasks.md). |31 | `planTool` | bool | `true` | Declare `plan`, through which the model keeps a plan of tasks during multi-step work. `false` removes it. See [tools](tools.md#plan) and [Manage plans and tasks](../how-to/manage-plans-and-tasks.md). |
32 | `askUserTool` | bool | `true` | Declare `ask_user`, through which the model asks the user questions (pick one, pick several, type a text, yes or no) and waits for the answers. `false` removes it: the model cannot ask. Never declared beside a `structuredOutput`. Overridden by `AGENT_ASK_USER`; for an agent asked by another one, also by the asker's `mmConfigs.<name>.askUser`. See [tools](tools.md#ask_user). |32 | `askUserTool` | bool | `true` | Declare `ask_user`, through which the model asks the user questions (pick one, pick several, type a text, yes or no) and waits for the answers. `false` removes it: the model cannot ask. Never declared beside a `structuredOutput`. Overridden by `AGENT_ASK_USER`; for an agent asked by another one, also by the asker's `mmConfigs.<name>.askUser`. See [tools](tools.md#ask_user). |
33 | `saveAnswerTool` | bool | `false` | Declare `save_answer`, which writes an answer the model already gave to a new file: the model sends a file name, not the text again. Off by default. The `/save` command exists either way. See [tools](tools.md#save_answer). |33 | `saveAnswerTool` | bool | `false` | Declare `save_answer`, which writes an answer the model already gave to a new file: the model sends a file name, not the text again. Off by default. The `/save` command exists either way. See [tools](tools.md#save_answer). |
34-| `unloadModels` | bool | `true` | The `/unload [model]` command (terminal, window, editor) and the `unload_model` tool, which free a model from the server's memory: llmman and Ollama, llama-server in router mode. `false` removes both. The tool is never declared beside a `structuredOutput` or a `decision` block. See [tools](tools.md#unload_model) and [How to free a model's memory](../how-to/unload-a-model.md). |34+| `unloadModels` | bool | `true` | The `/unload [model]` command (terminal, window, editor) and the `unload_model` tool, which free a model from the server's memory: llmman and Ollama, Kronk, llama-server in router mode. `false` removes both. The tool is never declared beside a `structuredOutput` or a `decision` block. See [tools](tools.md#unload_model) and [How to free a model's memory](../how-to/unload-a-model.md). |
35-| `autoPull` | bool | `false` | At start-up, download the models this agent will use and the server does not list: its own, its `mmConfigs` agents', its `skillRouting` / `toolRouting` deciders'. The progress goes to stderr — one line rewritten in place on a terminal (`⬇ pulling <model> — 52 MB / 145 MB (35 %)`), a line per tenth elsewhere — then `⬇ pulled <model> in 0.8s`. The agent's own model failing stops `mm` with the server's message (`[engine error: autoPull: <model>: the server says: …]`); another model failing, a server that does not answer, or a provider that cannot pull is a `[warning: autoPull: …]`. An agent started by `/ask` or `ask_agent` pulls nothing; `mm --ask` pulls nothing either. A model the server already lists is never pulled (a name without a tag also matches its `:latest` entry). By provider: `llmman` and `ollama` — `POST /api/pull`; `dmr` — `POST /models/create` on the runner (the base URL without `/engines/…`), the pull counting as done once the runner lists the model; `llamacpp` — no download over the API: the warning says to start llama-server with `-hf <repo>:<quant>`, or to put the GGUF in the router's models directory; `openai` — a warning. |35+| `autoPull` | bool | `false` | At start-up, download the models this agent will use and the server does not list: its own, its `mmConfigs` agents', its `skillRouting` / `toolRouting` deciders'. The progress goes to stderr — one line rewritten in place on a terminal (`⬇ pulling <model> — 52 MB / 145 MB (35 %)`), a line per tenth elsewhere — then `⬇ pulled <model> in 0.8s`. The agent's own model failing stops `mm` with the server's message (`[engine error: autoPull: <model>: the server says: …]`); another model failing, a server that does not answer, or a provider that cannot pull is a `[warning: autoPull: …]`. An agent started by `/ask` or `ask_agent` pulls nothing; `mm --ask` pulls nothing either. A model the server already lists is never pulled (a name without a tag also matches its `:latest` entry). By provider: `llmman` and `ollama` — `POST /api/pull`; `kronk` — `POST /v1/kronk/models/pull`, the pull counting as done only when Kronk's stream ends with `"downloaded": true` (Kronk answers 200 to a failed pull; its last status is then the message); `dmr` — `POST /models/create` on the runner (the base URL without `/engines/…`), the pull counting as done once the runner lists the model; `llamacpp` — no download over the API: the warning says to start llama-server with `-hf <repo>:<quant>`, or to put the GGUF in the router's models directory; `openai` — a warning. |
36-| `sampling` | map | `temperature: 0.0`, `top_p: 0.9`, `max_tokens: 4096` | Generation settings, OpenAI API key names, passed to the server. The file's block is merged with these defaults: a default the file does not name is still sent. **Except with `provider: openai`**: the request carries exactly the file's block, nothing added (no block: no sampling key, the server's defaults apply) — the hosted OpenAI reasoning models refuse `max_tokens` and accept only the default temperature, so a default left in would be a 400. A key the OpenAI API does not declare is sent as it is, at the top level of the request — for example `chat_template_kwargs: {enable_thinking: false}`, which tells a llama.cpp server (`dmr`, `llamacpp`, `llmman`) not to reason; `reasoning_effort: none` does the same on Ollama. `messages`, `tools`, `tool_choice` (and the legacy `functions`, `function_call`) are ignored: Genkit builds them from the conversation. The shipped `agent.yaml` adds `parallel_tool_calls: false`. `max_tokens` also bounds one tool call: a `write_file` of a long document (or a thinking model's reasoning plus the call) can hit it, and the call is then cut off. mm hands it back to the model twice, asking for smaller pieces, then stops with `the model hit max_tokens (N) while writing a tool call`; raise the value if that happens. A config that names the limit `max_completion_tokens` (the OpenAI API's name for its newer models) is read the same way: that name is used in this message and for the summary request of the context compression. With streaming, mm also asks the server for token counts (`stream_options.include_usage`). |36+| `sampling` | map | `temperature: 0.0`, `top_p: 0.9`, `max_tokens: 4096` | Generation settings, OpenAI API key names, passed to the server. The file's block is merged with these defaults: a default the file does not name is still sent. **Except with `provider: openai`**: the request carries exactly the file's block, nothing added (no block: no sampling key, the server's defaults apply) — the hosted OpenAI reasoning models refuse `max_tokens` and accept only the default temperature, so a default left in would be a 400. A key the OpenAI API does not declare is sent as it is, at the top level of the request — for example `chat_template_kwargs: {enable_thinking: false}`, which tells a llama.cpp server (`dmr`, `llamacpp`, `llmman`) or Kronk not to reason; `reasoning_effort: none` does the same on Ollama. `messages`, `tools`, `tool_choice` (and the legacy `functions`, `function_call`) are ignored: Genkit builds them from the conversation. The shipped `agent.yaml` adds `parallel_tool_calls: false`. `max_tokens` also bounds one tool call: a `write_file` of a long document (or a thinking model's reasoning plus the call) can hit it, and the call is then cut off. mm hands it back to the model twice, asking for smaller pieces, then stops with `the model hit max_tokens (N) while writing a tool call`; raise the value if that happens. A config that names the limit `max_completion_tokens` (the OpenAI API's name for its newer models) is read the same way: that name is used in this message and for the summary request of the context compression. With streaming, mm also asks the server for token counts (`stream_options.include_usage`). |
37 | `watchdogTimeout` | duration | `20s` | Silence tolerated between two streamed tokens before the connection is declared hung. Doubled for summary requests. |37 | `watchdogTimeout` | duration | `20s` | Silence tolerated between two streamed tokens before the connection is declared hung. Doubled for summary requests. |
38-| `firstTokenTimeout` | duration | `3m` | Silence tolerated before a model call's first token, while the server reads the prompt (every round trip of a turn has one). Never shorter than `watchdogTimeout`; must be >= 0. With a llama.cpp server (`dmr`, `llamacpp`, `llmman` — whose local GGUF models run in llama-server) mm asks for `return_progress`: each progress report restarts this clock, and the spinner shows `Reading the prompt 12.0k/25.3k tokens`. Ollama sends no progress: this one wait covers the whole prompt (and the model's loading). On a timeout the watchdog says `no first token from the server after …` (before the first token) or `connection hang detected after …` (after). |38+| `firstTokenTimeout` | duration | `3m` | Silence tolerated before a model call's first token, while the server reads the prompt (every round trip of a turn has one). Never shorter than `watchdogTimeout`; must be >= 0. With a llama.cpp server (`dmr`, `llamacpp`, `llmman` — whose local GGUF models run in llama-server) mm asks for `return_progress`: each progress report restarts this clock, and the spinner shows `Reading the prompt 12.0k/25.3k tokens`. Ollama and Kronk send no progress: this one wait covers the whole prompt (and the model's loading). On a timeout the watchdog says `no first token from the server after …` (before the first token) or `connection hang detected after …` (after). |
39 | `bashTimeout` | duration | `30s` | How long one `bash` command may run before it is killed; the tool then answers `[error: <bashTimeout> timeout exceeded]`. Must be > 0. The watchdog keeps counting while a command runs (ticket 57): a command longer than `watchdogTimeout` ends the turn, so raise both together. |39 | `bashTimeout` | duration | `30s` | How long one `bash` command may run before it is killed; the tool then answers `[error: <bashTimeout> timeout exceeded]`. Must be > 0. The watchdog keeps counting while a command runs (ticket 57): a command longer than `watchdogTimeout` ends the turn, so raise both together. |
40 | `context` | map | see below | Automatic history compression. |40 | `context` | map | see below | Automatic history compression. |
41 | `sessions` | map | see below | Persistent, per-project sessions. |41 | `sessions` | map | see below | Persistent, per-project sessions. |
@@ -59,7 +59,7 @@
59 | `threshold` | int | `75` | Percentage of the context window beyond which the history is compressed before the next question. Between 1 and 100. |59 | `threshold` | int | `75` | Percentage of the context window beyond which the history is compressed before the next question. Between 1 and 100. |
60 | `maxMessages` | int | `80` | Message count that triggers a compression when the window is unknown or the estimate is low. `0` disables. Must be ≥ 0. |60 | `maxMessages` | int | `80` | Message count that triggers a compression when the window is unknown or the estimate is low. `0` disables. Must be ≥ 0. |
61 | `keepLastTurns` | int | `3` | Question turns kept raw at the end of the history. Must be ≥ 1. |61 | `keepLastTurns` | int | `3` | Question turns kept raw at the end of the history. Must be ≥ 1. |
62-| `summaryMaxTokens` | int | `1200` | `max_tokens` of the summary request. Must be > 0. A thinking model's reasoning counts against it: on `dmr`, `llamacpp` and `llmman` the summary request turns reasoning off (`chat_template_kwargs.enable_thinking: false`), on `ollama` too (`reasoning_effort: "none"`, the one switch Ollama obeys) — whatever `sampling` says; other `chat_template_kwargs` are kept, and a `reasoning_effort` set there is lowered to `low` (gpt-oss has no "off"); elsewhere, if the error says the model "reasoned until the limit", raise it. |62+| `summaryMaxTokens` | int | `1200` | `max_tokens` of the summary request. Must be > 0. A thinking model's reasoning counts against it: on `dmr`, `llamacpp`, `llmman` and `kronk` the summary request turns reasoning off (`chat_template_kwargs.enable_thinking: false`), on `ollama` too (`reasoning_effort: "none"`, the one switch Ollama obeys) — whatever `sampling` says; other `chat_template_kwargs` are kept, and a `reasoning_effort` set there is lowered to `low` (gpt-oss has no "off"); elsewhere, if the error says the model "reasoned until the limit", raise it. |
63 | `prompt` | string | `""` | Replaces the built-in summary prompt when not empty. |63 | `prompt` | string | `""` | Replaces the built-in summary prompt when not empty. |
64 | `showStats` | bool | `true` | Print the one-line `🗜️` report after each compression. |64 | `showStats` | bool | `true` | Print the one-line `🗜️` report after each compression. |
65 65
@@ -197,6 +197,7 @@ tools:
197 | `llamacpp` | `http://127.0.0.1:8080/v1` | none | `LLAMA_API_KEY` | no |197 | `llamacpp` | `http://127.0.0.1:8080/v1` | none | `LLAMA_API_KEY` | no |
198 | `llmman` | `http://127.0.0.1:17434/v1` | `http://host.docker.internal:17434/v1` | `LLMMAN_API_KEY` | no |198 | `llmman` | `http://127.0.0.1:17434/v1` | `http://host.docker.internal:17434/v1` | `LLMMAN_API_KEY` | no |
199 | `ollama` | `http://localhost:11434/v1` | `http://host.docker.internal:11434/v1` | `OLLAMA_API_KEY` | no |199 | `ollama` | `http://localhost:11434/v1` | `http://host.docker.internal:11434/v1` | `OLLAMA_API_KEY` | no |
200+| `kronk` | `http://127.0.0.1:11435/v1` | `http://host.docker.internal:11435/v1` | `KRONK_TOKEN` | no — only Kronk's `authenticated` and `full-protected` modes check it |
200 | `openai` | `https://api.openai.com/v1` | none | `OPENAI_API_KEY` | no — a server that needs one answers 401, and the start-up warning names the variable |201 | `openai` | `https://api.openai.com/v1` | none | `OPENAI_API_KEY` | no — a server that needs one answers 401, and the start-up warning names the variable |
201 202
202 The `openai` provider sends only standard fields: no `return_progress` (so no prompt-reading progress while a long prompt is read), no reasoning switch on a summary request, no `/unload`. It cannot ask the server for its window: set `contextWindow`, or compression counts messages (`context.maxMessages`). Its start-up probe sends the key to `GET /models`; a 401 or 403 is reported as a refused key, not as a server that does not answer.203 The `openai` provider sends only standard fields: no `return_progress` (so no prompt-reading progress while a long prompt is read), no reasoning switch on a summary request, no `/unload`. It cannot ask the server for its window: set `contextWindow`, or compression counts messages (`context.maxMessages`). Its start-up probe sends the key to `GET /models`; a 401 or 403 is reported as a refused key, not as a server that does not answer.
@@ -464,7 +465,7 @@ Reported on stderr as `[config error: <path>: <message>]`, exit code 1 — in ev
464 | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | a name starting with `with-context` |465 | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | a name starting with `with-context` |
465 | `mmConfigs.<name>: the path must not be empty` | `<name>: ""`, or a long form without `path` |466 | `mmConfigs.<name>: the path must not be empty` | `<name>: ""`, or a long form without `path` |
466 | `hooks: unknown event "x" (…)`, `hooks.<event>[N]: …` | an invalid `hooks:` block — see [hooks](hooks.md#validation-errors) |467 | `hooks: unknown event "x" (…)`, `hooks.<event>[N]: …` | an invalid `hooks:` block — see [hooks](hooks.md#validation-errors) |
467-| `unknown provider "x" (known: dmr, llamacpp, llmman, ollama)` | Reported by the engine, as `[engine error: …]`. |468+| `unknown provider "x" (known: dmr, kronk, llamacpp, llmman, ollama, openai)` | Reported by the engine, as `[engine error: …]`. |
468 469
469 A start-up warning, not an error, is printed when `context.enabled` is `true`, the window is unknown and `context.maxMessages` is `0`, and when a declared tool has the name of an MCP tool (`tools: <name> skipped — an MCP tool already has that name`; the tool is not declared), and when the file sets the deprecated `permissions` key (`permissions: ask is deprecated — use yolo: false instead …`).470 A start-up warning, not an error, is printed when `context.enabled` is `true`, the window is unknown and `context.maxMessages` is `0`, and when a declared tool has the name of an MCP tool (`tools: <name> skipped — an MCP tool already has that name`; the tool is not declared), and when the file sets the deprecated `permissions` key (`permissions: ask is deprecated — use yolo: false instead …`).
470 471
modified docs/en/reference/tools.md +1 -0
@@ -311,6 +311,7 @@ Declared when `unloadModels` is true (the default), except beside a `structuredO
311311 | Aspect | Behaviour |
312312 |--------|-----------|
313313 | llmman, `ollama` | Looks the model up in `GET /api/ps` first; when it is loaded, `POST /api/generate {"model": …, "keep_alive": 0}` and expects `done_reason: "unload"`. A model that is not loaded is never asked for: the request would load it first. |
314+| `kronk` | `POST /v1/kronk/models/unload {"id": …}`, which expects `status: "unloaded"`. Kronk's 404 for a model that is not loaded gives `<model> was not loaded: nothing to unload`. |
314315 | `llamacpp` | `POST /models/unload {"model": …}`, which exists in router mode only (llama-server started without a model). A single-model server's 404 gives the reason and the alternatives: router mode, `--sleep-idle-seconds N`, stopping the server. |
315316 | `dmr` | `cannot unload <model>: the dmr provider cannot unload a model`. |
316317 | Result | One line: `unloaded <model>`, `<model> was not loaded: nothing to unload`, or `cannot unload <model>: <reason>` (unreachable server, the server's own error, no confirmation). |
@@ -311,6 +311,7 @@ Declared when `unloadModels` is true (the default), except beside a `structuredO
311 | Aspect | Behaviour |311 | Aspect | Behaviour |
312 |--------|-----------|312 |--------|-----------|
313 | llmman, `ollama` | Looks the model up in `GET /api/ps` first; when it is loaded, `POST /api/generate {"model": …, "keep_alive": 0}` and expects `done_reason: "unload"`. A model that is not loaded is never asked for: the request would load it first. |313 | llmman, `ollama` | Looks the model up in `GET /api/ps` first; when it is loaded, `POST /api/generate {"model": …, "keep_alive": 0}` and expects `done_reason: "unload"`. A model that is not loaded is never asked for: the request would load it first. |
314+| `kronk` | `POST /v1/kronk/models/unload {"id": …}`, which expects `status: "unloaded"`. Kronk's 404 for a model that is not loaded gives `<model> was not loaded: nothing to unload`. |
314 | `llamacpp` | `POST /models/unload {"model": …}`, which exists in router mode only (llama-server started without a model). A single-model server's 404 gives the reason and the alternatives: router mode, `--sleep-idle-seconds N`, stopping the server. |315 | `llamacpp` | `POST /models/unload {"model": …}`, which exists in router mode only (llama-server started without a model). A single-model server's 404 gives the reason and the alternatives: router mode, `--sleep-idle-seconds N`, stopping the server. |
315 | `dmr` | `cannot unload <model>: the dmr provider cannot unload a model`. |316 | `dmr` | `cannot unload <model>: the dmr provider cannot unload a model`. |
316 | Result | One line: `unloaded <model>`, `<model> was not loaded: nothing to unload`, or `cannot unload <model>: <reason>` (unreachable server, the server's own error, no confirmation). |317 | Result | One line: `unloaded <model>`, `<model> was not loaded: nothing to unload`, or `cannot unload <model>: <reason>` (unreachable server, the server's own error, no confirmation). |
modified docs/fr/README.md +2 -1
@@ -1,6 +1,6 @@
11 # mini-me — documentation (français)
22
3-`mm` est un agent de code qui tourne dans votre terminal — ligne à ligne, ou dans une fenêtre unique avec `mm -tui` — ou dans un éditeur via l'Agent Client Protocol. Il dialogue avec un serveur de modèle local (Docker Model Runner, `llama-server` de llama.cpp, llmman ou Ollama) et travaille avec une poignée d'outils intégrés : `bash`, `read_skill`, `read_file`, `write_file`, `edit_file` — et, quand ils sont activés, `lsp` et `fetch_content`.
3+`mm` est un agent de code qui tourne dans votre terminal — ligne à ligne, ou dans une fenêtre unique avec `mm -tui` — ou dans un éditeur via l'Agent Client Protocol. Il dialogue avec un serveur de modèle local (Docker Model Runner, `llama-server` de llama.cpp, llmman, Ollama ou Kronk) et travaille avec une poignée d'outils intégrés : `bash`, `read_skill`, `read_file`, `write_file`, `edit_file` — et, quand ils sont activés, `lsp` et `fetch_content`.
44
55 Cette documentation est organisée selon les quatre quadrants [Diátaxis](https://diataxis.fr). Choisissez celui qui correspond à votre besoin du moment.
66
@@ -15,6 +15,7 @@ Cette documentation est organisée selon les quatre quadrants [Diátaxis](https:
1515 - [Faire tourner mini-me sur llama.cpp plutôt que Docker Model Runner](how-to/run-with-llama-cpp.md)
1616 - [Faire tourner mini-me sur llmman](how-to/run-with-llmman.md) — `provider: llmman`, la config de démo dans `demos/llmman/`
1717 - [Faire tourner mini-me sur Ollama](how-to/run-with-ollama.md) — `provider: ollama`, la config de démo dans `demos/ollama/`
18+- [Faire tourner mini-me sur Kronk](how-to/run-with-kronk.md) — `provider: kronk`, la config de démo dans `demos/kronk/`
1819 - [Utiliser n'importe quel serveur compatible OpenAI](how-to/run-with-an-openai-compatible-server.md) — `provider: openai` : l'API d'OpenAI, vLLM, LM Studio, une passerelle
1920 - [Interroger un autre agent mm depuis votre session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<nom>`, `/ask-with-context-<nom>`
2021 - [Voir le JSON envoyé au modèle et sa réponse](how-to/inspect-model-traffic.md) — `trace`, `AGENT_TRACE`, lire les fichiers avec jq
@@ -1,6 +1,6 @@
1 # mini-me — documentation (français)1 # mini-me — documentation (français)
2 2
3-`mm` est un agent de code qui tourne dans votre terminal — ligne à ligne, ou dans une fenêtre unique avec `mm -tui` — ou dans un éditeur via l'Agent Client Protocol. Il dialogue avec un serveur de modèle local (Docker Model Runner, `llama-server` de llama.cpp, llmman ou Ollama) et travaille avec une poignée d'outils intégrés : `bash`, `read_skill`, `read_file`, `write_file`, `edit_file` — et, quand ils sont activés, `lsp` et `fetch_content`.3+`mm` est un agent de code qui tourne dans votre terminal — ligne à ligne, ou dans une fenêtre unique avec `mm -tui` — ou dans un éditeur via l'Agent Client Protocol. Il dialogue avec un serveur de modèle local (Docker Model Runner, `llama-server` de llama.cpp, llmman, Ollama ou Kronk) et travaille avec une poignée d'outils intégrés : `bash`, `read_skill`, `read_file`, `write_file`, `edit_file` — et, quand ils sont activés, `lsp` et `fetch_content`.
4 4
5 Cette documentation est organisée selon les quatre quadrants [Diátaxis](https://diataxis.fr). Choisissez celui qui correspond à votre besoin du moment.5 Cette documentation est organisée selon les quatre quadrants [Diátaxis](https://diataxis.fr). Choisissez celui qui correspond à votre besoin du moment.
6 6
@@ -15,6 +15,7 @@ Cette documentation est organisée selon les quatre quadrants [Diátaxis](https:
15 - [Faire tourner mini-me sur llama.cpp plutôt que Docker Model Runner](how-to/run-with-llama-cpp.md)15 - [Faire tourner mini-me sur llama.cpp plutôt que Docker Model Runner](how-to/run-with-llama-cpp.md)
16 - [Faire tourner mini-me sur llmman](how-to/run-with-llmman.md) — `provider: llmman`, la config de démo dans `demos/llmman/`16 - [Faire tourner mini-me sur llmman](how-to/run-with-llmman.md) — `provider: llmman`, la config de démo dans `demos/llmman/`
17 - [Faire tourner mini-me sur Ollama](how-to/run-with-ollama.md) — `provider: ollama`, la config de démo dans `demos/ollama/`17 - [Faire tourner mini-me sur Ollama](how-to/run-with-ollama.md) — `provider: ollama`, la config de démo dans `demos/ollama/`
18+- [Faire tourner mini-me sur Kronk](how-to/run-with-kronk.md) — `provider: kronk`, la config de démo dans `demos/kronk/`
18 - [Utiliser n'importe quel serveur compatible OpenAI](how-to/run-with-an-openai-compatible-server.md) — `provider: openai` : l'API d'OpenAI, vLLM, LM Studio, une passerelle19 - [Utiliser n'importe quel serveur compatible OpenAI](how-to/run-with-an-openai-compatible-server.md) — `provider: openai` : l'API d'OpenAI, vLLM, LM Studio, une passerelle
19 - [Interroger un autre agent mm depuis votre session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<nom>`, `/ask-with-context-<nom>`20 - [Interroger un autre agent mm depuis votre session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<nom>`, `/ask-with-context-<nom>`
20 - [Voir le JSON envoyé au modèle et sa réponse](how-to/inspect-model-traffic.md) — `trace`, `AGENT_TRACE`, lire les fichiers avec jq21 - [Voir le JSON envoyé au modèle et sa réponse](how-to/inspect-model-traffic.md) — `trace`, `AGENT_TRACE`, lire les fichiers avec jq
modified docs/fr/explanation/mm-init.md +2 -1
@@ -16,6 +16,7 @@ Le serveur connaît le modèle exact qui est chargé : c'est la source la plus p
1616 |---|---|---|
1717 | `llmman`, `ollama` | `/api/tags`, puis `/api/show` | la liste des modèles, la famille annoncée par le serveur (`details.family`), la fenêtre de contexte native, l'échantillonnage écrit dans le GGUF (`temperature`, `top_p`, `top_k`, `min_p`), le template de chat |
1818 | `llamacpp` | `/props` | la fenêtre et le template de chat |
19+| `kronk` | `/models`, puis `/v1/kronk/models/<id>` | la liste des modèles, l'architecture, la fenêtre avec laquelle le modèle est configuré (celle d'entraînement à défaut), l'échantillonnage écrit dans le GGUF, le template de chat ; pour un profil, son propre échantillonnage, qui l'emporte sur toute autre source : `mm` envoie son échantillonnage à chaque requête et une valeur de la requête remplace celle du profil, donc sans cette copie les valeurs du profil ne s'appliqueraient jamais |
1920 | `dmr` | `/models` | la liste des modèles seulement |
2021 | `openai` | `/models` (avec la clé) | la liste des modèles seulement |
2122
@@ -106,7 +107,7 @@ L'assistant n'écrase un fichier existant que si vous l'acceptez, et jamais avec
106107
107108 ## Limites connues
108109
109-- Les vérifications en direct n'ont été faites qu'avec llmman (Qwen3.5 4B, gemma-4-E4B). Ollama, Docker Model Runner et llama-server ont été testés contre de faux serveurs enregistrés dans les tests. L'API OpenAI elle-même n'a pas été essayée ; ses contraintes viennent de sa référence.
110+- Les vérifications en direct ont été faites avec llmman (Qwen3.5 4B, gemma-4-E4B) et Kronk 1.33.2 (`mm init --provider kronk … --check`, Qwopus3.5 4B Coder, 2026-11-11). Ollama, Docker Model Runner et llama-server ont été testés contre de faux serveurs enregistrés dans les tests. L'API OpenAI elle-même n'a pas été essayée ; ses contraintes viennent de sa référence.
110111 - Une valeur `[card]` a été recopiée, pas mesurée dans ce dépôt.
111112 - Les familles sont reconnues sur des mots de l'identifiant du modèle : un modèle renommé par son éditeur peut tomber dans `other`.
112113
@@ -16,6 +16,7 @@ Le serveur connaît le modèle exact qui est chargé : c'est la source la plus p
16 |---|---|---|16 |---|---|---|
17 | `llmman`, `ollama` | `/api/tags`, puis `/api/show` | la liste des modèles, la famille annoncée par le serveur (`details.family`), la fenêtre de contexte native, l'échantillonnage écrit dans le GGUF (`temperature`, `top_p`, `top_k`, `min_p`), le template de chat |17 | `llmman`, `ollama` | `/api/tags`, puis `/api/show` | la liste des modèles, la famille annoncée par le serveur (`details.family`), la fenêtre de contexte native, l'échantillonnage écrit dans le GGUF (`temperature`, `top_p`, `top_k`, `min_p`), le template de chat |
18 | `llamacpp` | `/props` | la fenêtre et le template de chat |18 | `llamacpp` | `/props` | la fenêtre et le template de chat |
19+| `kronk` | `/models`, puis `/v1/kronk/models/<id>` | la liste des modèles, l'architecture, la fenêtre avec laquelle le modèle est configuré (celle d'entraînement à défaut), l'échantillonnage écrit dans le GGUF, le template de chat ; pour un profil, son propre échantillonnage, qui l'emporte sur toute autre source : `mm` envoie son échantillonnage à chaque requête et une valeur de la requête remplace celle du profil, donc sans cette copie les valeurs du profil ne s'appliqueraient jamais |
19 | `dmr` | `/models` | la liste des modèles seulement |20 | `dmr` | `/models` | la liste des modèles seulement |
20 | `openai` | `/models` (avec la clé) | la liste des modèles seulement |21 | `openai` | `/models` (avec la clé) | la liste des modèles seulement |
21 22
@@ -106,7 +107,7 @@ L'assistant n'écrase un fichier existant que si vous l'acceptez, et jamais avec
106 107
107 ## Limites connues108 ## Limites connues
108 109
109-- Les vérifications en direct n'ont été faites qu'avec llmman (Qwen3.5 4B, gemma-4-E4B). Ollama, Docker Model Runner et llama-server ont été testés contre de faux serveurs enregistrés dans les tests. L'API OpenAI elle-même n'a pas été essayée ; ses contraintes viennent de sa référence.110+- Les vérifications en direct ont été faites avec llmman (Qwen3.5 4B, gemma-4-E4B) et Kronk 1.33.2 (`mm init --provider kronk … --check`, Qwopus3.5 4B Coder, 2026-11-11). Ollama, Docker Model Runner et llama-server ont été testés contre de faux serveurs enregistrés dans les tests. L'API OpenAI elle-même n'a pas été essayée ; ses contraintes viennent de sa référence.
110 - Une valeur `[card]` a été recopiée, pas mesurée dans ce dépôt.111 - Une valeur `[card]` a été recopiée, pas mesurée dans ce dépôt.
111 - Les familles sont reconnues sur des mots de l'identifiant du modèle : un modèle renommé par son éditeur peut tomber dans `other`.112 - Les familles sont reconnues sur des mots de l'identifiant du modèle : un modèle renommé par son éditeur peut tomber dans `other`.
112 113
modified docs/fr/explanation/providers.md +4 -2
@@ -2,7 +2,7 @@
22
33 ## De quoi s'agit-il ?
44
5-`mm` dialogue avec un serveur de modèle local via l'API chat-completions d'OpenAI. Quatre serveurs sont pris en charge d'origine : Docker Model Runner, `llama-server` de llama.cpp, llmman et Ollama, sous les noms `dmr`, `llamacpp`, `llmman` et `ollama`. Une cinquième entrée, `openai` (ticket 118), prend tout autre serveur qui parle le protocole — l'API d'OpenAI par défaut, vLLM, LM Studio, une passerelle — et ne suppose rien au-delà : c'est la même entrée faite de données, avec tous les champs propres à un serveur laissés vides, et une sonde qui envoie la clé. Le paquet moteur était autrefois lié à Docker Model Runner ; l'abstraction de fournisseur est ce qui l'a généralisé.
5+`mm` dialogue avec un serveur de modèle local via l'API chat-completions d'OpenAI. Cinq serveurs sont pris en charge d'origine : Docker Model Runner, `llama-server` de llama.cpp, llmman, Ollama et Kronk, sous les noms `dmr`, `llamacpp`, `llmman`, `ollama` et `kronk`. Une autre entrée, `openai` (ticket 118), prend tout autre serveur qui parle le protocole — l'API d'OpenAI par défaut, vLLM, LM Studio, une passerelle — et ne suppose rien au-delà : c'est la même entrée faite de données, avec tous les champs propres à un serveur laissés vides, et une sonde qui envoie la clé. Le paquet moteur était autrefois lié à Docker Model Runner ; l'abstraction de fournisseur est ce qui l'a généralisé.
66
77 ## Pourquoi c'est conçu ainsi
88
@@ -20,6 +20,8 @@
2020
2121 **Ollama est une quatrième entrée, et montre pourquoi chaque serveur garde la sienne.** Le `/v1` d'Ollama ressemble aux autres, mais, mesuré sur Ollama 0.35.0, il diffère sur trois points. Le raisonnement arrive dans `delta.reasoning`, que le tap lit déjà. Aucun avancement de lecture du prompt ne revient, donc `return_progress` n'est pas envoyé. Et `chat_template_kwargs` est ignoré : le résumé d'une compaction a raisonné jusqu'à sa limite de tokens et est revenu vide ; c'est `reasoning_effort: "none"` qu'Ollama respecte, et avec lui la même compaction a réussi. Voilà pourquoi la façon de couper le raisonnement est un champ par fournisseur, et non une conséquence de « est llama.cpp ». `mm` ne s'en sert que pour les résumés ; pour chaque tour, l'utilisateur écrit la même clé propre au serveur dans `sampling`, qui transmet depuis Genkit 1.12 les clés que l'API OpenAI ne connaît pas (voir [Couper le raisonnement d'un modèle](../how-to/turn-reasoning-off.md)). Comme llmman, Ollama rapporte la fenêtre servie sur `/api/ps` une fois le modèle chargé (les deux partagent cette lecture) ; contrairement à llmman, il ne télécharge pas un modèle absent, d'où l'avertissement de la sonde qui dit `ollama pull`.
2222
23+**Kronk est une cinquième entrée, avec ses propres routes à côté de celles d'OpenAI.** [Kronk](https://www.kronkai.com) (`kronk server start`, port 11435, ticket 75) est un serveur Go qui embarque llama.cpp directement, sans processus `llama-server`. Mesuré sur Kronk 1.33.2, il se comporte comme llama.cpp là où cela compte pour `mm` : le raisonnement arrive dans `reasoning_content`, et `chat_template_kwargs` le coupe, alors que `reasoning_effort: "none"` est ignoré. Comme Ollama, il n'envoie pas d'avancement de lecture du prompt. La fenêtre, le téléchargement et le déchargement ont leurs propres routes sous `/v1/kronk/…`. `GET /v1/kronk/models/<id>` donne la fenêtre avec laquelle le modèle est configuré, chargé ou non : elle est donc connue dès le démarrage. L'id contient un `/` (`provider/modelID`), il voyage donc échappé : un `/` brut coupe le chemin. Le téléchargement, `POST /v1/kronk/models/pull`, répond 200 même quand il échoue : seul le dernier enregistrement du flux (`"downloaded": true`, ou la raison) dit comment il s'est terminé. Un modèle installé se charge à la première requête ; un modèle absent donne un 404, d'où l'avertissement `kronk model pull` de la sonde. Kronk n'écoute que sur la boucle locale : le repli depuis une sandbox ne marche qu'une fois Kronk lancé sur l'hôte avec `--api-host 0.0.0.0:11435`. Kronk liste aussi des profils de configuration (`provider/modelID/PROFIL`, les mêmes poids avec leurs propres réglages dans `model_config.yaml`) ; `mm` les traite comme des modèles, et la sonde lit la fenêtre propre au profil. L'échantillonnage d'un profil ne fait que compléter ce que la requête ne donne pas : le `sampling` de `mm` l'emporte, mesuré avec `temperature` et `max_tokens`.
24+
2325 **Les erreurs sont réécrites dans les mots du serveur.** Une pile de messages imbriqués est du bruit ; « tool calls need llama-server started with --jinja » est tout le diagnostic. Le fournisseur reconnaît les refus de connexion, l'option `--jinja` manquante, les 401/403, les 404 avec une indication de pull ou d'alias, les 429 et les 5xx. Les erreurs inconnues reviennent inchangées : un message brut vaut mieux qu'une mauvaise piste.
2426
2527 **Un flux que le serveur interrompt est relancé, et ses mots sont gardés.** llama.cpp signale un échec au milieu d'une réponse diffusée par un événement SSE `error:` (le statut 200 est déjà parti). openai-go ne lit que les lignes `data:` : le message disparaissait derrière `unexpected end of JSON input`. Le tap qui lit déjà le flux pour le raisonnement récupère aussi ces événements, et l'appel au modèle qui a échoué est relancé une fois — cet appel seulement : les outils déjà exécutés dans le tour ne sont pas rejoués, et le texte déjà diffusé par la tentative interrompue n'est pas retiré. S'il échoue encore, l'erreur donne le message du serveur (`the model server reported an error while generating: Failed to parse input at pos …`), ou indique que la connexion a été coupée. Dans les deux cas l'historique est conservé : demander de continuer reprend le travail.
@@ -38,5 +40,5 @@
3840
3941 - Les deux fichiers de configuration qui paramètrent chaque fournisseur sont listés dans la [référence de la configuration](../reference/configuration.md).
4042 - La fenêtre apprise par la sonde est ce contre quoi la [compression du contexte](context-compression.md) mesure.
41-- Changer de serveur pour une exécution est couvert dans [comment faire tourner sur llama.cpp](../how-to/run-with-llama-cpp.md) [comment faire tourner sur llmman](../how-to/run-with-llmman.md) et [comment faire tourner sur Ollama](../how-to/run-with-ollama.md).
43+- Changer de serveur pour une exécution est couvert dans [comment faire tourner sur llama.cpp](../how-to/run-with-llama-cpp.md) [comment faire tourner sur llmman](../how-to/run-with-llmman.md) [comment faire tourner sur Ollama](../how-to/run-with-ollama.md) et [comment faire tourner sur Kronk](../how-to/run-with-kronk.md).
4244 - Vérifier l'ordre des outils et mesurer le gain de cache sur votre propre serveur : [vérifier que les outils partent dans un ordre stable](../how-to/verify-tool-order.md).
@@ -2,7 +2,7 @@
2 2
3 ## De quoi s'agit-il ?3 ## De quoi s'agit-il ?
4 4
5-`mm` dialogue avec un serveur de modèle local via l'API chat-completions d'OpenAI. Quatre serveurs sont pris en charge d'origine : Docker Model Runner, `llama-server` de llama.cpp, llmman et Ollama, sous les noms `dmr`, `llamacpp`, `llmman` et `ollama`. Une cinquième entrée, `openai` (ticket 118), prend tout autre serveur qui parle le protocole — l'API d'OpenAI par défaut, vLLM, LM Studio, une passerelle — et ne suppose rien au-delà : c'est la même entrée faite de données, avec tous les champs propres à un serveur laissés vides, et une sonde qui envoie la clé. Le paquet moteur était autrefois lié à Docker Model Runner ; l'abstraction de fournisseur est ce qui l'a généralisé.5+`mm` dialogue avec un serveur de modèle local via l'API chat-completions d'OpenAI. Cinq serveurs sont pris en charge d'origine : Docker Model Runner, `llama-server` de llama.cpp, llmman, Ollama et Kronk, sous les noms `dmr`, `llamacpp`, `llmman`, `ollama` et `kronk`. Une autre entrée, `openai` (ticket 118), prend tout autre serveur qui parle le protocole — l'API d'OpenAI par défaut, vLLM, LM Studio, une passerelle — et ne suppose rien au-delà : c'est la même entrée faite de données, avec tous les champs propres à un serveur laissés vides, et une sonde qui envoie la clé. Le paquet moteur était autrefois lié à Docker Model Runner ; l'abstraction de fournisseur est ce qui l'a généralisé.
6 6
7 ## Pourquoi c'est conçu ainsi7 ## Pourquoi c'est conçu ainsi
8 8
@@ -20,6 +20,8 @@
20 20
21 **Ollama est une quatrième entrée, et montre pourquoi chaque serveur garde la sienne.** Le `/v1` d'Ollama ressemble aux autres, mais, mesuré sur Ollama 0.35.0, il diffère sur trois points. Le raisonnement arrive dans `delta.reasoning`, que le tap lit déjà. Aucun avancement de lecture du prompt ne revient, donc `return_progress` n'est pas envoyé. Et `chat_template_kwargs` est ignoré : le résumé d'une compaction a raisonné jusqu'à sa limite de tokens et est revenu vide ; c'est `reasoning_effort: "none"` qu'Ollama respecte, et avec lui la même compaction a réussi. Voilà pourquoi la façon de couper le raisonnement est un champ par fournisseur, et non une conséquence de « est llama.cpp ». `mm` ne s'en sert que pour les résumés ; pour chaque tour, l'utilisateur écrit la même clé propre au serveur dans `sampling`, qui transmet depuis Genkit 1.12 les clés que l'API OpenAI ne connaît pas (voir [Couper le raisonnement d'un modèle](../how-to/turn-reasoning-off.md)). Comme llmman, Ollama rapporte la fenêtre servie sur `/api/ps` une fois le modèle chargé (les deux partagent cette lecture) ; contrairement à llmman, il ne télécharge pas un modèle absent, d'où l'avertissement de la sonde qui dit `ollama pull`.21 **Ollama est une quatrième entrée, et montre pourquoi chaque serveur garde la sienne.** Le `/v1` d'Ollama ressemble aux autres, mais, mesuré sur Ollama 0.35.0, il diffère sur trois points. Le raisonnement arrive dans `delta.reasoning`, que le tap lit déjà. Aucun avancement de lecture du prompt ne revient, donc `return_progress` n'est pas envoyé. Et `chat_template_kwargs` est ignoré : le résumé d'une compaction a raisonné jusqu'à sa limite de tokens et est revenu vide ; c'est `reasoning_effort: "none"` qu'Ollama respecte, et avec lui la même compaction a réussi. Voilà pourquoi la façon de couper le raisonnement est un champ par fournisseur, et non une conséquence de « est llama.cpp ». `mm` ne s'en sert que pour les résumés ; pour chaque tour, l'utilisateur écrit la même clé propre au serveur dans `sampling`, qui transmet depuis Genkit 1.12 les clés que l'API OpenAI ne connaît pas (voir [Couper le raisonnement d'un modèle](../how-to/turn-reasoning-off.md)). Comme llmman, Ollama rapporte la fenêtre servie sur `/api/ps` une fois le modèle chargé (les deux partagent cette lecture) ; contrairement à llmman, il ne télécharge pas un modèle absent, d'où l'avertissement de la sonde qui dit `ollama pull`.
22 22
23+**Kronk est une cinquième entrée, avec ses propres routes à côté de celles d'OpenAI.** [Kronk](https://www.kronkai.com) (`kronk server start`, port 11435, ticket 75) est un serveur Go qui embarque llama.cpp directement, sans processus `llama-server`. Mesuré sur Kronk 1.33.2, il se comporte comme llama.cpp là où cela compte pour `mm` : le raisonnement arrive dans `reasoning_content`, et `chat_template_kwargs` le coupe, alors que `reasoning_effort: "none"` est ignoré. Comme Ollama, il n'envoie pas d'avancement de lecture du prompt. La fenêtre, le téléchargement et le déchargement ont leurs propres routes sous `/v1/kronk/…`. `GET /v1/kronk/models/<id>` donne la fenêtre avec laquelle le modèle est configuré, chargé ou non : elle est donc connue dès le démarrage. L'id contient un `/` (`provider/modelID`), il voyage donc échappé : un `/` brut coupe le chemin. Le téléchargement, `POST /v1/kronk/models/pull`, répond 200 même quand il échoue : seul le dernier enregistrement du flux (`"downloaded": true`, ou la raison) dit comment il s'est terminé. Un modèle installé se charge à la première requête ; un modèle absent donne un 404, d'où l'avertissement `kronk model pull` de la sonde. Kronk n'écoute que sur la boucle locale : le repli depuis une sandbox ne marche qu'une fois Kronk lancé sur l'hôte avec `--api-host 0.0.0.0:11435`. Kronk liste aussi des profils de configuration (`provider/modelID/PROFIL`, les mêmes poids avec leurs propres réglages dans `model_config.yaml`) ; `mm` les traite comme des modèles, et la sonde lit la fenêtre propre au profil. L'échantillonnage d'un profil ne fait que compléter ce que la requête ne donne pas : le `sampling` de `mm` l'emporte, mesuré avec `temperature` et `max_tokens`.
24+
23 **Les erreurs sont réécrites dans les mots du serveur.** Une pile de messages imbriqués est du bruit ; « tool calls need llama-server started with --jinja » est tout le diagnostic. Le fournisseur reconnaît les refus de connexion, l'option `--jinja` manquante, les 401/403, les 404 avec une indication de pull ou d'alias, les 429 et les 5xx. Les erreurs inconnues reviennent inchangées : un message brut vaut mieux qu'une mauvaise piste.25 **Les erreurs sont réécrites dans les mots du serveur.** Une pile de messages imbriqués est du bruit ; « tool calls need llama-server started with --jinja » est tout le diagnostic. Le fournisseur reconnaît les refus de connexion, l'option `--jinja` manquante, les 401/403, les 404 avec une indication de pull ou d'alias, les 429 et les 5xx. Les erreurs inconnues reviennent inchangées : un message brut vaut mieux qu'une mauvaise piste.
24 26
25 **Un flux que le serveur interrompt est relancé, et ses mots sont gardés.** llama.cpp signale un échec au milieu d'une réponse diffusée par un événement SSE `error:` (le statut 200 est déjà parti). openai-go ne lit que les lignes `data:` : le message disparaissait derrière `unexpected end of JSON input`. Le tap qui lit déjà le flux pour le raisonnement récupère aussi ces événements, et l'appel au modèle qui a échoué est relancé une fois — cet appel seulement : les outils déjà exécutés dans le tour ne sont pas rejoués, et le texte déjà diffusé par la tentative interrompue n'est pas retiré. S'il échoue encore, l'erreur donne le message du serveur (`the model server reported an error while generating: Failed to parse input at pos …`), ou indique que la connexion a été coupée. Dans les deux cas l'historique est conservé : demander de continuer reprend le travail.27 **Un flux que le serveur interrompt est relancé, et ses mots sont gardés.** llama.cpp signale un échec au milieu d'une réponse diffusée par un événement SSE `error:` (le statut 200 est déjà parti). openai-go ne lit que les lignes `data:` : le message disparaissait derrière `unexpected end of JSON input`. Le tap qui lit déjà le flux pour le raisonnement récupère aussi ces événements, et l'appel au modèle qui a échoué est relancé une fois — cet appel seulement : les outils déjà exécutés dans le tour ne sont pas rejoués, et le texte déjà diffusé par la tentative interrompue n'est pas retiré. S'il échoue encore, l'erreur donne le message du serveur (`the model server reported an error while generating: Failed to parse input at pos …`), ou indique que la connexion a été coupée. Dans les deux cas l'historique est conservé : demander de continuer reprend le travail.
@@ -38,5 +40,5 @@
38 40
39 - Les deux fichiers de configuration qui paramètrent chaque fournisseur sont listés dans la [référence de la configuration](../reference/configuration.md).41 - Les deux fichiers de configuration qui paramètrent chaque fournisseur sont listés dans la [référence de la configuration](../reference/configuration.md).
40 - La fenêtre apprise par la sonde est ce contre quoi la [compression du contexte](context-compression.md) mesure.42 - La fenêtre apprise par la sonde est ce contre quoi la [compression du contexte](context-compression.md) mesure.
41-- Changer de serveur pour une exécution est couvert dans [comment faire tourner sur llama.cpp](../how-to/run-with-llama-cpp.md) [comment faire tourner sur llmman](../how-to/run-with-llmman.md) et [comment faire tourner sur Ollama](../how-to/run-with-ollama.md).43+- Changer de serveur pour une exécution est couvert dans [comment faire tourner sur llama.cpp](../how-to/run-with-llama-cpp.md) [comment faire tourner sur llmman](../how-to/run-with-llmman.md) [comment faire tourner sur Ollama](../how-to/run-with-ollama.md) et [comment faire tourner sur Kronk](../how-to/run-with-kronk.md).
42 - Vérifier l'ordre des outils et mesurer le gain de cache sur votre propre serveur : [vérifier que les outils partent dans un ordre stable](../how-to/verify-tool-order.md).44 - Vérifier l'ordre des outils et mesurer le gain de cache sur votre propre serveur : [vérifier que les outils partent dans un ordre stable](../how-to/verify-tool-order.md).
modified docs/fr/explanation/thinking-models.md +1 -1
@@ -48,6 +48,6 @@ Trois propriétés valent partout :
4848
4949 ## Ce que ce n'est pas
5050
51-Ce n'est pas un réglage d'« effort de raisonnement » : `mm` n'a pas de clé à lui pour cela, et affiche ce qui arrive. On peut tout de même demander au serveur de ne pas raisonner, via `sampling`, dont les clés inconnues de l'API OpenAI sont transmises telles quelles (depuis Genkit 1.12) : `chat_template_kwargs: {enable_thinking: false}` pour un serveur llama.cpp (`dmr`, `llamacpp`, `llmman`), `reasoning_effort: none` pour Ollama. Mesuré avec Qwen3.5 4B sur Docker Model Runner : la même réponse en moins d'une seconde, sans les quatre secondes de raisonnement. `mm` le fait de lui-même, serveur par serveur, pour le seul résumé d'une compaction. Voir [Couper le raisonnement d'un modèle](../how-to/turn-reasoning-off.md).
51+Ce n'est pas un réglage d'« effort de raisonnement » : `mm` n'a pas de clé à lui pour cela, et affiche ce qui arrive. On peut tout de même demander au serveur de ne pas raisonner, via `sampling`, dont les clés inconnues de l'API OpenAI sont transmises telles quelles (depuis Genkit 1.12) : `chat_template_kwargs: {enable_thinking: false}` pour un serveur llama.cpp (`dmr`, `llamacpp`, `llmman`) et pour Kronk, `reasoning_effort: none` pour Ollama. Mesuré avec Qwen3.5 4B sur Docker Model Runner : la même réponse en moins d'une seconde, sans les quatre secondes de raisonnement. `mm` le fait de lui-même, serveur par serveur, pour le seul résumé d'une compaction. Voir [Couper le raisonnement d'un modèle](../how-to/turn-reasoning-off.md).
5252
5353 Ce n'est pas un analyseur. Certains modèles encadrent plutôt leur raisonnement de balises `<think>` à l'intérieur de `content`. Celles-là arrivent comme du texte de réponse ordinaire et `mm` les affiche comme tel — les deux formes demanderaient des traitements différents, et seul le champ est géré aujourd'hui.
@@ -48,6 +48,6 @@ Trois propriétés valent partout :
48 48
49 ## Ce que ce n'est pas49 ## Ce que ce n'est pas
50 50
51-Ce n'est pas un réglage d'« effort de raisonnement » : `mm` n'a pas de clé à lui pour cela, et affiche ce qui arrive. On peut tout de même demander au serveur de ne pas raisonner, via `sampling`, dont les clés inconnues de l'API OpenAI sont transmises telles quelles (depuis Genkit 1.12) : `chat_template_kwargs: {enable_thinking: false}` pour un serveur llama.cpp (`dmr`, `llamacpp`, `llmman`), `reasoning_effort: none` pour Ollama. Mesuré avec Qwen3.5 4B sur Docker Model Runner : la même réponse en moins d'une seconde, sans les quatre secondes de raisonnement. `mm` le fait de lui-même, serveur par serveur, pour le seul résumé d'une compaction. Voir [Couper le raisonnement d'un modèle](../how-to/turn-reasoning-off.md).51+Ce n'est pas un réglage d'« effort de raisonnement » : `mm` n'a pas de clé à lui pour cela, et affiche ce qui arrive. On peut tout de même demander au serveur de ne pas raisonner, via `sampling`, dont les clés inconnues de l'API OpenAI sont transmises telles quelles (depuis Genkit 1.12) : `chat_template_kwargs: {enable_thinking: false}` pour un serveur llama.cpp (`dmr`, `llamacpp`, `llmman`) et pour Kronk, `reasoning_effort: none` pour Ollama. Mesuré avec Qwen3.5 4B sur Docker Model Runner : la même réponse en moins d'une seconde, sans les quatre secondes de raisonnement. `mm` le fait de lui-même, serveur par serveur, pour le seul résumé d'une compaction. Voir [Couper le raisonnement d'un modèle](../how-to/turn-reasoning-off.md).
52 52
53 Ce n'est pas un analyseur. Certains modèles encadrent plutôt leur raisonnement de balises `<think>` à l'intérieur de `content`. Celles-là arrivent comme du texte de réponse ordinaire et `mm` les affiche comme tel — les deux formes demanderaient des traitements différents, et seul le champ est géré aujourd'hui.53 Ce n'est pas un analyseur. Certains modèles encadrent plutôt leur raisonnement de balises `<think>` à l'intérieur de `content`. Celles-là arrivent comme du texte de réponse ordinaire et `mm` les affiche comme tel — les deux formes demanderaient des traitements différents, et seul le champ est géré aujourd'hui.
modified docs/fr/how-to/run-in-a-docker-sandbox.md +1 -0
@@ -165,6 +165,7 @@ sbx env rm --force --env-arg PROJECT_DIR="$PWD"
165165 Dans un sandbox, `localhost` est le sandbox lui-même. Votre serveur de modèles continue de tourner sur votre machine et se joint par `host.docker.internal`, que le proxy du sandbox redirige vers votre machine.
166166
167167 - La politique réseau du kit `mini-me` autorise **`host.docker.internal:12434`** (Docker Model Runner), **`:11434`** (Ollama) et **`:17434`** (llmman). `agent.dmr.yaml` se replie déjà sur `http://host.docker.internal:12434/engines/v1` ; les configurations livrées dans `demos/use-with-sbx/` utilisent llmman (`fallback: http://host.docker.internal:17434/v1`).
168+- Kronk (`:11435`) n'est pas dans cette liste : autorisez `localhost:11435` dans la politique, et lancez Kronk sur l'hôte avec `--api-host 0.0.0.0:11435` — sinon il n'écoute que sur la boucle locale. Voir [Faire tourner mini-me sur Kronk](run-with-kronk.md).
168169 - Pour un autre port — le `8080` de llama-server, par exemple — ajoutez-le à la capacité `network-policy` du kit, ou autorisez-le pour un seul sandbox :
169170
170171 ```bash
@@ -165,6 +165,7 @@ sbx env rm --force --env-arg PROJECT_DIR="$PWD"
165 Dans un sandbox, `localhost` est le sandbox lui-même. Votre serveur de modèles continue de tourner sur votre machine et se joint par `host.docker.internal`, que le proxy du sandbox redirige vers votre machine.165 Dans un sandbox, `localhost` est le sandbox lui-même. Votre serveur de modèles continue de tourner sur votre machine et se joint par `host.docker.internal`, que le proxy du sandbox redirige vers votre machine.
166 166
167 - La politique réseau du kit `mini-me` autorise **`host.docker.internal:12434`** (Docker Model Runner), **`:11434`** (Ollama) et **`:17434`** (llmman). `agent.dmr.yaml` se replie déjà sur `http://host.docker.internal:12434/engines/v1` ; les configurations livrées dans `demos/use-with-sbx/` utilisent llmman (`fallback: http://host.docker.internal:17434/v1`).167 - La politique réseau du kit `mini-me` autorise **`host.docker.internal:12434`** (Docker Model Runner), **`:11434`** (Ollama) et **`:17434`** (llmman). `agent.dmr.yaml` se replie déjà sur `http://host.docker.internal:12434/engines/v1` ; les configurations livrées dans `demos/use-with-sbx/` utilisent llmman (`fallback: http://host.docker.internal:17434/v1`).
168+- Kronk (`:11435`) n'est pas dans cette liste : autorisez `localhost:11435` dans la politique, et lancez Kronk sur l'hôte avec `--api-host 0.0.0.0:11435` — sinon il n'écoute que sur la boucle locale. Voir [Faire tourner mini-me sur Kronk](run-with-kronk.md).
168 - Pour un autre port — le `8080` de llama-server, par exemple — ajoutez-le à la capacité `network-policy` du kit, ou autorisez-le pour un seul sandbox :169 - Pour un autre port — le `8080` de llama-server, par exemple — ajoutez-le à la capacité `network-policy` du kit, ou autorisez-le pour un seul sandbox :
169 170
170 ```bash171 ```bash
added docs/fr/how-to/run-with-kronk.md +88 -0
new file mode 100644
@@ -0,0 +1,88 @@
1+# Faire tourner mini-me sur Kronk
2+
3+Ce guide montre comment brancher `mm` sur [Kronk](https://www.kronkai.com), le serveur d'inférence Go d'Ardan Labs, via son point d'accès compatible OpenAI. Il suppose que Kronk est installé (`brew install ardanlabs/kronk/kronk`, ou `go install github.com/ardanlabs/kronk/cmd/kronk@latest`) et que vous savez construire `mm`.
4+
5+## Étapes
6+
7+1. Téléchargez le modèle. Kronk charge un modèle installé à la première requête, mais un modèle non installé donne une erreur `model not found` :
8+
9+ ```bash
10+ kronk model pull mradermacher/Qwopus3.5-4B-Coder.Q8_0
11+ ```
12+
13+2. Lancez le serveur. Il écoute sur `127.0.0.1:11435` :
14+
15+ ```bash
16+ kronk server start
17+ ```
18+
19+3. Lancez la configuration de démo depuis son répertoire (son `skillsDir` vaut `../../skills`) :
20+
21+ ```bash
22+ cd demos/kronk
23+ ../../mm -config agent.kronk.yaml
24+ ```
25+
26+4. Vérifiez la bannière. `provider: kronk` confirme le fournisseur, et `ctx: 131072 (/v1/kronk/models)` est la fenêtre avec laquelle Kronk sert le modèle, lue au démarrage, que le modèle soit chargé ou non.
27+
28+## Variantes
29+
30+- **Un autre modèle.** Mettez dans `model:` l'id exactement comme Kronk le liste sur `GET /v1/models` : `provider/modelID`. Kronk refuse un id sans son provider avec `must use provider/modelID`. **L'id du téléchargement n'est pas toujours celui que Kronk sert.** Par exemple Qwen3.5 2B, un modèle plus petit :
31+
32+ ```bash
33+ kronk model pull unsloth/Qwen3.5-2B-GGUF:Q4_K_M --local # --local : serveur arrêté
34+ curl -s localhost:11435/v1/models # le liste comme unsloth/Qwen3.5-2B-Q4_K_M
35+ ```
36+
37+ ```yaml
38+ model: unsloth/Qwen3.5-2B-Q4_K_M # pas unsloth/Qwen3.5-2B-GGUF:Q4_K_M, qui donne un 404
39+ ```
40+
41+ Mesuré sur Kronk 1.33.2 : `ctx: 131072 (/v1/kronk/models)`, et les appels d'outils marchent.
42+- **Un profil Kronk.** Kronk liste aussi des profils de configuration, `provider/modelID/PROFIL`, par exemple `mradermacher/Qwopus3.5-4B-Coder.Q8_0/AGENT`. Un profil, c'est le même fichier GGUF, servi avec les réglages de cette clé dans `~/.kronk/models/model_config.yaml`. Rien n'est téléchargé deux fois. Les profils `AGENT` viennent du `model_config.yaml` livré avec Kronk ; vous pouvez y ajouter les vôtres, puis relancer Kronk. Celui de Qwopus dit :
43+
44+ ```yaml
45+ mradermacher/Qwopus3.5-4B-Coder.Q8_0/AGENT:
46+ context-window: 65536
47+ sampling-parameters:
48+ max_tokens: 8192
49+ temperature: 1
50+ top_k: 20
51+ top_p: 0.95
52+ ```
53+
54+ Un profil est un `model:` valide. Ce qu'il change pour `mm` :
55+
56+ - **La fenêtre : oui.** C'est celle du profil, ici 65536 au lieu des 131072 que Kronk choisit pour le modèle simple. La bannière l'affiche (`ctx: 65536 (/v1/kronk/models)`), et la compaction se déclenche à 75 % de cette fenêtre. Une fenêtre plus petite prend aussi moins de mémoire sur le serveur.
57+ - **L'échantillonnage : seulement pour les clés que `mm` n'envoie pas.** Une valeur de la requête l'emporte sur celle du profil. Mesuré sur Kronk 1.33.2 : `temperature: 0` dans la requête a donné trois fois de suite la même réponse malgré le `temperature: 1` du profil, et `max_tokens: 5` a coupé la réponse à 5 tokens malgré les 8192 du profil. `mm` envoie toujours `temperature`, `top_p` et `max_tokens` : ses valeurs par défaut, fusionnées avec le `sampling` du fichier (voir [`sampling`](../reference/configuration.md)). Les `temperature`, `top_p` et `max_tokens` du profil ne s'appliquent donc jamais, et ses autres clés seulement quand `sampling` ne les donne pas, ici `top_k: 20`. C'est vrai même pour un fichier sans bloc `sampling:` : `mm` envoie alors `temperature: 0`, `top_p: 0.9`, `max_tokens: 4096` (vérifié dans une trace). Pour avoir les valeurs d'un profil, écrivez-les dans `sampling` ; `mm init` le fait pour vous (ci-dessous).
58+ - **Le raisonnement : non.** `enable_thinking` et `chat_template_kwargs` dans `sampling` valent aussi par requête.
59+
60+ `mm init` liste les profils avec une ligne qui dit ce qu'ils sont, et la fenêtre les affiche comme `Qwopus3.5-4B-Coder.Q8…/AGENT`. Quand vous choisissez un profil, `mm init` lit son échantillonnage auprès de Kronk et le recopie dans le `sampling` du fichier. Les valeurs recopiées sont `temperature`, `top_p`, `top_k`, `min_p`, et `max_tokens` quand le profil le plafonne. Elles remplacent celles de la table des familles et du GGUF, chacune marquée `[server] Kronk profile AGENT` :
61+
62+ ```yaml
63+ sampling:
64+ # [server] Kronk profile AGENT (model_config.yaml), read from the server:
65+ # mm's values replace a profile's, so they are copied here
66+ temperature: 1.0
67+ top_p: 0.95
68+ top_k: 20
69+ min_p: 0.0
70+ max_tokens: 8192
71+ ```
72+- **Écrire le fichier avec l'assistant.** `mm init --provider kronk` liste les modèles de Kronk et lit auprès de Kronk la fenêtre, l'échantillonnage et le chat template du modèle choisi. Voir [Créer un agent](create-an-agent.md).
73+- **Laisser mm télécharger le modèle.** Avec `autoPull: true`, mm télécharge au démarrage un modèle que Kronk ne liste pas, via `POST /v1/kronk/models/pull`, et affiche la progression. Voir [`autoPull`](../reference/configuration.md).
74+- **Depuis une sandbox Docker.** `kronk server start` n'écoute que sur la boucle locale, qu'une sandbox ne peut pas atteindre. Sur l'hôte, lancez-le avec `kronk server start --api-host 0.0.0.0:11435`, et autorisez `localhost:11435` dans la politique réseau de la sandbox. Quand aucun `baseUrl` ne répond, `mm` se replie alors tout seul sur `http://host.docker.internal:11435/v1`.
75+- **Une fenêtre plus grande ou plus petite.** La fenêtre est celle de Kronk, par modèle : réglez `context-window` pour le modèle dans `~/.kronk/models/model_config.yaml` et relancez Kronk. `mm` lit la nouvelle valeur au démarrage.
76+- **Un Kronk protégé.** Dans les modes `authenticated` ou `full-protected`, exportez un jeton dans `KRONK_TOKEN` (ou nommez une autre variable dans `apiKeyEnv`). Un jeton refusé donne un avertissement au démarrage qui nomme la variable.
77+- **Libérer la mémoire.** `/unload` et l'outil `unload_model` déchargent le modèle via `POST /v1/kronk/models/unload`. Voir [Libérer la mémoire d'un modèle](unload-a-model.md).
78+- **Garder votre propre `agent.yaml`.** `AGENT_PROVIDER=kronk ./mm` marche tant que le fichier laisse `baseUrl` vide.
79+
80+## Ce qui diffère des autres serveurs
81+
82+- Pas de ligne `Reading the prompt …` : Kronk n'envoie pas d'avancement pendant qu'il lit, donc `firstTokenTimeout` doit couvrir tout le prompt et le chargement du modèle.
83+- Le raisonnement se coupe avec `chat_template_kwargs: {enable_thinking: false}`, comme avec llama.cpp ; Kronk ignore `reasoning_effort: none`. Le résumé d'une compaction le fait tout seul. Voir [Couper le raisonnement d'un modèle](turn-reasoning-off.md).
84+
85+## Voir aussi
86+
87+- [Fournisseurs : une implémentation par protocole](../explanation/providers.md) — ce qui a été mesuré sur Kronk.
88+- [Référence de configuration](../reference/configuration.md) — valeurs par défaut des fournisseurs.
new file mode 100644
@@ -0,0 +1,88 @@
1+# Faire tourner mini-me sur Kronk
2+
3+Ce guide montre comment brancher `mm` sur [Kronk](https://www.kronkai.com), le serveur d'inférence Go d'Ardan Labs, via son point d'accès compatible OpenAI. Il suppose que Kronk est installé (`brew install ardanlabs/kronk/kronk`, ou `go install github.com/ardanlabs/kronk/cmd/kronk@latest`) et que vous savez construire `mm`.
4+
5+## Étapes
6+
7+1. Téléchargez le modèle. Kronk charge un modèle installé à la première requête, mais un modèle non installé donne une erreur `model not found` :
8+
9+ ```bash
10+ kronk model pull mradermacher/Qwopus3.5-4B-Coder.Q8_0
11+ ```
12+
13+2. Lancez le serveur. Il écoute sur `127.0.0.1:11435` :
14+
15+ ```bash
16+ kronk server start
17+ ```
18+
19+3. Lancez la configuration de démo depuis son répertoire (son `skillsDir` vaut `../../skills`) :
20+
21+ ```bash
22+ cd demos/kronk
23+ ../../mm -config agent.kronk.yaml
24+ ```
25+
26+4. Vérifiez la bannière. `provider: kronk` confirme le fournisseur, et `ctx: 131072 (/v1/kronk/models)` est la fenêtre avec laquelle Kronk sert le modèle, lue au démarrage, que le modèle soit chargé ou non.
27+
28+## Variantes
29+
30+- **Un autre modèle.** Mettez dans `model:` l'id exactement comme Kronk le liste sur `GET /v1/models` : `provider/modelID`. Kronk refuse un id sans son provider avec `must use provider/modelID`. **L'id du téléchargement n'est pas toujours celui que Kronk sert.** Par exemple Qwen3.5 2B, un modèle plus petit :
31+
32+ ```bash
33+ kronk model pull unsloth/Qwen3.5-2B-GGUF:Q4_K_M --local # --local : serveur arrêté
34+ curl -s localhost:11435/v1/models # le liste comme unsloth/Qwen3.5-2B-Q4_K_M
35+ ```
36+
37+ ```yaml
38+ model: unsloth/Qwen3.5-2B-Q4_K_M # pas unsloth/Qwen3.5-2B-GGUF:Q4_K_M, qui donne un 404
39+ ```
40+
41+ Mesuré sur Kronk 1.33.2 : `ctx: 131072 (/v1/kronk/models)`, et les appels d'outils marchent.
42+- **Un profil Kronk.** Kronk liste aussi des profils de configuration, `provider/modelID/PROFIL`, par exemple `mradermacher/Qwopus3.5-4B-Coder.Q8_0/AGENT`. Un profil, c'est le même fichier GGUF, servi avec les réglages de cette clé dans `~/.kronk/models/model_config.yaml`. Rien n'est téléchargé deux fois. Les profils `AGENT` viennent du `model_config.yaml` livré avec Kronk ; vous pouvez y ajouter les vôtres, puis relancer Kronk. Celui de Qwopus dit :
43+
44+ ```yaml
45+ mradermacher/Qwopus3.5-4B-Coder.Q8_0/AGENT:
46+ context-window: 65536
47+ sampling-parameters:
48+ max_tokens: 8192
49+ temperature: 1
50+ top_k: 20
51+ top_p: 0.95
52+ ```
53+
54+ Un profil est un `model:` valide. Ce qu'il change pour `mm` :
55+
56+ - **La fenêtre : oui.** C'est celle du profil, ici 65536 au lieu des 131072 que Kronk choisit pour le modèle simple. La bannière l'affiche (`ctx: 65536 (/v1/kronk/models)`), et la compaction se déclenche à 75 % de cette fenêtre. Une fenêtre plus petite prend aussi moins de mémoire sur le serveur.
57+ - **L'échantillonnage : seulement pour les clés que `mm` n'envoie pas.** Une valeur de la requête l'emporte sur celle du profil. Mesuré sur Kronk 1.33.2 : `temperature: 0` dans la requête a donné trois fois de suite la même réponse malgré le `temperature: 1` du profil, et `max_tokens: 5` a coupé la réponse à 5 tokens malgré les 8192 du profil. `mm` envoie toujours `temperature`, `top_p` et `max_tokens` : ses valeurs par défaut, fusionnées avec le `sampling` du fichier (voir [`sampling`](../reference/configuration.md)). Les `temperature`, `top_p` et `max_tokens` du profil ne s'appliquent donc jamais, et ses autres clés seulement quand `sampling` ne les donne pas, ici `top_k: 20`. C'est vrai même pour un fichier sans bloc `sampling:` : `mm` envoie alors `temperature: 0`, `top_p: 0.9`, `max_tokens: 4096` (vérifié dans une trace). Pour avoir les valeurs d'un profil, écrivez-les dans `sampling` ; `mm init` le fait pour vous (ci-dessous).
58+ - **Le raisonnement : non.** `enable_thinking` et `chat_template_kwargs` dans `sampling` valent aussi par requête.
59+
60+ `mm init` liste les profils avec une ligne qui dit ce qu'ils sont, et la fenêtre les affiche comme `Qwopus3.5-4B-Coder.Q8…/AGENT`. Quand vous choisissez un profil, `mm init` lit son échantillonnage auprès de Kronk et le recopie dans le `sampling` du fichier. Les valeurs recopiées sont `temperature`, `top_p`, `top_k`, `min_p`, et `max_tokens` quand le profil le plafonne. Elles remplacent celles de la table des familles et du GGUF, chacune marquée `[server] Kronk profile AGENT` :
61+
62+ ```yaml
63+ sampling:
64+ # [server] Kronk profile AGENT (model_config.yaml), read from the server:
65+ # mm's values replace a profile's, so they are copied here
66+ temperature: 1.0
67+ top_p: 0.95
68+ top_k: 20
69+ min_p: 0.0
70+ max_tokens: 8192
71+ ```
72+- **Écrire le fichier avec l'assistant.** `mm init --provider kronk` liste les modèles de Kronk et lit auprès de Kronk la fenêtre, l'échantillonnage et le chat template du modèle choisi. Voir [Créer un agent](create-an-agent.md).
73+- **Laisser mm télécharger le modèle.** Avec `autoPull: true`, mm télécharge au démarrage un modèle que Kronk ne liste pas, via `POST /v1/kronk/models/pull`, et affiche la progression. Voir [`autoPull`](../reference/configuration.md).
74+- **Depuis une sandbox Docker.** `kronk server start` n'écoute que sur la boucle locale, qu'une sandbox ne peut pas atteindre. Sur l'hôte, lancez-le avec `kronk server start --api-host 0.0.0.0:11435`, et autorisez `localhost:11435` dans la politique réseau de la sandbox. Quand aucun `baseUrl` ne répond, `mm` se replie alors tout seul sur `http://host.docker.internal:11435/v1`.
75+- **Une fenêtre plus grande ou plus petite.** La fenêtre est celle de Kronk, par modèle : réglez `context-window` pour le modèle dans `~/.kronk/models/model_config.yaml` et relancez Kronk. `mm` lit la nouvelle valeur au démarrage.
76+- **Un Kronk protégé.** Dans les modes `authenticated` ou `full-protected`, exportez un jeton dans `KRONK_TOKEN` (ou nommez une autre variable dans `apiKeyEnv`). Un jeton refusé donne un avertissement au démarrage qui nomme la variable.
77+- **Libérer la mémoire.** `/unload` et l'outil `unload_model` déchargent le modèle via `POST /v1/kronk/models/unload`. Voir [Libérer la mémoire d'un modèle](unload-a-model.md).
78+- **Garder votre propre `agent.yaml`.** `AGENT_PROVIDER=kronk ./mm` marche tant que le fichier laisse `baseUrl` vide.
79+
80+## Ce qui diffère des autres serveurs
81+
82+- Pas de ligne `Reading the prompt …` : Kronk n'envoie pas d'avancement pendant qu'il lit, donc `firstTokenTimeout` doit couvrir tout le prompt et le chargement du modèle.
83+- Le raisonnement se coupe avec `chat_template_kwargs: {enable_thinking: false}`, comme avec llama.cpp ; Kronk ignore `reasoning_effort: none`. Le résumé d'une compaction le fait tout seul. Voir [Couper le raisonnement d'un modèle](turn-reasoning-off.md).
84+
85+## Voir aussi
86+
87+- [Fournisseurs : une implémentation par protocole](../explanation/providers.md) — ce qui a été mesuré sur Kronk.
88+- [Référence de configuration](../reference/configuration.md) — valeurs par défaut des fournisseurs.
modified docs/fr/how-to/turn-reasoning-off.md +2 -2
@@ -8,7 +8,7 @@ Un modèle à raisonnement (Qwen3.5, Qwen3.x, gpt-oss…) réfléchit avant de r
88
99 2. Ajoutez l'interrupteur propre au serveur sous `sampling`.
1010
11- Pour `dmr`, `llamacpp` ou `llmman` — tous trois llama.cpp en dessous :
11+ Pour `dmr`, `llamacpp`, `llmman` ou `kronk` — tous llama.cpp en dessous :
1212
1313 ```yaml
1414 sampling:
@@ -61,7 +61,7 @@ Un modèle à raisonnement (Qwen3.5, Qwen3.x, gpt-oss…) réfléchit avant de r
6161 - Avec `ollama`, `reasoning_effort` se place au premier niveau de `sampling`, pas sous `chat_template_kwargs` ; seule la valeur `none` y a été mesurée.
6262 - Les résumés de compaction ne le suivent pas — voir ci-dessous.
6363 - **Le couper pour une seule exécution.** Gardez deux fichiers de configuration, ou démarrez le serveur lui-même sans raisonnement (pour llama-server, voir son option `--reasoning-budget`).
64-- **Résumés de compaction.** Rien à faire : `mm` coupe le raisonnement pour le résumé de `/compact` et de la compression automatique, quoi que dise `sampling`. Avec `dmr`, `llamacpp` et `llmman`, il fixe `enable_thinking: false` dans `chat_template_kwargs`, garde vos autres variables de template, et ramène à `low` un `reasoning_effort` que vous y avez mis — le template de gpt-oss n'a pas d'« arrêt », donc un résumé produit par ce modèle raisonne encore un peu, et cela est décompté de `context.summaryMaxTokens`. Avec `ollama`, il envoie `reasoning_effort: none`.
64+- **Résumés de compaction.** Rien à faire : `mm` coupe le raisonnement pour le résumé de `/compact` et de la compression automatique, quoi que dise `sampling`. Avec `dmr`, `llamacpp`, `llmman` et `kronk`, il fixe `enable_thinking: false` dans `chat_template_kwargs`, garde vos autres variables de template, et ramène à `low` un `reasoning_effort` que vous y avez mis — le template de gpt-oss n'a pas d'« arrêt », donc un résumé produit par ce modèle raisonne encore un peu, et cela est décompté de `context.summaryMaxTokens`. Avec `ollama`, il envoie `reasoning_effort: none`.
6565
6666 ## Voir aussi
6767
@@ -8,7 +8,7 @@ Un modèle à raisonnement (Qwen3.5, Qwen3.x, gpt-oss…) réfléchit avant de r
8 8
9 2. Ajoutez l'interrupteur propre au serveur sous `sampling`.9 2. Ajoutez l'interrupteur propre au serveur sous `sampling`.
10 10
11- Pour `dmr`, `llamacpp` ou `llmman` — tous trois llama.cpp en dessous :11+ Pour `dmr`, `llamacpp`, `llmman` ou `kronk` — tous llama.cpp en dessous :
12 12
13 ```yaml13 ```yaml
14 sampling:14 sampling:
@@ -61,7 +61,7 @@ Un modèle à raisonnement (Qwen3.5, Qwen3.x, gpt-oss…) réfléchit avant de r
61 - Avec `ollama`, `reasoning_effort` se place au premier niveau de `sampling`, pas sous `chat_template_kwargs` ; seule la valeur `none` y a été mesurée.61 - Avec `ollama`, `reasoning_effort` se place au premier niveau de `sampling`, pas sous `chat_template_kwargs` ; seule la valeur `none` y a été mesurée.
62 - Les résumés de compaction ne le suivent pas — voir ci-dessous.62 - Les résumés de compaction ne le suivent pas — voir ci-dessous.
63 - **Le couper pour une seule exécution.** Gardez deux fichiers de configuration, ou démarrez le serveur lui-même sans raisonnement (pour llama-server, voir son option `--reasoning-budget`).63 - **Le couper pour une seule exécution.** Gardez deux fichiers de configuration, ou démarrez le serveur lui-même sans raisonnement (pour llama-server, voir son option `--reasoning-budget`).
64-- **Résumés de compaction.** Rien à faire : `mm` coupe le raisonnement pour le résumé de `/compact` et de la compression automatique, quoi que dise `sampling`. Avec `dmr`, `llamacpp` et `llmman`, il fixe `enable_thinking: false` dans `chat_template_kwargs`, garde vos autres variables de template, et ramène à `low` un `reasoning_effort` que vous y avez mis — le template de gpt-oss n'a pas d'« arrêt », donc un résumé produit par ce modèle raisonne encore un peu, et cela est décompté de `context.summaryMaxTokens`. Avec `ollama`, il envoie `reasoning_effort: none`.64+- **Résumés de compaction.** Rien à faire : `mm` coupe le raisonnement pour le résumé de `/compact` et de la compression automatique, quoi que dise `sampling`. Avec `dmr`, `llamacpp`, `llmman` et `kronk`, il fixe `enable_thinking: false` dans `chat_template_kwargs`, garde vos autres variables de template, et ramène à `low` un `reasoning_effort` que vous y avez mis — le template de gpt-oss n'a pas d'« arrêt », donc un résumé produit par ce modèle raisonne encore un peu, et cela est décompté de `context.summaryMaxTokens`. Avec `ollama`, il envoie `reasoning_effort: none`.
65 65
66 ## Voir aussi66 ## Voir aussi
67 67
modified docs/fr/how-to/unload-a-model.md +2 -2
@@ -1,6 +1,6 @@
11 # Libérer la mémoire d'un modèle
22
3-Ce guide montre comment décharger un modèle de la mémoire du serveur sans arrêter le serveur : à la main avec `/unload`, ou par l'agent lui-même avec son outil `unload_model` — typiquement pour libérer la mémoire du modèle d'un autre agent une fois sa réponse obtenue. Il suppose que mm tourne sur llmman, Ollama, ou llama-server en mode router. Le modèle se recharge tout seul la prochaine fois qu'il sert.
3+Ce guide montre comment décharger un modèle de la mémoire du serveur sans arrêter le serveur : à la main avec `/unload`, ou par l'agent lui-même avec son outil `unload_model` — typiquement pour libérer la mémoire du modèle d'un autre agent une fois sa réponse obtenue. Il suppose que mm tourne sur llmman, Ollama, Kronk, ou llama-server en mode router. Le modèle se recharge tout seul la prochaine fois qu'il sert.
44
55 ## Décharger à la main
66
@@ -22,7 +22,7 @@ Nommez un autre modèle servi par le même serveur pour décharger celui-là :
2222 /unload hf.co/unsloth/Qwen3.5-9B-GGUF:Q4_K_M
2323 ```
2424
25-Un modèle qui n'est pas chargé donne `🧹 <modèle> was not loaded: nothing to unload` ; rien n'est envoyé au serveur, donc rien n'est chargé non plus. Vérifiez ce qui est chargé avec `llmman ps`, `ollama ps`, ou `curl <serveur>/api/ps`.
25+Un modèle qui n'est pas chargé donne `🧹 <modèle> was not loaded: nothing to unload`, et rien n'est chargé : llmman et Ollama ne sont même pas interrogés, Kronk répond que le modèle n'est pas chargé. Vérifiez ce qui est chargé avec `llmman ps`, `ollama ps`, `curl <serveur>/api/ps`, ou, sur Kronk, `curl <serveur>/v1/kronk/models/ps`.
2626
2727 ## Laisser l'agent décharger un modèle
2828
@@ -1,6 +1,6 @@
1 # Libérer la mémoire d'un modèle1 # Libérer la mémoire d'un modèle
2 2
3-Ce guide montre comment décharger un modèle de la mémoire du serveur sans arrêter le serveur : à la main avec `/unload`, ou par l'agent lui-même avec son outil `unload_model` — typiquement pour libérer la mémoire du modèle d'un autre agent une fois sa réponse obtenue. Il suppose que mm tourne sur llmman, Ollama, ou llama-server en mode router. Le modèle se recharge tout seul la prochaine fois qu'il sert.3+Ce guide montre comment décharger un modèle de la mémoire du serveur sans arrêter le serveur : à la main avec `/unload`, ou par l'agent lui-même avec son outil `unload_model` — typiquement pour libérer la mémoire du modèle d'un autre agent une fois sa réponse obtenue. Il suppose que mm tourne sur llmman, Ollama, Kronk, ou llama-server en mode router. Le modèle se recharge tout seul la prochaine fois qu'il sert.
4 4
5 ## Décharger à la main5 ## Décharger à la main
6 6
@@ -22,7 +22,7 @@ Nommez un autre modèle servi par le même serveur pour décharger celui-là :
22 /unload hf.co/unsloth/Qwen3.5-9B-GGUF:Q4_K_M22 /unload hf.co/unsloth/Qwen3.5-9B-GGUF:Q4_K_M
23 ```23 ```
24 24
25-Un modèle qui n'est pas chargé donne `🧹 <modèle> was not loaded: nothing to unload` ; rien n'est envoyé au serveur, donc rien n'est chargé non plus. Vérifiez ce qui est chargé avec `llmman ps`, `ollama ps`, ou `curl <serveur>/api/ps`.25+Un modèle qui n'est pas chargé donne `🧹 <modèle> was not loaded: nothing to unload`, et rien n'est chargé : llmman et Ollama ne sont même pas interrogés, Kronk répond que le modèle n'est pas chargé. Vérifiez ce qui est chargé avec `llmman ps`, `ollama ps`, `curl <serveur>/api/ps`, ou, sur Kronk, `curl <serveur>/v1/kronk/models/ps`.
26 26
27 ## Laisser l'agent décharger un modèle27 ## Laisser l'agent décharger un modèle
28 28
modified docs/fr/reference/cli.md +3 -3
@@ -71,7 +71,7 @@ La première source existante gagne.
7171 | `MM_BIN` | Posée par `mm` dans son propre environnement, dont héritent tous les processus qu'il lance — les commandes de son outil `bash`, ses hooks : le chemin du binaire `mm` en cours. |
7272 | `MM_CONFIG` | Posée par `mm` dans son propre environnement, comme `MM_BIN`, quand un fichier de configuration a été lu : le chemin absolu de son fichier de configuration. Lue par `--ask` / `--ask-with-context` quand ni `-config` ni chemin positionnel ne sont donnés. |
7373 | `MM_ASK_DEPTH` | Nombre de niveaux de `/ask` au-dessus de cet agent (absente = 0). Posée par `/ask` ; à 2, `/ask` est refusée. |
74-| Variable nommée par `apiKeyEnv` | Contient la clé d'API. Nom par défaut : aucun pour `dmr`, `LLAMA_API_KEY` pour `llamacpp`, `LLMMAN_API_KEY` pour `llmman`, `OLLAMA_API_KEY` pour `ollama`. |
74+| Variable nommée par `apiKeyEnv` | Contient la clé d'API. Nom par défaut : aucun pour `dmr`, `LLAMA_API_KEY` pour `llamacpp`, `LLMMAN_API_KEY` pour `llmman`, `OLLAMA_API_KEY` pour `ollama`, `KRONK_TOKEN` pour `kronk`. |
7575
7676 ## Sortie de démarrage
7777
@@ -222,7 +222,7 @@ Un fichier d'agent hors du workspace ne donne qu'un avertissement : voir [quand
222222
223223 | Option | Question quand elle est absente | Défaut |
224224 |--------|---------------------------------|--------|
225-| `--provider` | Serveur de modèles : `llmman`, `ollama`, `dmr`, `llamacpp`, `openai` | le premier serveur local qui répond, sinon `llmman` (`openai` n'est jamais sondé : son adresse par défaut est l'API d'OpenAI) |
225+| `--provider` | Serveur de modèles : `llmman`, `ollama`, `dmr`, `llamacpp`, `kronk`, `openai` | le premier serveur local qui répond, sinon `llmman` (`openai` n'est jamais sondé : son adresse par défaut est l'API d'OpenAI) |
226226 | `--base-url` | `openai` seulement : Endpoint — l'adresse compatible OpenAI du service (OpenAI, Cerebras, Hugging Face…) ; pour les autres fournisseurs, pas posée | `https://api.openai.com/v1` pour `openai` ; sinon l'adresse du fournisseur, ou son adresse de secours quand elle ne répond pas. Écrite comme `baseUrl` sauf si c'est la valeur par défaut |
227227 | `--api-key-env` | `openai` seulement : variable d'environnement qui contient la clé d'API | `OPENAI_API_KEY`. Écrite comme `apiKeyEnv` sauf si c'est la valeur par défaut |
228228 | `--model` | Modèle : la liste des modèles du serveur (`/api/tags` pour llmman et Ollama, `/models` pour les autres ; 5 s pour un serveur local, 20 s pour `openai`), ou un nom à taper quand il n'y a pas de liste — la question dit pourquoi (`<VARIABLE> is not set: no list`, ou l'erreur du serveur). Pour `openai`, la liste est demandée avec la clé lue dans la variable nommée plus haut, et pas demandée du tout quand cette variable est vide | le premier de la liste |
@@ -240,7 +240,7 @@ Un fichier d'agent hors du workspace ne donne qu'un avertissement : voir [quand
240240 |--------|--------------|
241241 | 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, `←` `→` déplacent le curseur dans une réponse de texte (`Début` `Fin` ou `Ctrl-A` `Ctrl-E` pour aller à ses bouts ; la saisie s'insère au curseur, `Retour arrière` efface avant lui, `Suppr` après), `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. |
242242 | 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. |
243-| 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`. |
243+| 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. Kronk : `GET /v1/kronk/models/<id>` (l'id échappé) — la fenêtre avec laquelle le modèle est configuré (`model_config.context-window` ; la longueur d'entraînement du GGUF à défaut), l'échantillonnage du GGUF, le chat template ; pour un profil (`provider/modelID/PROFIL`), l'échantillonnage que Kronk lui applique (`temperature`, `top_p`, `top_k`, `min_p`, `max_tokens` s'il est plafonné), recopié dans `sampling` par-dessus toute autre source. Docker Model Runner et `openai` : rien — la table des familles décide ; `openai` liste ses modèles avec `OPENAI_API_KEY`. |
244244 | 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`). |
245245 | 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à. |
246246 | É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. |
@@ -71,7 +71,7 @@ La première source existante gagne.
71 | `MM_BIN` | Posée par `mm` dans son propre environnement, dont héritent tous les processus qu'il lance — les commandes de son outil `bash`, ses hooks : le chemin du binaire `mm` en cours. |71 | `MM_BIN` | Posée par `mm` dans son propre environnement, dont héritent tous les processus qu'il lance — les commandes de son outil `bash`, ses hooks : le chemin du binaire `mm` en cours. |
72 | `MM_CONFIG` | Posée par `mm` dans son propre environnement, comme `MM_BIN`, quand un fichier de configuration a été lu : le chemin absolu de son fichier de configuration. Lue par `--ask` / `--ask-with-context` quand ni `-config` ni chemin positionnel ne sont donnés. |72 | `MM_CONFIG` | Posée par `mm` dans son propre environnement, comme `MM_BIN`, quand un fichier de configuration a été lu : le chemin absolu de son fichier de configuration. Lue par `--ask` / `--ask-with-context` quand ni `-config` ni chemin positionnel ne sont donnés. |
73 | `MM_ASK_DEPTH` | Nombre de niveaux de `/ask` au-dessus de cet agent (absente = 0). Posée par `/ask` ; à 2, `/ask` est refusée. |73 | `MM_ASK_DEPTH` | Nombre de niveaux de `/ask` au-dessus de cet agent (absente = 0). Posée par `/ask` ; à 2, `/ask` est refusée. |
74-| Variable nommée par `apiKeyEnv` | Contient la clé d'API. Nom par défaut : aucun pour `dmr`, `LLAMA_API_KEY` pour `llamacpp`, `LLMMAN_API_KEY` pour `llmman`, `OLLAMA_API_KEY` pour `ollama`. |74+| Variable nommée par `apiKeyEnv` | Contient la clé d'API. Nom par défaut : aucun pour `dmr`, `LLAMA_API_KEY` pour `llamacpp`, `LLMMAN_API_KEY` pour `llmman`, `OLLAMA_API_KEY` pour `ollama`, `KRONK_TOKEN` pour `kronk`. |
75 75
76 ## Sortie de démarrage76 ## Sortie de démarrage
77 77
@@ -222,7 +222,7 @@ Un fichier d'agent hors du workspace ne donne qu'un avertissement : voir [quand
222 222
223 | Option | Question quand elle est absente | Défaut |223 | Option | Question quand elle est absente | Défaut |
224 |--------|---------------------------------|--------|224 |--------|---------------------------------|--------|
225-| `--provider` | Serveur de modèles : `llmman`, `ollama`, `dmr`, `llamacpp`, `openai` | le premier serveur local qui répond, sinon `llmman` (`openai` n'est jamais sondé : son adresse par défaut est l'API d'OpenAI) |225+| `--provider` | Serveur de modèles : `llmman`, `ollama`, `dmr`, `llamacpp`, `kronk`, `openai` | le premier serveur local qui répond, sinon `llmman` (`openai` n'est jamais sondé : son adresse par défaut est l'API d'OpenAI) |
226 | `--base-url` | `openai` seulement : Endpoint — l'adresse compatible OpenAI du service (OpenAI, Cerebras, Hugging Face…) ; pour les autres fournisseurs, pas posée | `https://api.openai.com/v1` pour `openai` ; sinon l'adresse du fournisseur, ou son adresse de secours quand elle ne répond pas. Écrite comme `baseUrl` sauf si c'est la valeur par défaut |226 | `--base-url` | `openai` seulement : Endpoint — l'adresse compatible OpenAI du service (OpenAI, Cerebras, Hugging Face…) ; pour les autres fournisseurs, pas posée | `https://api.openai.com/v1` pour `openai` ; sinon l'adresse du fournisseur, ou son adresse de secours quand elle ne répond pas. Écrite comme `baseUrl` sauf si c'est la valeur par défaut |
227 | `--api-key-env` | `openai` seulement : variable d'environnement qui contient la clé d'API | `OPENAI_API_KEY`. Écrite comme `apiKeyEnv` sauf si c'est la valeur par défaut |227 | `--api-key-env` | `openai` seulement : variable d'environnement qui contient la clé d'API | `OPENAI_API_KEY`. Écrite comme `apiKeyEnv` sauf si c'est la valeur par défaut |
228 | `--model` | Modèle : la liste des modèles du serveur (`/api/tags` pour llmman et Ollama, `/models` pour les autres ; 5 s pour un serveur local, 20 s pour `openai`), ou un nom à taper quand il n'y a pas de liste — la question dit pourquoi (`<VARIABLE> is not set: no list`, ou l'erreur du serveur). Pour `openai`, la liste est demandée avec la clé lue dans la variable nommée plus haut, et pas demandée du tout quand cette variable est vide | le premier de la liste |228 | `--model` | Modèle : la liste des modèles du serveur (`/api/tags` pour llmman et Ollama, `/models` pour les autres ; 5 s pour un serveur local, 20 s pour `openai`), ou un nom à taper quand il n'y a pas de liste — la question dit pourquoi (`<VARIABLE> is not set: no list`, ou l'erreur du serveur). Pour `openai`, la liste est demandée avec la clé lue dans la variable nommée plus haut, et pas demandée du tout quand cette variable est vide | le premier de la liste |
@@ -240,7 +240,7 @@ Un fichier d'agent hors du workspace ne donne qu'un avertissement : voir [quand
240 |--------|--------------|240 |--------|--------------|
241 | 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, `←` `→` déplacent le curseur dans une réponse de texte (`Début` `Fin` ou `Ctrl-A` `Ctrl-E` pour aller à ses bouts ; la saisie s'insère au curseur, `Retour arrière` efface avant lui, `Suppr` après), `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. |241 | 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, `←` `→` déplacent le curseur dans une réponse de texte (`Début` `Fin` ou `Ctrl-A` `Ctrl-E` pour aller à ses bouts ; la saisie s'insère au curseur, `Retour arrière` efface avant lui, `Suppr` après), `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. |
242 | 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. |242 | 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. |
243-| 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`. |243+| 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. Kronk : `GET /v1/kronk/models/<id>` (l'id échappé) — la fenêtre avec laquelle le modèle est configuré (`model_config.context-window` ; la longueur d'entraînement du GGUF à défaut), l'échantillonnage du GGUF, le chat template ; pour un profil (`provider/modelID/PROFIL`), l'échantillonnage que Kronk lui applique (`temperature`, `top_p`, `top_k`, `min_p`, `max_tokens` s'il est plafonné), recopié dans `sampling` par-dessus toute autre source. Docker Model Runner et `openai` : rien — la table des familles décide ; `openai` liste ses modèles avec `OPENAI_API_KEY`. |
244 | 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`). |244 | 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`). |
245 | 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à. |245 | 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à. |
246 | É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. |246 | É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. |
modified docs/fr/reference/configuration.md +8 -7
@@ -6,7 +6,7 @@
66
77 | Clé | Type | Défaut | Description |
88 |-----|------|--------|-------------|
9-| `provider` | chaîne | `dmr` | Clé du registre du serveur LLM : `dmr`, `llamacpp`, `llmman`, `ollama`, ou `openai` pour tout autre serveur compatible OpenAI (voir [Utiliser n'importe quel serveur compatible OpenAI](../how-to/run-with-an-openai-compatible-server.md)). Surchargée par `AGENT_PROVIDER`. |
9+| `provider` | chaîne | `dmr` | Clé du registre du serveur LLM : `dmr`, `llamacpp`, `llmman`, `ollama`, `kronk`, ou `openai` pour tout autre serveur compatible OpenAI (voir [Utiliser n'importe quel serveur compatible OpenAI](../how-to/run-with-an-openai-compatible-server.md)). Surchargée par `AGENT_PROVIDER`. |
1010 | `model` | chaîne | `huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M` | Identifiant du modèle dans la nomenclature du fournisseur. Surchargé par `AGENT_MODEL`. |
1111 | `baseUrl` | chaîne | `""` (défaut du fournisseur) | Point d'accès compatible OpenAI essayé en premier. Surchargé par `AGENT_BASE_URL`. |
1212 | `fallback` | chaîne ou absente | absente (défaut du fournisseur) | Point d'accès utilisé quand `baseUrl` ne répond pas sur `/models` en 2 s. Absente = défaut du fournisseur ; `""` = pas de repli. |
@@ -31,11 +31,11 @@
3131 | `planTool` | booléen | `true` | Déclare `plan`, par lequel le modèle tient un plan de tâches pendant un travail en plusieurs étapes. `false` le retire. Voir [outils](tools.md#plan) et [Gérer des plans et des tâches](../how-to/manage-plans-and-tasks.md). |
3232 | `askUserTool` | booléen | `true` | Déclare `ask_user`, par lequel le modèle pose des questions à l'utilisateur (choisir une option, en choisir plusieurs, taper un texte, oui ou non) et attend les réponses. `false` le retire : le modèle ne peut pas demander. Jamais déclaré à côté d'un `structuredOutput`. Surchargé par `AGENT_ASK_USER` ; pour un agent interrogé par un autre, aussi par le `mmConfigs.<nom>.askUser` de celui qui l'interroge. Voir [outils](tools.md#ask_user). |
3333 | `saveAnswerTool` | bool | `false` | Déclare `save_answer`, qui écrit dans un nouveau fichier une réponse que le modèle a déjà donnée : le modèle envoie un nom de fichier, pas le texte une seconde fois. Désactivé par défaut. La commande `/save` existe dans tous les cas. Voir les [outils](tools.md#save_answer). |
34-| `unloadModels` | bool | `true` | La commande `/unload [modèle]` (terminal, fenêtre, éditeur) et l'outil `unload_model`, qui libèrent un modèle de la mémoire du serveur : llmman et Ollama, llama-server en mode router. `false` retire les deux. L'outil n'est jamais déclaré à côté d'une `structuredOutput` ni d'un bloc `decision`. Voir les [outils](tools.md#unload_model) et [Libérer la mémoire d'un modèle](../how-to/unload-a-model.md). |
35-| `autoPull` | booléen | `false` | Au démarrage, télécharge les modèles que cet agent utilisera et que le serveur ne liste pas : le sien, ceux de ses agents `mmConfigs`, ceux de ses deciders `skillRouting` / `toolRouting`. La progression va sur stderr — une ligne réécrite sur place dans un terminal (`⬇ pulling <modèle> — 52 MB / 145 MB (35 %)`), une ligne par dixième ailleurs — puis `⬇ pulled <modèle> in 0.8s`. Un échec sur le modèle de l'agent arrête `mm` avec le message du serveur (`[engine error: autoPull: <modèle>: the server says: …]`) ; un échec sur un autre modèle, un serveur qui ne répond pas ou un fournisseur qui ne sait pas télécharger donne un `[warning: autoPull: …]`. Un agent lancé par `/ask` ou `ask_agent` ne télécharge rien ; `mm --ask` non plus. Un modèle que le serveur liste déjà n'est jamais téléchargé (un nom sans tag correspond aussi à son entrée `:latest`). Par fournisseur : `llmman` et `ollama` — `POST /api/pull` ; `dmr` — `POST /models/create` sur le runner (l'URL de base sans `/engines/…`), le téléchargement ne comptant comme fait qu'une fois le modèle listé par le runner ; `llamacpp` — pas de téléchargement par l'API : l'avertissement dit de lancer llama-server avec `-hf <dépôt>:<quant>`, ou de mettre le GGUF dans le dossier de modèles du router ; `openai` — un avertissement. |
36-| `sampling` | dictionnaire | `temperature: 0.0`, `top_p: 0.9`, `max_tokens: 4096` | Paramètres de génération, noms de l'API OpenAI, transmis au serveur. Le bloc du fichier est fusionné avec ces valeurs par défaut : une valeur par défaut que le fichier ne nomme pas est quand même envoyée. **Sauf avec `provider: openai`** : la requête porte exactement le bloc du fichier, rien d'ajouté (pas de bloc : aucune clé de génération, les valeurs par défaut du serveur s'appliquent) — les modèles de raisonnement hébergés par OpenAI refusent `max_tokens` et n'acceptent que la température par défaut, une valeur par défaut laissée là donnerait une erreur 400. Une clé que l'API OpenAI ne déclare pas est envoyée telle quelle, au premier niveau de la requête — par exemple `chat_template_kwargs: {enable_thinking: false}`, qui demande à un serveur llama.cpp (`dmr`, `llamacpp`, `llmman`) de ne pas raisonner ; `reasoning_effort: none` fait de même avec Ollama. `messages`, `tools`, `tool_choice` (et les anciens `functions`, `function_call`) sont ignorées : Genkit les construit à partir de la conversation. L'`agent.yaml` livré ajoute `parallel_tool_calls: false`. `max_tokens` borne aussi un appel d'outil : un `write_file` de long document (ou le raisonnement d'un modèle « thinking » plus l'appel) peut l'atteindre, et l'appel est alors coupé. mm le rend deux fois au modèle en lui demandant de procéder par morceaux, puis s'arrête avec `the model hit max_tokens (N) while writing a tool call` ; augmentez la valeur si cela arrive. Une configuration qui nomme la limite `max_completion_tokens` (le nom de l'API OpenAI pour ses modèles récents) est lue de la même façon : ce nom est repris dans ce message et dans la requête de résumé de la compression du contexte. En streaming, mm demande aussi au serveur le compte des tokens (`stream_options.include_usage`). |
34+| `unloadModels` | bool | `true` | La commande `/unload [modèle]` (terminal, fenêtre, éditeur) et l'outil `unload_model`, qui libèrent un modèle de la mémoire du serveur : llmman et Ollama, Kronk, llama-server en mode router. `false` retire les deux. L'outil n'est jamais déclaré à côté d'une `structuredOutput` ni d'un bloc `decision`. Voir les [outils](tools.md#unload_model) et [Libérer la mémoire d'un modèle](../how-to/unload-a-model.md). |
35+| `autoPull` | booléen | `false` | Au démarrage, télécharge les modèles que cet agent utilisera et que le serveur ne liste pas : le sien, ceux de ses agents `mmConfigs`, ceux de ses deciders `skillRouting` / `toolRouting`. La progression va sur stderr — une ligne réécrite sur place dans un terminal (`⬇ pulling <modèle> — 52 MB / 145 MB (35 %)`), une ligne par dixième ailleurs — puis `⬇ pulled <modèle> in 0.8s`. Un échec sur le modèle de l'agent arrête `mm` avec le message du serveur (`[engine error: autoPull: <modèle>: the server says: …]`) ; un échec sur un autre modèle, un serveur qui ne répond pas ou un fournisseur qui ne sait pas télécharger donne un `[warning: autoPull: …]`. Un agent lancé par `/ask` ou `ask_agent` ne télécharge rien ; `mm --ask` non plus. Un modèle que le serveur liste déjà n'est jamais téléchargé (un nom sans tag correspond aussi à son entrée `:latest`). Par fournisseur : `llmman` et `ollama` — `POST /api/pull` ; `kronk` — `POST /v1/kronk/models/pull`, le téléchargement ne comptant comme fait que si le flux de Kronk se termine par `"downloaded": true` (Kronk répond 200 à un téléchargement raté ; son dernier statut est alors le message) ; `dmr` — `POST /models/create` sur le runner (l'URL de base sans `/engines/…`), le téléchargement ne comptant comme fait qu'une fois le modèle listé par le runner ; `llamacpp` — pas de téléchargement par l'API : l'avertissement dit de lancer llama-server avec `-hf <dépôt>:<quant>`, ou de mettre le GGUF dans le dossier de modèles du router ; `openai` — un avertissement. |
36+| `sampling` | dictionnaire | `temperature: 0.0`, `top_p: 0.9`, `max_tokens: 4096` | Paramètres de génération, noms de l'API OpenAI, transmis au serveur. Le bloc du fichier est fusionné avec ces valeurs par défaut : une valeur par défaut que le fichier ne nomme pas est quand même envoyée. **Sauf avec `provider: openai`** : la requête porte exactement le bloc du fichier, rien d'ajouté (pas de bloc : aucune clé de génération, les valeurs par défaut du serveur s'appliquent) — les modèles de raisonnement hébergés par OpenAI refusent `max_tokens` et n'acceptent que la température par défaut, une valeur par défaut laissée là donnerait une erreur 400. Une clé que l'API OpenAI ne déclare pas est envoyée telle quelle, au premier niveau de la requête — par exemple `chat_template_kwargs: {enable_thinking: false}`, qui demande à un serveur llama.cpp (`dmr`, `llamacpp`, `llmman`) ou à Kronk de ne pas raisonner ; `reasoning_effort: none` fait de même avec Ollama. `messages`, `tools`, `tool_choice` (et les anciens `functions`, `function_call`) sont ignorées : Genkit les construit à partir de la conversation. L'`agent.yaml` livré ajoute `parallel_tool_calls: false`. `max_tokens` borne aussi un appel d'outil : un `write_file` de long document (ou le raisonnement d'un modèle « thinking » plus l'appel) peut l'atteindre, et l'appel est alors coupé. mm le rend deux fois au modèle en lui demandant de procéder par morceaux, puis s'arrête avec `the model hit max_tokens (N) while writing a tool call` ; augmentez la valeur si cela arrive. Une configuration qui nomme la limite `max_completion_tokens` (le nom de l'API OpenAI pour ses modèles récents) est lue de la même façon : ce nom est repris dans ce message et dans la requête de résumé de la compression du contexte. En streaming, mm demande aussi au serveur le compte des tokens (`stream_options.include_usage`). |
3737 | `watchdogTimeout` | durée | `20s` | Silence toléré entre deux tokens diffusés avant de déclarer la connexion bloquée. Doublé pour les requêtes de résumé. |
38-| `firstTokenTimeout` | durée | `3m` | Silence toléré avant le premier token d'un appel au modèle, pendant que le serveur lit le prompt (chaque aller-retour d'un tour en a un). Jamais plus court que `watchdogTimeout` ; doit être >= 0. Avec un serveur llama.cpp (`dmr`, `llamacpp`, `llmman` — dont les modèles GGUF locaux tournent dans llama-server), mm demande `return_progress` : chaque rapport d'avancement relance ce délai, et le spinner affiche `Reading the prompt 12.0k/25.3k tokens`. Ollama n'envoie pas d'avancement : cette seule attente couvre tout le prompt (et le chargement du modèle). À l'expiration, le watchdog dit `no first token from the server after …` (avant le premier token) ou `connection hang detected after …` (après). |
38+| `firstTokenTimeout` | durée | `3m` | Silence toléré avant le premier token d'un appel au modèle, pendant que le serveur lit le prompt (chaque aller-retour d'un tour en a un). Jamais plus court que `watchdogTimeout` ; doit être >= 0. Avec un serveur llama.cpp (`dmr`, `llamacpp`, `llmman` — dont les modèles GGUF locaux tournent dans llama-server), mm demande `return_progress` : chaque rapport d'avancement relance ce délai, et le spinner affiche `Reading the prompt 12.0k/25.3k tokens`. Ollama et Kronk n'envoient pas d'avancement : cette seule attente couvre tout le prompt (et le chargement du modèle). À l'expiration, le watchdog dit `no first token from the server after …` (avant le premier token) ou `connection hang detected after …` (après). |
3939 | `bashTimeout` | durée | `30s` | Durée maximale d'une commande `bash` avant qu'elle soit tuée ; l'outil répond alors `[error: <bashTimeout> timeout exceeded]`. Doit être > 0. Le watchdog continue de compter pendant qu'une commande tourne (ticket 57) : une commande plus longue que `watchdogTimeout` termine le tour, donc relevez les deux ensemble. |
4040 | `context` | dictionnaire | voir ci-dessous | Compression automatique de l'historique. |
4141 | `sessions` | dictionnaire | voir ci-dessous | Sessions persistantes, par projet. |
@@ -59,7 +59,7 @@
5959 | `threshold` | entier | `75` | Pourcentage de la fenêtre de contexte au-delà duquel l'historique est compressé avant la question suivante. Entre 1 et 100. |
6060 | `maxMessages` | entier | `80` | Nombre de messages qui déclenche une compression quand la fenêtre est inconnue ou l'estimation trop basse. `0` désactive. Doit être ≥ 0. |
6161 | `keepLastTurns` | entier | `3` | Tours de question conservés bruts en fin d'historique. Doit être ≥ 1. |
62-| `summaryMaxTokens` | entier | `1200` | `max_tokens` de la requête de résumé. Doit être > 0. Le raisonnement d'un modèle qui réfléchit est décompté de ce budget : sur `dmr`, `llamacpp` et `llmman`, la requête de résumé désactive le raisonnement (`chat_template_kwargs.enable_thinking: false`), sur `ollama` aussi (`reasoning_effort: "none"`, le seul interrupteur qu'Ollama respecte) — quoi que dise `sampling` ; les autres `chat_template_kwargs` sont gardés, et un `reasoning_effort` qui y est fixé est ramené à `low` (gpt-oss n'a pas d'« arrêt ») ; ailleurs, si l'erreur dit que le modèle « reasoned until the limit », augmentez-le. |
62+| `summaryMaxTokens` | entier | `1200` | `max_tokens` de la requête de résumé. Doit être > 0. Le raisonnement d'un modèle qui réfléchit est décompté de ce budget : sur `dmr`, `llamacpp`, `llmman` et `kronk`, la requête de résumé désactive le raisonnement (`chat_template_kwargs.enable_thinking: false`), sur `ollama` aussi (`reasoning_effort: "none"`, le seul interrupteur qu'Ollama respecte) — quoi que dise `sampling` ; les autres `chat_template_kwargs` sont gardés, et un `reasoning_effort` qui y est fixé est ramené à `low` (gpt-oss n'a pas d'« arrêt ») ; ailleurs, si l'erreur dit que le modèle « reasoned until the limit », augmentez-le. |
6363 | `prompt` | chaîne | `""` | Remplace le prompt de résumé intégré quand non vide. |
6464 | `showStats` | booléen | `true` | Affiche le rapport `🗜️` d'une ligne après chaque compression. |
6565
@@ -197,6 +197,7 @@ tools:
197197 | `llamacpp` | `http://127.0.0.1:8080/v1` | aucun | `LLAMA_API_KEY` | non |
198198 | `llmman` | `http://127.0.0.1:17434/v1` | `http://host.docker.internal:17434/v1` | `LLMMAN_API_KEY` | non |
199199 | `ollama` | `http://localhost:11434/v1` | `http://host.docker.internal:11434/v1` | `OLLAMA_API_KEY` | non |
200+| `kronk` | `http://127.0.0.1:11435/v1` | `http://host.docker.internal:11435/v1` | `KRONK_TOKEN` | non — seuls les modes `authenticated` et `full-protected` de Kronk la vérifient |
200201 | `openai` | `https://api.openai.com/v1` | aucun | `OPENAI_API_KEY` | non — un serveur qui en exige une répond 401, et l'avertissement de démarrage nomme la variable |
201202
202203 Le fournisseur `openai` n'envoie que des champs standard : pas de `return_progress` (donc pas de progression pendant la lecture d'un long prompt), pas d'interrupteur de raisonnement sur une requête de résumé, pas de `/unload`. Il ne peut pas demander sa fenêtre au serveur : renseignez `contextWindow`, sinon la compression compte les messages (`context.maxMessages`). Sa sonde de démarrage envoie la clé à `GET /models` ; un 401 ou un 403 est signalé comme une clé refusée, pas comme un serveur qui ne répond pas.
@@ -464,7 +465,7 @@ Signalées sur stderr sous la forme `[config error: <chemin>: <message>]`, code
464465 | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | un nom commençant par `with-context` |
465466 | `mmConfigs.<nom>: the path must not be empty` | `<nom>: ""`, ou une forme longue sans `path` |
466467 | `hooks: unknown event "x" (…)`, `hooks.<événement>[N]: …` | un bloc `hooks:` invalide — voir [les hooks](hooks.md#erreurs-de-validation) |
467-| `unknown provider "x" (known: dmr, llamacpp, llmman, ollama)` | Signalée par le moteur, sous la forme `[engine error: …]`. |
468+| `unknown provider "x" (known: dmr, kronk, llamacpp, llmman, ollama, openai)` | Signalée par le moteur, sous la forme `[engine error: …]`. |
468469
469470 Un avertissement au démarrage, et non une erreur, est affiché quand `context.enabled` vaut `true`, que la fenêtre est inconnue et que `context.maxMessages` vaut `0`, quand un outil déclaré porte le nom d'un outil MCP (`tools: <nom> skipped — an MCP tool already has that name` ; l'outil n'est pas déclaré), et quand le fichier pose la clé obsolète `permissions` (`permissions: ask is deprecated — use yolo: false instead …`).
470471
@@ -6,7 +6,7 @@
6 6
7 | Clé | Type | Défaut | Description |7 | Clé | Type | Défaut | Description |
8 |-----|------|--------|-------------|8 |-----|------|--------|-------------|
9-| `provider` | chaîne | `dmr` | Clé du registre du serveur LLM : `dmr`, `llamacpp`, `llmman`, `ollama`, ou `openai` pour tout autre serveur compatible OpenAI (voir [Utiliser n'importe quel serveur compatible OpenAI](../how-to/run-with-an-openai-compatible-server.md)). Surchargée par `AGENT_PROVIDER`. |9+| `provider` | chaîne | `dmr` | Clé du registre du serveur LLM : `dmr`, `llamacpp`, `llmman`, `ollama`, `kronk`, ou `openai` pour tout autre serveur compatible OpenAI (voir [Utiliser n'importe quel serveur compatible OpenAI](../how-to/run-with-an-openai-compatible-server.md)). Surchargée par `AGENT_PROVIDER`. |
10 | `model` | chaîne | `huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M` | Identifiant du modèle dans la nomenclature du fournisseur. Surchargé par `AGENT_MODEL`. |10 | `model` | chaîne | `huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M` | Identifiant du modèle dans la nomenclature du fournisseur. Surchargé par `AGENT_MODEL`. |
11 | `baseUrl` | chaîne | `""` (défaut du fournisseur) | Point d'accès compatible OpenAI essayé en premier. Surchargé par `AGENT_BASE_URL`. |11 | `baseUrl` | chaîne | `""` (défaut du fournisseur) | Point d'accès compatible OpenAI essayé en premier. Surchargé par `AGENT_BASE_URL`. |
12 | `fallback` | chaîne ou absente | absente (défaut du fournisseur) | Point d'accès utilisé quand `baseUrl` ne répond pas sur `/models` en 2 s. Absente = défaut du fournisseur ; `""` = pas de repli. |12 | `fallback` | chaîne ou absente | absente (défaut du fournisseur) | Point d'accès utilisé quand `baseUrl` ne répond pas sur `/models` en 2 s. Absente = défaut du fournisseur ; `""` = pas de repli. |
@@ -31,11 +31,11 @@
31 | `planTool` | booléen | `true` | Déclare `plan`, par lequel le modèle tient un plan de tâches pendant un travail en plusieurs étapes. `false` le retire. Voir [outils](tools.md#plan) et [Gérer des plans et des tâches](../how-to/manage-plans-and-tasks.md). |31 | `planTool` | booléen | `true` | Déclare `plan`, par lequel le modèle tient un plan de tâches pendant un travail en plusieurs étapes. `false` le retire. Voir [outils](tools.md#plan) et [Gérer des plans et des tâches](../how-to/manage-plans-and-tasks.md). |
32 | `askUserTool` | booléen | `true` | Déclare `ask_user`, par lequel le modèle pose des questions à l'utilisateur (choisir une option, en choisir plusieurs, taper un texte, oui ou non) et attend les réponses. `false` le retire : le modèle ne peut pas demander. Jamais déclaré à côté d'un `structuredOutput`. Surchargé par `AGENT_ASK_USER` ; pour un agent interrogé par un autre, aussi par le `mmConfigs.<nom>.askUser` de celui qui l'interroge. Voir [outils](tools.md#ask_user). |32 | `askUserTool` | booléen | `true` | Déclare `ask_user`, par lequel le modèle pose des questions à l'utilisateur (choisir une option, en choisir plusieurs, taper un texte, oui ou non) et attend les réponses. `false` le retire : le modèle ne peut pas demander. Jamais déclaré à côté d'un `structuredOutput`. Surchargé par `AGENT_ASK_USER` ; pour un agent interrogé par un autre, aussi par le `mmConfigs.<nom>.askUser` de celui qui l'interroge. Voir [outils](tools.md#ask_user). |
33 | `saveAnswerTool` | bool | `false` | Déclare `save_answer`, qui écrit dans un nouveau fichier une réponse que le modèle a déjà donnée : le modèle envoie un nom de fichier, pas le texte une seconde fois. Désactivé par défaut. La commande `/save` existe dans tous les cas. Voir les [outils](tools.md#save_answer). |33 | `saveAnswerTool` | bool | `false` | Déclare `save_answer`, qui écrit dans un nouveau fichier une réponse que le modèle a déjà donnée : le modèle envoie un nom de fichier, pas le texte une seconde fois. Désactivé par défaut. La commande `/save` existe dans tous les cas. Voir les [outils](tools.md#save_answer). |
34-| `unloadModels` | bool | `true` | La commande `/unload [modèle]` (terminal, fenêtre, éditeur) et l'outil `unload_model`, qui libèrent un modèle de la mémoire du serveur : llmman et Ollama, llama-server en mode router. `false` retire les deux. L'outil n'est jamais déclaré à côté d'une `structuredOutput` ni d'un bloc `decision`. Voir les [outils](tools.md#unload_model) et [Libérer la mémoire d'un modèle](../how-to/unload-a-model.md). |34+| `unloadModels` | bool | `true` | La commande `/unload [modèle]` (terminal, fenêtre, éditeur) et l'outil `unload_model`, qui libèrent un modèle de la mémoire du serveur : llmman et Ollama, Kronk, llama-server en mode router. `false` retire les deux. L'outil n'est jamais déclaré à côté d'une `structuredOutput` ni d'un bloc `decision`. Voir les [outils](tools.md#unload_model) et [Libérer la mémoire d'un modèle](../how-to/unload-a-model.md). |
35-| `autoPull` | booléen | `false` | Au démarrage, télécharge les modèles que cet agent utilisera et que le serveur ne liste pas : le sien, ceux de ses agents `mmConfigs`, ceux de ses deciders `skillRouting` / `toolRouting`. La progression va sur stderr — une ligne réécrite sur place dans un terminal (`⬇ pulling <modèle> — 52 MB / 145 MB (35 %)`), une ligne par dixième ailleurs — puis `⬇ pulled <modèle> in 0.8s`. Un échec sur le modèle de l'agent arrête `mm` avec le message du serveur (`[engine error: autoPull: <modèle>: the server says: …]`) ; un échec sur un autre modèle, un serveur qui ne répond pas ou un fournisseur qui ne sait pas télécharger donne un `[warning: autoPull: …]`. Un agent lancé par `/ask` ou `ask_agent` ne télécharge rien ; `mm --ask` non plus. Un modèle que le serveur liste déjà n'est jamais téléchargé (un nom sans tag correspond aussi à son entrée `:latest`). Par fournisseur : `llmman` et `ollama` — `POST /api/pull` ; `dmr` — `POST /models/create` sur le runner (l'URL de base sans `/engines/…`), le téléchargement ne comptant comme fait qu'une fois le modèle listé par le runner ; `llamacpp` — pas de téléchargement par l'API : l'avertissement dit de lancer llama-server avec `-hf <dépôt>:<quant>`, ou de mettre le GGUF dans le dossier de modèles du router ; `openai` — un avertissement. |35+| `autoPull` | booléen | `false` | Au démarrage, télécharge les modèles que cet agent utilisera et que le serveur ne liste pas : le sien, ceux de ses agents `mmConfigs`, ceux de ses deciders `skillRouting` / `toolRouting`. La progression va sur stderr — une ligne réécrite sur place dans un terminal (`⬇ pulling <modèle> — 52 MB / 145 MB (35 %)`), une ligne par dixième ailleurs — puis `⬇ pulled <modèle> in 0.8s`. Un échec sur le modèle de l'agent arrête `mm` avec le message du serveur (`[engine error: autoPull: <modèle>: the server says: …]`) ; un échec sur un autre modèle, un serveur qui ne répond pas ou un fournisseur qui ne sait pas télécharger donne un `[warning: autoPull: …]`. Un agent lancé par `/ask` ou `ask_agent` ne télécharge rien ; `mm --ask` non plus. Un modèle que le serveur liste déjà n'est jamais téléchargé (un nom sans tag correspond aussi à son entrée `:latest`). Par fournisseur : `llmman` et `ollama` — `POST /api/pull` ; `kronk` — `POST /v1/kronk/models/pull`, le téléchargement ne comptant comme fait que si le flux de Kronk se termine par `"downloaded": true` (Kronk répond 200 à un téléchargement raté ; son dernier statut est alors le message) ; `dmr` — `POST /models/create` sur le runner (l'URL de base sans `/engines/…`), le téléchargement ne comptant comme fait qu'une fois le modèle listé par le runner ; `llamacpp` — pas de téléchargement par l'API : l'avertissement dit de lancer llama-server avec `-hf <dépôt>:<quant>`, ou de mettre le GGUF dans le dossier de modèles du router ; `openai` — un avertissement. |
36-| `sampling` | dictionnaire | `temperature: 0.0`, `top_p: 0.9`, `max_tokens: 4096` | Paramètres de génération, noms de l'API OpenAI, transmis au serveur. Le bloc du fichier est fusionné avec ces valeurs par défaut : une valeur par défaut que le fichier ne nomme pas est quand même envoyée. **Sauf avec `provider: openai`** : la requête porte exactement le bloc du fichier, rien d'ajouté (pas de bloc : aucune clé de génération, les valeurs par défaut du serveur s'appliquent) — les modèles de raisonnement hébergés par OpenAI refusent `max_tokens` et n'acceptent que la température par défaut, une valeur par défaut laissée là donnerait une erreur 400. Une clé que l'API OpenAI ne déclare pas est envoyée telle quelle, au premier niveau de la requête — par exemple `chat_template_kwargs: {enable_thinking: false}`, qui demande à un serveur llama.cpp (`dmr`, `llamacpp`, `llmman`) de ne pas raisonner ; `reasoning_effort: none` fait de même avec Ollama. `messages`, `tools`, `tool_choice` (et les anciens `functions`, `function_call`) sont ignorées : Genkit les construit à partir de la conversation. L'`agent.yaml` livré ajoute `parallel_tool_calls: false`. `max_tokens` borne aussi un appel d'outil : un `write_file` de long document (ou le raisonnement d'un modèle « thinking » plus l'appel) peut l'atteindre, et l'appel est alors coupé. mm le rend deux fois au modèle en lui demandant de procéder par morceaux, puis s'arrête avec `the model hit max_tokens (N) while writing a tool call` ; augmentez la valeur si cela arrive. Une configuration qui nomme la limite `max_completion_tokens` (le nom de l'API OpenAI pour ses modèles récents) est lue de la même façon : ce nom est repris dans ce message et dans la requête de résumé de la compression du contexte. En streaming, mm demande aussi au serveur le compte des tokens (`stream_options.include_usage`). |36+| `sampling` | dictionnaire | `temperature: 0.0`, `top_p: 0.9`, `max_tokens: 4096` | Paramètres de génération, noms de l'API OpenAI, transmis au serveur. Le bloc du fichier est fusionné avec ces valeurs par défaut : une valeur par défaut que le fichier ne nomme pas est quand même envoyée. **Sauf avec `provider: openai`** : la requête porte exactement le bloc du fichier, rien d'ajouté (pas de bloc : aucune clé de génération, les valeurs par défaut du serveur s'appliquent) — les modèles de raisonnement hébergés par OpenAI refusent `max_tokens` et n'acceptent que la température par défaut, une valeur par défaut laissée là donnerait une erreur 400. Une clé que l'API OpenAI ne déclare pas est envoyée telle quelle, au premier niveau de la requête — par exemple `chat_template_kwargs: {enable_thinking: false}`, qui demande à un serveur llama.cpp (`dmr`, `llamacpp`, `llmman`) ou à Kronk de ne pas raisonner ; `reasoning_effort: none` fait de même avec Ollama. `messages`, `tools`, `tool_choice` (et les anciens `functions`, `function_call`) sont ignorées : Genkit les construit à partir de la conversation. L'`agent.yaml` livré ajoute `parallel_tool_calls: false`. `max_tokens` borne aussi un appel d'outil : un `write_file` de long document (ou le raisonnement d'un modèle « thinking » plus l'appel) peut l'atteindre, et l'appel est alors coupé. mm le rend deux fois au modèle en lui demandant de procéder par morceaux, puis s'arrête avec `the model hit max_tokens (N) while writing a tool call` ; augmentez la valeur si cela arrive. Une configuration qui nomme la limite `max_completion_tokens` (le nom de l'API OpenAI pour ses modèles récents) est lue de la même façon : ce nom est repris dans ce message et dans la requête de résumé de la compression du contexte. En streaming, mm demande aussi au serveur le compte des tokens (`stream_options.include_usage`). |
37 | `watchdogTimeout` | durée | `20s` | Silence toléré entre deux tokens diffusés avant de déclarer la connexion bloquée. Doublé pour les requêtes de résumé. |37 | `watchdogTimeout` | durée | `20s` | Silence toléré entre deux tokens diffusés avant de déclarer la connexion bloquée. Doublé pour les requêtes de résumé. |
38-| `firstTokenTimeout` | durée | `3m` | Silence toléré avant le premier token d'un appel au modèle, pendant que le serveur lit le prompt (chaque aller-retour d'un tour en a un). Jamais plus court que `watchdogTimeout` ; doit être >= 0. Avec un serveur llama.cpp (`dmr`, `llamacpp`, `llmman` — dont les modèles GGUF locaux tournent dans llama-server), mm demande `return_progress` : chaque rapport d'avancement relance ce délai, et le spinner affiche `Reading the prompt 12.0k/25.3k tokens`. Ollama n'envoie pas d'avancement : cette seule attente couvre tout le prompt (et le chargement du modèle). À l'expiration, le watchdog dit `no first token from the server after …` (avant le premier token) ou `connection hang detected after …` (après). |38+| `firstTokenTimeout` | durée | `3m` | Silence toléré avant le premier token d'un appel au modèle, pendant que le serveur lit le prompt (chaque aller-retour d'un tour en a un). Jamais plus court que `watchdogTimeout` ; doit être >= 0. Avec un serveur llama.cpp (`dmr`, `llamacpp`, `llmman` — dont les modèles GGUF locaux tournent dans llama-server), mm demande `return_progress` : chaque rapport d'avancement relance ce délai, et le spinner affiche `Reading the prompt 12.0k/25.3k tokens`. Ollama et Kronk n'envoient pas d'avancement : cette seule attente couvre tout le prompt (et le chargement du modèle). À l'expiration, le watchdog dit `no first token from the server after …` (avant le premier token) ou `connection hang detected after …` (après). |
39 | `bashTimeout` | durée | `30s` | Durée maximale d'une commande `bash` avant qu'elle soit tuée ; l'outil répond alors `[error: <bashTimeout> timeout exceeded]`. Doit être > 0. Le watchdog continue de compter pendant qu'une commande tourne (ticket 57) : une commande plus longue que `watchdogTimeout` termine le tour, donc relevez les deux ensemble. |39 | `bashTimeout` | durée | `30s` | Durée maximale d'une commande `bash` avant qu'elle soit tuée ; l'outil répond alors `[error: <bashTimeout> timeout exceeded]`. Doit être > 0. Le watchdog continue de compter pendant qu'une commande tourne (ticket 57) : une commande plus longue que `watchdogTimeout` termine le tour, donc relevez les deux ensemble. |
40 | `context` | dictionnaire | voir ci-dessous | Compression automatique de l'historique. |40 | `context` | dictionnaire | voir ci-dessous | Compression automatique de l'historique. |
41 | `sessions` | dictionnaire | voir ci-dessous | Sessions persistantes, par projet. |41 | `sessions` | dictionnaire | voir ci-dessous | Sessions persistantes, par projet. |
@@ -59,7 +59,7 @@
59 | `threshold` | entier | `75` | Pourcentage de la fenêtre de contexte au-delà duquel l'historique est compressé avant la question suivante. Entre 1 et 100. |59 | `threshold` | entier | `75` | Pourcentage de la fenêtre de contexte au-delà duquel l'historique est compressé avant la question suivante. Entre 1 et 100. |
60 | `maxMessages` | entier | `80` | Nombre de messages qui déclenche une compression quand la fenêtre est inconnue ou l'estimation trop basse. `0` désactive. Doit être ≥ 0. |60 | `maxMessages` | entier | `80` | Nombre de messages qui déclenche une compression quand la fenêtre est inconnue ou l'estimation trop basse. `0` désactive. Doit être ≥ 0. |
61 | `keepLastTurns` | entier | `3` | Tours de question conservés bruts en fin d'historique. Doit être ≥ 1. |61 | `keepLastTurns` | entier | `3` | Tours de question conservés bruts en fin d'historique. Doit être ≥ 1. |
62-| `summaryMaxTokens` | entier | `1200` | `max_tokens` de la requête de résumé. Doit être > 0. Le raisonnement d'un modèle qui réfléchit est décompté de ce budget : sur `dmr`, `llamacpp` et `llmman`, la requête de résumé désactive le raisonnement (`chat_template_kwargs.enable_thinking: false`), sur `ollama` aussi (`reasoning_effort: "none"`, le seul interrupteur qu'Ollama respecte) — quoi que dise `sampling` ; les autres `chat_template_kwargs` sont gardés, et un `reasoning_effort` qui y est fixé est ramené à `low` (gpt-oss n'a pas d'« arrêt ») ; ailleurs, si l'erreur dit que le modèle « reasoned until the limit », augmentez-le. |62+| `summaryMaxTokens` | entier | `1200` | `max_tokens` de la requête de résumé. Doit être > 0. Le raisonnement d'un modèle qui réfléchit est décompté de ce budget : sur `dmr`, `llamacpp`, `llmman` et `kronk`, la requête de résumé désactive le raisonnement (`chat_template_kwargs.enable_thinking: false`), sur `ollama` aussi (`reasoning_effort: "none"`, le seul interrupteur qu'Ollama respecte) — quoi que dise `sampling` ; les autres `chat_template_kwargs` sont gardés, et un `reasoning_effort` qui y est fixé est ramené à `low` (gpt-oss n'a pas d'« arrêt ») ; ailleurs, si l'erreur dit que le modèle « reasoned until the limit », augmentez-le. |
63 | `prompt` | chaîne | `""` | Remplace le prompt de résumé intégré quand non vide. |63 | `prompt` | chaîne | `""` | Remplace le prompt de résumé intégré quand non vide. |
64 | `showStats` | booléen | `true` | Affiche le rapport `🗜️` d'une ligne après chaque compression. |64 | `showStats` | booléen | `true` | Affiche le rapport `🗜️` d'une ligne après chaque compression. |
65 65
@@ -197,6 +197,7 @@ tools:
197 | `llamacpp` | `http://127.0.0.1:8080/v1` | aucun | `LLAMA_API_KEY` | non |197 | `llamacpp` | `http://127.0.0.1:8080/v1` | aucun | `LLAMA_API_KEY` | non |
198 | `llmman` | `http://127.0.0.1:17434/v1` | `http://host.docker.internal:17434/v1` | `LLMMAN_API_KEY` | non |198 | `llmman` | `http://127.0.0.1:17434/v1` | `http://host.docker.internal:17434/v1` | `LLMMAN_API_KEY` | non |
199 | `ollama` | `http://localhost:11434/v1` | `http://host.docker.internal:11434/v1` | `OLLAMA_API_KEY` | non |199 | `ollama` | `http://localhost:11434/v1` | `http://host.docker.internal:11434/v1` | `OLLAMA_API_KEY` | non |
200+| `kronk` | `http://127.0.0.1:11435/v1` | `http://host.docker.internal:11435/v1` | `KRONK_TOKEN` | non — seuls les modes `authenticated` et `full-protected` de Kronk la vérifient |
200 | `openai` | `https://api.openai.com/v1` | aucun | `OPENAI_API_KEY` | non — un serveur qui en exige une répond 401, et l'avertissement de démarrage nomme la variable |201 | `openai` | `https://api.openai.com/v1` | aucun | `OPENAI_API_KEY` | non — un serveur qui en exige une répond 401, et l'avertissement de démarrage nomme la variable |
201 202
202 Le fournisseur `openai` n'envoie que des champs standard : pas de `return_progress` (donc pas de progression pendant la lecture d'un long prompt), pas d'interrupteur de raisonnement sur une requête de résumé, pas de `/unload`. Il ne peut pas demander sa fenêtre au serveur : renseignez `contextWindow`, sinon la compression compte les messages (`context.maxMessages`). Sa sonde de démarrage envoie la clé à `GET /models` ; un 401 ou un 403 est signalé comme une clé refusée, pas comme un serveur qui ne répond pas.203 Le fournisseur `openai` n'envoie que des champs standard : pas de `return_progress` (donc pas de progression pendant la lecture d'un long prompt), pas d'interrupteur de raisonnement sur une requête de résumé, pas de `/unload`. Il ne peut pas demander sa fenêtre au serveur : renseignez `contextWindow`, sinon la compression compte les messages (`context.maxMessages`). Sa sonde de démarrage envoie la clé à `GET /models` ; un 401 ou un 403 est signalé comme une clé refusée, pas comme un serveur qui ne répond pas.
@@ -464,7 +465,7 @@ Signalées sur stderr sous la forme `[config error: <chemin>: <message>]`, code
464 | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | un nom commençant par `with-context` |465 | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | un nom commençant par `with-context` |
465 | `mmConfigs.<nom>: the path must not be empty` | `<nom>: ""`, ou une forme longue sans `path` |466 | `mmConfigs.<nom>: the path must not be empty` | `<nom>: ""`, ou une forme longue sans `path` |
466 | `hooks: unknown event "x" (…)`, `hooks.<événement>[N]: …` | un bloc `hooks:` invalide — voir [les hooks](hooks.md#erreurs-de-validation) |467 | `hooks: unknown event "x" (…)`, `hooks.<événement>[N]: …` | un bloc `hooks:` invalide — voir [les hooks](hooks.md#erreurs-de-validation) |
467-| `unknown provider "x" (known: dmr, llamacpp, llmman, ollama)` | Signalée par le moteur, sous la forme `[engine error: …]`. |468+| `unknown provider "x" (known: dmr, kronk, llamacpp, llmman, ollama, openai)` | Signalée par le moteur, sous la forme `[engine error: …]`. |
468 469
469 Un avertissement au démarrage, et non une erreur, est affiché quand `context.enabled` vaut `true`, que la fenêtre est inconnue et que `context.maxMessages` vaut `0`, quand un outil déclaré porte le nom d'un outil MCP (`tools: <nom> skipped — an MCP tool already has that name` ; l'outil n'est pas déclaré), et quand le fichier pose la clé obsolète `permissions` (`permissions: ask is deprecated — use yolo: false instead …`).470 Un avertissement au démarrage, et non une erreur, est affiché quand `context.enabled` vaut `true`, que la fenêtre est inconnue et que `context.maxMessages` vaut `0`, quand un outil déclaré porte le nom d'un outil MCP (`tools: <nom> skipped — an MCP tool already has that name` ; l'outil n'est pas déclaré), et quand le fichier pose la clé obsolète `permissions` (`permissions: ask is deprecated — use yolo: false instead …`).
470 471
modified docs/fr/reference/tools.md +1 -0
@@ -311,6 +311,7 @@ Déclaré quand `unloadModels` vaut true (le défaut), sauf à côté d'une `str
311311 | Aspect | Comportement |
312312 |--------|--------------|
313313 | llmman, `ollama` | Cherche d'abord le modèle dans `GET /api/ps` ; quand il est chargé, `POST /api/generate {"model": …, "keep_alive": 0}` et attend `done_reason: "unload"`. Un modèle non chargé n'est jamais demandé : la requête le chargerait d'abord. |
314+| `kronk` | `POST /v1/kronk/models/unload {"id": …}`, qui attend `status: "unloaded"`. Le 404 de Kronk pour un modèle non chargé donne `<modèle> was not loaded: nothing to unload`. |
314315 | `llamacpp` | `POST /models/unload {"model": …}`, qui n'existe qu'en mode router (llama-server lancé sans modèle). Le 404 d'un serveur à modèle unique donne la raison et les alternatives : mode router, `--sleep-idle-seconds N`, arrêt du serveur. |
315316 | `dmr` | `cannot unload <modèle>: the dmr provider cannot unload a model`. |
316317 | Résultat | Une ligne : `unloaded <modèle>`, `<modèle> was not loaded: nothing to unload`, ou `cannot unload <modèle>: <raison>` (serveur injoignable, erreur du serveur lui-même, absence de confirmation). |
@@ -311,6 +311,7 @@ Déclaré quand `unloadModels` vaut true (le défaut), sauf à côté d'une `str
311 | Aspect | Comportement |311 | Aspect | Comportement |
312 |--------|--------------|312 |--------|--------------|
313 | llmman, `ollama` | Cherche d'abord le modèle dans `GET /api/ps` ; quand il est chargé, `POST /api/generate {"model": …, "keep_alive": 0}` et attend `done_reason: "unload"`. Un modèle non chargé n'est jamais demandé : la requête le chargerait d'abord. |313 | llmman, `ollama` | Cherche d'abord le modèle dans `GET /api/ps` ; quand il est chargé, `POST /api/generate {"model": …, "keep_alive": 0}` et attend `done_reason: "unload"`. Un modèle non chargé n'est jamais demandé : la requête le chargerait d'abord. |
314+| `kronk` | `POST /v1/kronk/models/unload {"id": …}`, qui attend `status: "unloaded"`. Le 404 de Kronk pour un modèle non chargé donne `<modèle> was not loaded: nothing to unload`. |
314 | `llamacpp` | `POST /models/unload {"model": …}`, qui n'existe qu'en mode router (llama-server lancé sans modèle). Le 404 d'un serveur à modèle unique donne la raison et les alternatives : mode router, `--sleep-idle-seconds N`, arrêt du serveur. |315 | `llamacpp` | `POST /models/unload {"model": …}`, qui n'existe qu'en mode router (llama-server lancé sans modèle). Le 404 d'un serveur à modèle unique donne la raison et les alternatives : mode router, `--sleep-idle-seconds N`, arrêt du serveur. |
315 | `dmr` | `cannot unload <modèle>: the dmr provider cannot unload a model`. |316 | `dmr` | `cannot unload <modèle>: the dmr provider cannot unload a model`. |
316 | Résultat | Une ligne : `unloaded <modèle>`, `<modèle> was not loaded: nothing to unload`, ou `cannot unload <modèle>: <raison>` (serveur injoignable, erreur du serveur lui-même, absence de confirmation). |317 | Résultat | Une ligne : `unloaded <modèle>`, `<modèle> was not loaded: nothing to unload`, ou `cannot unload <modèle>: <raison>` (serveur injoignable, erreur du serveur lui-même, absence de confirmation). |
modified internal/agentinit/decide.go +12 -2
@@ -176,6 +176,12 @@ func (s *Settings) decideSampling() {
176176 if s.Thinking && values["temperature"] < 0.5 {
177177 values["temperature"], reasons["temperature"], origins["temperature"] = 0.6, "reasoning is on: a thinking model loops at a low temperature", OriginRule
178178 }
179+ profileReason := "Kronk profile " + f.Profile + " (model_config.yaml), read from the server: mm's values replace a profile's, so they are copied here"
180+ for k, v := range f.ProfileSampling {
181+ if k != "max_tokens" {
182+ values[k], reasons[k], origins[k] = v, profileReason, OriginServer
183+ }
184+ }
179185 for _, k := range []string{"temperature", "top_p", "top_k", "min_p"} {
180186 if v, ok := values[k]; ok {
181187 s.Sampling = append(s.Sampling, Setting{Key: k, Value: number(k, v), Reason: reasons[k], Origin: origins[k]})
@@ -185,11 +191,15 @@ func (s *Settings) decideSampling() {
185191 if s.Thinking {
186192 tokens, tokensReason = "16384", "reasoning on: room for the thoughts plus a whole tool call"
187193 }
194+ tokensOrigin := OriginRule
195+ if n, ok := f.ProfileSampling["max_tokens"]; ok {
196+ tokens, tokensReason, tokensOrigin = fmt.Sprint(int(n)), profileReason, OriginServer
197+ }
188198 maxKey := "max_tokens"
189199 if s.Family.MaxTokensKey != "" {
190200 maxKey, tokensReason = s.Family.MaxTokensKey, tokensReason+" (family "+s.Family.Name+" names it "+s.Family.MaxTokensKey+")"
191201 }
192- s.Sampling = append(s.Sampling, Setting{Key: maxKey, Value: tokens, Reason: tokensReason, Origin: OriginRule})
202+ s.Sampling = append(s.Sampling, Setting{Key: maxKey, Value: tokens, Reason: tokensReason, Origin: tokensOrigin})
193203 if s.anyTool() {
194204 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"})
195205 }
@@ -266,7 +276,7 @@ func boolYAML(b bool) string {
266276 }
267277
268278 func number(key string, v float64) string {
269- if key == "top_k" {
279+ if key == "top_k" || key == "max_tokens" {
270280 return fmt.Sprint(int(v))
271281 }
272282 text := fmt.Sprintf("%.2f", v)
@@ -176,6 +176,12 @@ func (s *Settings) decideSampling() {
176 if s.Thinking && values["temperature"] < 0.5 {176 if s.Thinking && values["temperature"] < 0.5 {
177 values["temperature"], reasons["temperature"], origins["temperature"] = 0.6, "reasoning is on: a thinking model loops at a low temperature", OriginRule177 values["temperature"], reasons["temperature"], origins["temperature"] = 0.6, "reasoning is on: a thinking model loops at a low temperature", OriginRule
178 }178 }
179+ profileReason := "Kronk profile " + f.Profile + " (model_config.yaml), read from the server: mm's values replace a profile's, so they are copied here"
180+ for k, v := range f.ProfileSampling {
181+ if k != "max_tokens" {
182+ values[k], reasons[k], origins[k] = v, profileReason, OriginServer
183+ }
184+ }
179 for _, k := range []string{"temperature", "top_p", "top_k", "min_p"} {185 for _, k := range []string{"temperature", "top_p", "top_k", "min_p"} {
180 if v, ok := values[k]; ok {186 if v, ok := values[k]; ok {
181 s.Sampling = append(s.Sampling, Setting{Key: k, Value: number(k, v), Reason: reasons[k], Origin: origins[k]})187 s.Sampling = append(s.Sampling, Setting{Key: k, Value: number(k, v), Reason: reasons[k], Origin: origins[k]})
@@ -185,11 +191,15 @@ func (s *Settings) decideSampling() {
185 if s.Thinking {191 if s.Thinking {
186 tokens, tokensReason = "16384", "reasoning on: room for the thoughts plus a whole tool call"192 tokens, tokensReason = "16384", "reasoning on: room for the thoughts plus a whole tool call"
187 }193 }
194+ tokensOrigin := OriginRule
195+ if n, ok := f.ProfileSampling["max_tokens"]; ok {
196+ tokens, tokensReason, tokensOrigin = fmt.Sprint(int(n)), profileReason, OriginServer
197+ }
188 maxKey := "max_tokens"198 maxKey := "max_tokens"
189 if s.Family.MaxTokensKey != "" {199 if s.Family.MaxTokensKey != "" {
190 maxKey, tokensReason = s.Family.MaxTokensKey, tokensReason+" (family "+s.Family.Name+" names it "+s.Family.MaxTokensKey+")"200 maxKey, tokensReason = s.Family.MaxTokensKey, tokensReason+" (family "+s.Family.Name+" names it "+s.Family.MaxTokensKey+")"
191 }201 }
192- s.Sampling = append(s.Sampling, Setting{Key: maxKey, Value: tokens, Reason: tokensReason, Origin: OriginRule})202+ s.Sampling = append(s.Sampling, Setting{Key: maxKey, Value: tokens, Reason: tokensReason, Origin: tokensOrigin})
193 if s.anyTool() {203 if s.anyTool() {
194 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"})204 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"})
195 }205 }
@@ -266,7 +276,7 @@ func boolYAML(b bool) string {
266 }276 }
267 277
268 func number(key string, v float64) string {278 func number(key string, v float64) string {
269- if key == "top_k" {279+ if key == "top_k" || key == "max_tokens" {
270 return fmt.Sprint(int(v))280 return fmt.Sprint(int(v))
271 }281 }
272 text := fmt.Sprintf("%.2f", v)282 text := fmt.Sprintf("%.2f", v)
modified internal/agentinit/decide_test.go +6 -0
@@ -69,6 +69,12 @@ func TestDecideSampling(t *testing.T) {
6969 map[string]string{"temperature": "1.0"}, map[string]string{"reasoning_effort": "medium"}},
7070 {"no switch: no kwargs, no reasoning", Choices{Model: "llama3.2:3b", Usage: "developer", Machine: "small"}, Facts{},
7171 map[string]string{"temperature": "0.2", "max_tokens": "4096"}, map[string]string{}},
72+ {"a Kronk profile's sampling wins over everything, max_tokens included", Choices{Provider: "kronk", Model: "m/Qwopus/AGENT", Usage: "docs", Machine: "small"},
73+ Facts{Known: true, Sampling: map[string]float64{"temperature": 0.6}, Profile: "AGENT", ProfileSampling: map[string]float64{"temperature": 1, "top_p": 0.95, "top_k": 20, "min_p": 0, "max_tokens": 8192}},
74+ map[string]string{"temperature": "1.0", "top_p": "0.95", "top_k": "20", "min_p": "0.0", "max_tokens": "8192"}, nil},
75+ {"a profile without a max_tokens cap keeps mm's rule", Choices{Provider: "kronk", Model: "m/Qwopus/AGENT", Usage: "developer", Machine: "small"},
76+ Facts{Known: true, Thinking: Thinking{Switch: "enable_thinking"}, Profile: "AGENT", ProfileSampling: map[string]float64{"temperature": 1}},
77+ map[string]string{"temperature": "1.0", "max_tokens": "16384"}, nil},
7278 }
7379 for _, c := range cases {
7480 s := Decide(p, c.c, c.f)
@@ -69,6 +69,12 @@ func TestDecideSampling(t *testing.T) {
69 map[string]string{"temperature": "1.0"}, map[string]string{"reasoning_effort": "medium"}},69 map[string]string{"temperature": "1.0"}, map[string]string{"reasoning_effort": "medium"}},
70 {"no switch: no kwargs, no reasoning", Choices{Model: "llama3.2:3b", Usage: "developer", Machine: "small"}, Facts{},70 {"no switch: no kwargs, no reasoning", Choices{Model: "llama3.2:3b", Usage: "developer", Machine: "small"}, Facts{},
71 map[string]string{"temperature": "0.2", "max_tokens": "4096"}, map[string]string{}},71 map[string]string{"temperature": "0.2", "max_tokens": "4096"}, map[string]string{}},
72+ {"a Kronk profile's sampling wins over everything, max_tokens included", Choices{Provider: "kronk", Model: "m/Qwopus/AGENT", Usage: "docs", Machine: "small"},
73+ Facts{Known: true, Sampling: map[string]float64{"temperature": 0.6}, Profile: "AGENT", ProfileSampling: map[string]float64{"temperature": 1, "top_p": 0.95, "top_k": 20, "min_p": 0, "max_tokens": 8192}},
74+ map[string]string{"temperature": "1.0", "top_p": "0.95", "top_k": "20", "min_p": "0.0", "max_tokens": "8192"}, nil},
75+ {"a profile without a max_tokens cap keeps mm's rule", Choices{Provider: "kronk", Model: "m/Qwopus/AGENT", Usage: "developer", Machine: "small"},
76+ Facts{Known: true, Thinking: Thinking{Switch: "enable_thinking"}, Profile: "AGENT", ProfileSampling: map[string]float64{"temperature": 1}},
77+ map[string]string{"temperature": "1.0", "max_tokens": "16384"}, nil},
72 }78 }
73 for _, c := range cases {79 for _, c := range cases {
74 s := Decide(p, c.c, c.f)80 s := Decide(p, c.c, c.f)
modified internal/agentinit/facts.go +95 -2
@@ -6,6 +6,7 @@ import (
66 "encoding/json"
77 "fmt"
88 "net/http"
9+ neturl "net/url"
910 "os"
1011 "regexp"
1112 "sort"
@@ -23,6 +24,15 @@ type Facts struct {
2324 ContextLength int // the model's native window, 0 when unknown
2425 Sampling map[string]float64 // the GGUF's recommended sampling: temperature, top_p, top_k, min_p
2526 Thinking Thinking
27+ // Profile is the Kronk configuration profile the model id names
28+ // (`provider/modelID/PROFILE`), "" for a plain model. ProfileSampling is
29+ // what the server applies for that profile — temperature, top_p, top_k,
30+ // min_p and, when the profile caps it, max_tokens. mm sends its own
31+ // values with every request and a request's value wins over the
32+ // profile's (measured), so the wizard copies these into the file:
33+ // otherwise the profile's sampling would never apply.
34+ Profile string
35+ ProfileSampling map[string]float64
2636 }
2737
2838 // Thinking is how the chat template switches reasoning.
@@ -155,18 +165,101 @@ func ListModels(ctx context.Context, provider, base, key string) ([]string, erro
155165
156166 // ModelFacts asks the server about one model. llmman and Ollama answer
157167 // /api/show (GGUF metadata, chat template); llama-server answers /props
158-// (window, chat template); Docker Model Runner says nothing more than the
159-// model list — Facts.Known is then false and the family table decides.
168+// (window, chat template); Kronk answers /v1/kronk/models/<id> (GGUF
169+// metadata, chat template, served window); Docker Model Runner says nothing
170+// more than the model list — Facts.Known is then false and the family table
171+// decides.
160172 func ModelFacts(ctx context.Context, provider, base, model string) Facts {
161173 switch provider {
162174 case "llmman", "ollama":
163175 return showFacts(ctx, base, model)
164176 case "llamacpp":
165177 return propsFacts(ctx, base)
178+ case "kronk":
179+ return kronkFacts(ctx, base, model)
166180 }
167181 return Facts{}
168182 }
169183
184+// modelDetail is the line shown beside a model in the wizard's list, "" for
185+// most. Kronk lists configuration profiles beside the models
186+// (`provider/modelID/PROFILE`, e.g. `…/Qwopus3.5-4B-Coder.Q8_0/AGENT`): the
187+// same weights, served with the settings of that key in Kronk's
188+// model_config.yaml (its own window, max_tokens…). A valid choice, but one
189+// that looks like a stray file without a word of explanation.
190+//
191+// modelDetail("kronk", "mradermacher/Qwopus3.5-4B-Coder.Q8_0/AGENT")
192+// // "Kronk profile of mradermacher/Qwopus3.5-4B-Coder.Q8_0: same model, its own settings in model_config.yaml"
193+func modelDetail(provider, model string) string {
194+ profile, ok := kronkProfile(provider, model)
195+ if !ok {
196+ return ""
197+ }
198+ base := strings.TrimSuffix(model, "/"+profile)
199+ return "Kronk profile of " + base + ": same model, its own settings in model_config.yaml"
200+}
201+
202+// kronkProfile returns the profile a Kronk model id names: the third
203+// segment of `provider/modelID/PROFILE`.
204+//
205+// kronkProfile("kronk", "mradermacher/Qwopus3.5-4B-Coder.Q8_0/AGENT") // "AGENT", true
206+func kronkProfile(provider, model string) (string, bool) {
207+ if provider != "kronk" || strings.Count(model, "/") != 2 {
208+ return "", false
209+ }
210+ return model[strings.LastIndex(model, "/")+1:], true
211+}
212+
213+// kronkFacts reads Kronk's model details (measured on Kronk 1.33.2): the
214+// GGUF metadata as strings — `general.architecture`, `<arch>.context_length`,
215+// `general.sampling.*`, `tokenizer.chat_template` — and the configuration
216+// it serves the model with, whose `context-window` is the window actually
217+// served (131072 for a model trained on 262144). The id holds a "/", so it
218+// goes in the path escaped.
219+func kronkFacts(ctx context.Context, base, model string) Facts {
220+ var details struct {
221+ Metadata map[string]string `json:"metadata"`
222+ Config struct {
223+ ContextWindow int `json:"context-window"`
224+ Sampling map[string]any `json:"sampling-parameters"`
225+ } `json:"model_config"`
226+ }
227+ url := root(base) + "/v1/kronk/models/" + neturl.PathEscape(model)
228+ if err := call(ctx, http.MethodGet, url, nil, 30*time.Second, &details); err != nil {
229+ return Facts{}
230+ }
231+ arch := details.Metadata["general.architecture"]
232+ f := Facts{Known: true, ServerFamily: arch, Sampling: map[string]float64{}, Thinking: templateThinking(details.Metadata["tokenizer.chat_template"])}
233+ f.ContextLength = details.Config.ContextWindow
234+ if f.ContextLength == 0 {
235+ f.ContextLength, _ = strconv.Atoi(details.Metadata[arch+".context_length"])
236+ }
237+ for key, value := range details.Metadata {
238+ if name, ok := strings.CutPrefix(key, "general.sampling."); ok {
239+ if n, err := strconv.ParseFloat(value, 64); err == nil {
240+ f.Sampling[samplingKey(name)] = n
241+ }
242+ }
243+ }
244+ if profile, ok := kronkProfile("kronk", model); ok {
245+ f.Profile, f.ProfileSampling = profile, profileSampling(details.Config.Sampling)
246+ }
247+ return f
248+}
249+
250+// profileSampling keeps, from a Kronk profile's `sampling-parameters`, the
251+// keys mm's file sets: the four sampling numbers, and max_tokens when the
252+// profile caps it (0 means no cap).
253+func profileSampling(params map[string]any) map[string]float64 {
254+ out := map[string]float64{}
255+ for _, k := range []string{"temperature", "top_p", "top_k", "min_p", "max_tokens"} {
256+ if n, ok := params[k].(float64); ok && (k != "max_tokens" || n > 0) {
257+ out[k] = n
258+ }
259+ }
260+ return out
261+}
262+
170263 func showFacts(ctx context.Context, base, model string) Facts {
171264 var show struct {
172265 ModelInfo map[string]any `json:"model_info"`
@@ -6,6 +6,7 @@ import (
6 "encoding/json"6 "encoding/json"
7 "fmt"7 "fmt"
8 "net/http"8 "net/http"
9+ neturl "net/url"
9 "os"10 "os"
10 "regexp"11 "regexp"
11 "sort"12 "sort"
@@ -23,6 +24,15 @@ type Facts struct {
23 ContextLength int // the model's native window, 0 when unknown24 ContextLength int // the model's native window, 0 when unknown
24 Sampling map[string]float64 // the GGUF's recommended sampling: temperature, top_p, top_k, min_p25 Sampling map[string]float64 // the GGUF's recommended sampling: temperature, top_p, top_k, min_p
25 Thinking Thinking26 Thinking Thinking
27+ // Profile is the Kronk configuration profile the model id names
28+ // (`provider/modelID/PROFILE`), "" for a plain model. ProfileSampling is
29+ // what the server applies for that profile — temperature, top_p, top_k,
30+ // min_p and, when the profile caps it, max_tokens. mm sends its own
31+ // values with every request and a request's value wins over the
32+ // profile's (measured), so the wizard copies these into the file:
33+ // otherwise the profile's sampling would never apply.
34+ Profile string
35+ ProfileSampling map[string]float64
26 }36 }
27 37
28 // Thinking is how the chat template switches reasoning.38 // Thinking is how the chat template switches reasoning.
@@ -155,18 +165,101 @@ func ListModels(ctx context.Context, provider, base, key string) ([]string, erro
155 165
156 // ModelFacts asks the server about one model. llmman and Ollama answer166 // ModelFacts asks the server about one model. llmman and Ollama answer
157 // /api/show (GGUF metadata, chat template); llama-server answers /props167 // /api/show (GGUF metadata, chat template); llama-server answers /props
158-// (window, chat template); Docker Model Runner says nothing more than the168+// (window, chat template); Kronk answers /v1/kronk/models/<id> (GGUF
159-// model list — Facts.Known is then false and the family table decides.169+// metadata, chat template, served window); Docker Model Runner says nothing
170+// more than the model list — Facts.Known is then false and the family table
171+// decides.
160 func ModelFacts(ctx context.Context, provider, base, model string) Facts {172 func ModelFacts(ctx context.Context, provider, base, model string) Facts {
161 switch provider {173 switch provider {
162 case "llmman", "ollama":174 case "llmman", "ollama":
163 return showFacts(ctx, base, model)175 return showFacts(ctx, base, model)
164 case "llamacpp":176 case "llamacpp":
165 return propsFacts(ctx, base)177 return propsFacts(ctx, base)
178+ case "kronk":
179+ return kronkFacts(ctx, base, model)
166 }180 }
167 return Facts{}181 return Facts{}
168 }182 }
169 183
184+// modelDetail is the line shown beside a model in the wizard's list, "" for
185+// most. Kronk lists configuration profiles beside the models
186+// (`provider/modelID/PROFILE`, e.g. `…/Qwopus3.5-4B-Coder.Q8_0/AGENT`): the
187+// same weights, served with the settings of that key in Kronk's
188+// model_config.yaml (its own window, max_tokens…). A valid choice, but one
189+// that looks like a stray file without a word of explanation.
190+//
191+// modelDetail("kronk", "mradermacher/Qwopus3.5-4B-Coder.Q8_0/AGENT")
192+// // "Kronk profile of mradermacher/Qwopus3.5-4B-Coder.Q8_0: same model, its own settings in model_config.yaml"
193+func modelDetail(provider, model string) string {
194+ profile, ok := kronkProfile(provider, model)
195+ if !ok {
196+ return ""
197+ }
198+ base := strings.TrimSuffix(model, "/"+profile)
199+ return "Kronk profile of " + base + ": same model, its own settings in model_config.yaml"
200+}
201+
202+// kronkProfile returns the profile a Kronk model id names: the third
203+// segment of `provider/modelID/PROFILE`.
204+//
205+// kronkProfile("kronk", "mradermacher/Qwopus3.5-4B-Coder.Q8_0/AGENT") // "AGENT", true
206+func kronkProfile(provider, model string) (string, bool) {
207+ if provider != "kronk" || strings.Count(model, "/") != 2 {
208+ return "", false
209+ }
210+ return model[strings.LastIndex(model, "/")+1:], true
211+}
212+
213+// kronkFacts reads Kronk's model details (measured on Kronk 1.33.2): the
214+// GGUF metadata as strings — `general.architecture`, `<arch>.context_length`,
215+// `general.sampling.*`, `tokenizer.chat_template` — and the configuration
216+// it serves the model with, whose `context-window` is the window actually
217+// served (131072 for a model trained on 262144). The id holds a "/", so it
218+// goes in the path escaped.
219+func kronkFacts(ctx context.Context, base, model string) Facts {
220+ var details struct {
221+ Metadata map[string]string `json:"metadata"`
222+ Config struct {
223+ ContextWindow int `json:"context-window"`
224+ Sampling map[string]any `json:"sampling-parameters"`
225+ } `json:"model_config"`
226+ }
227+ url := root(base) + "/v1/kronk/models/" + neturl.PathEscape(model)
228+ if err := call(ctx, http.MethodGet, url, nil, 30*time.Second, &details); err != nil {
229+ return Facts{}
230+ }
231+ arch := details.Metadata["general.architecture"]
232+ f := Facts{Known: true, ServerFamily: arch, Sampling: map[string]float64{}, Thinking: templateThinking(details.Metadata["tokenizer.chat_template"])}
233+ f.ContextLength = details.Config.ContextWindow
234+ if f.ContextLength == 0 {
235+ f.ContextLength, _ = strconv.Atoi(details.Metadata[arch+".context_length"])
236+ }
237+ for key, value := range details.Metadata {
238+ if name, ok := strings.CutPrefix(key, "general.sampling."); ok {
239+ if n, err := strconv.ParseFloat(value, 64); err == nil {
240+ f.Sampling[samplingKey(name)] = n
241+ }
242+ }
243+ }
244+ if profile, ok := kronkProfile("kronk", model); ok {
245+ f.Profile, f.ProfileSampling = profile, profileSampling(details.Config.Sampling)
246+ }
247+ return f
248+}
249+
250+// profileSampling keeps, from a Kronk profile's `sampling-parameters`, the
251+// keys mm's file sets: the four sampling numbers, and max_tokens when the
252+// profile caps it (0 means no cap).
253+func profileSampling(params map[string]any) map[string]float64 {
254+ out := map[string]float64{}
255+ for _, k := range []string{"temperature", "top_p", "top_k", "min_p", "max_tokens"} {
256+ if n, ok := params[k].(float64); ok && (k != "max_tokens" || n > 0) {
257+ out[k] = n
258+ }
259+ }
260+ return out
261+}
262+
170 func showFacts(ctx context.Context, base, model string) Facts {263 func showFacts(ctx context.Context, base, model string) Facts {
171 var show struct {264 var show struct {
172 ModelInfo map[string]any `json:"model_info"`265 ModelInfo map[string]any `json:"model_info"`
modified internal/agentinit/facts_test.go +72 -0
@@ -112,3 +112,75 @@ func TestModelsOpenAISendsTheKey(t *testing.T) {
112112 t.Errorf("another provider sent a key: %q", auth)
113113 }
114114 }
115+
116+// Kronk: the metadata come as strings; the window is the one the server is
117+// configured to serve, the trained length only when there is none; the id's
118+// "/" travels escaped (r.URL.Path is the decoded path).
119+func TestKronkFacts(t *testing.T) {
120+ details := map[string]any{
121+ "metadata": map[string]any{
122+ "general.architecture": "qwen35",
123+ "qwen35.context_length": "262144",
124+ "general.sampling.temp": "0.6",
125+ "general.sampling.top_k": "20",
126+ "tokenizer.chat_template": "{%- if enable_thinking is defined and enable_thinking is false %}…",
127+ },
128+ "model_config": map[string]any{"context-window": 131072},
129+ }
130+ s := fakeServer(t, map[string]any{"GET /v1/kronk/models/mradermacher/Qwopus3.5-4B-Coder.Q8_0": details})
131+ f := ModelFacts(context.Background(), "kronk", s.URL+"/v1", "mradermacher/Qwopus3.5-4B-Coder.Q8_0")
132+ want := Facts{Known: true, ServerFamily: "qwen35", ContextLength: 131072,
133+ Sampling: map[string]float64{"temperature": 0.6, "top_k": 20},
134+ Thinking: Thinking{Switch: "enable_thinking", Default: "on"}}
135+ if !reflect.DeepEqual(f, want) {
136+ t.Errorf("facts = %+v\nwant %+v", f, want)
137+ }
138+
139+ details["model_config"] = map[string]any{}
140+ if f := ModelFacts(context.Background(), "kronk", s.URL+"/v1", "mradermacher/Qwopus3.5-4B-Coder.Q8_0"); f.ContextLength != 262144 {
141+ t.Errorf("no configured window: ContextLength = %d, want the trained 262144", f.ContextLength)
142+ }
143+}
144+
145+// A Kronk profile (provider/modelID/PROFILE) is explained in the list; a
146+// plain model, or another provider's three-part name, is not.
147+func TestModelDetail(t *testing.T) {
148+ cases := []struct{ provider, model, want string }{
149+ {"kronk", "mradermacher/Qwopus3.5-4B-Coder.Q8_0/AGENT", "Kronk profile of mradermacher/Qwopus3.5-4B-Coder.Q8_0: same model, its own settings in model_config.yaml"},
150+ {"kronk", "mradermacher/Qwopus3.5-4B-Coder.Q8_0", ""},
151+ {"llmman", "hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M", ""},
152+ }
153+ for _, c := range cases {
154+ if got := modelDetail(c.provider, c.model); got != c.want {
155+ t.Errorf("modelDetail(%s, %s) = %q, want %q", c.provider, c.model, got, c.want)
156+ }
157+ }
158+}
159+
160+// A Kronk profile: the server's sampling for that profile is read, and
161+// max_tokens only when the profile caps it.
162+func TestKronkFactsReadsAProfileSampling(t *testing.T) {
163+ details := map[string]any{
164+ "metadata": map[string]any{"general.architecture": "qwen35"},
165+ "model_config": map[string]any{
166+ "context-window": 65536,
167+ "sampling-parameters": map[string]any{
168+ "temperature": 1, "top_k": 20, "top_p": 0.95, "min_p": 0, "max_tokens": 8192,
169+ "repeat_penalty": 1, "enable_thinking": "true",
170+ },
171+ },
172+ }
173+ s := fakeServer(t, map[string]any{"GET /v1/kronk/models/m/Qwopus/AGENT": details, "GET /v1/kronk/models/m/Qwopus": details})
174+ f := ModelFacts(context.Background(), "kronk", s.URL+"/v1", "m/Qwopus/AGENT")
175+ want := map[string]float64{"temperature": 1, "top_k": 20, "top_p": 0.95, "min_p": 0, "max_tokens": 8192}
176+ if f.Profile != "AGENT" || !reflect.DeepEqual(f.ProfileSampling, want) {
177+ t.Errorf("profile = %q %v, want AGENT %v", f.Profile, f.ProfileSampling, want)
178+ }
179+ if f := ModelFacts(context.Background(), "kronk", s.URL+"/v1", "m/Qwopus"); f.Profile != "" || f.ProfileSampling != nil {
180+ t.Errorf("a plain model has no profile: %q %v", f.Profile, f.ProfileSampling)
181+ }
182+ details["model_config"].(map[string]any)["sampling-parameters"].(map[string]any)["max_tokens"] = 0
183+ if f := ModelFacts(context.Background(), "kronk", s.URL+"/v1", "m/Qwopus/AGENT"); f.ProfileSampling["max_tokens"] != 0 || len(f.ProfileSampling) != 4 {
184+ t.Errorf("max_tokens 0 = no cap, not copied: %v", f.ProfileSampling)
185+ }
186+}
@@ -112,3 +112,75 @@ func TestModelsOpenAISendsTheKey(t *testing.T) {
112 t.Errorf("another provider sent a key: %q", auth)112 t.Errorf("another provider sent a key: %q", auth)
113 }113 }
114 }114 }
115+
116+// Kronk: the metadata come as strings; the window is the one the server is
117+// configured to serve, the trained length only when there is none; the id's
118+// "/" travels escaped (r.URL.Path is the decoded path).
119+func TestKronkFacts(t *testing.T) {
120+ details := map[string]any{
121+ "metadata": map[string]any{
122+ "general.architecture": "qwen35",
123+ "qwen35.context_length": "262144",
124+ "general.sampling.temp": "0.6",
125+ "general.sampling.top_k": "20",
126+ "tokenizer.chat_template": "{%- if enable_thinking is defined and enable_thinking is false %}…",
127+ },
128+ "model_config": map[string]any{"context-window": 131072},
129+ }
130+ s := fakeServer(t, map[string]any{"GET /v1/kronk/models/mradermacher/Qwopus3.5-4B-Coder.Q8_0": details})
131+ f := ModelFacts(context.Background(), "kronk", s.URL+"/v1", "mradermacher/Qwopus3.5-4B-Coder.Q8_0")
132+ want := Facts{Known: true, ServerFamily: "qwen35", ContextLength: 131072,
133+ Sampling: map[string]float64{"temperature": 0.6, "top_k": 20},
134+ Thinking: Thinking{Switch: "enable_thinking", Default: "on"}}
135+ if !reflect.DeepEqual(f, want) {
136+ t.Errorf("facts = %+v\nwant %+v", f, want)
137+ }
138+
139+ details["model_config"] = map[string]any{}
140+ if f := ModelFacts(context.Background(), "kronk", s.URL+"/v1", "mradermacher/Qwopus3.5-4B-Coder.Q8_0"); f.ContextLength != 262144 {
141+ t.Errorf("no configured window: ContextLength = %d, want the trained 262144", f.ContextLength)
142+ }
143+}
144+
145+// A Kronk profile (provider/modelID/PROFILE) is explained in the list; a
146+// plain model, or another provider's three-part name, is not.
147+func TestModelDetail(t *testing.T) {
148+ cases := []struct{ provider, model, want string }{
149+ {"kronk", "mradermacher/Qwopus3.5-4B-Coder.Q8_0/AGENT", "Kronk profile of mradermacher/Qwopus3.5-4B-Coder.Q8_0: same model, its own settings in model_config.yaml"},
150+ {"kronk", "mradermacher/Qwopus3.5-4B-Coder.Q8_0", ""},
151+ {"llmman", "hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M", ""},
152+ }
153+ for _, c := range cases {
154+ if got := modelDetail(c.provider, c.model); got != c.want {
155+ t.Errorf("modelDetail(%s, %s) = %q, want %q", c.provider, c.model, got, c.want)
156+ }
157+ }
158+}
159+
160+// A Kronk profile: the server's sampling for that profile is read, and
161+// max_tokens only when the profile caps it.
162+func TestKronkFactsReadsAProfileSampling(t *testing.T) {
163+ details := map[string]any{
164+ "metadata": map[string]any{"general.architecture": "qwen35"},
165+ "model_config": map[string]any{
166+ "context-window": 65536,
167+ "sampling-parameters": map[string]any{
168+ "temperature": 1, "top_k": 20, "top_p": 0.95, "min_p": 0, "max_tokens": 8192,
169+ "repeat_penalty": 1, "enable_thinking": "true",
170+ },
171+ },
172+ }
173+ s := fakeServer(t, map[string]any{"GET /v1/kronk/models/m/Qwopus/AGENT": details, "GET /v1/kronk/models/m/Qwopus": details})
174+ f := ModelFacts(context.Background(), "kronk", s.URL+"/v1", "m/Qwopus/AGENT")
175+ want := map[string]float64{"temperature": 1, "top_k": 20, "top_p": 0.95, "min_p": 0, "max_tokens": 8192}
176+ if f.Profile != "AGENT" || !reflect.DeepEqual(f.ProfileSampling, want) {
177+ t.Errorf("profile = %q %v, want AGENT %v", f.Profile, f.ProfileSampling, want)
178+ }
179+ if f := ModelFacts(context.Background(), "kronk", s.URL+"/v1", "m/Qwopus"); f.Profile != "" || f.ProfileSampling != nil {
180+ t.Errorf("a plain model has no profile: %q %v", f.Profile, f.ProfileSampling)
181+ }
182+ details["model_config"].(map[string]any)["sampling-parameters"].(map[string]any)["max_tokens"] = 0
183+ if f := ModelFacts(context.Background(), "kronk", s.URL+"/v1", "m/Qwopus/AGENT"); f.ProfileSampling["max_tokens"] != 0 || len(f.ProfileSampling) != 4 {
184+ t.Errorf("max_tokens 0 = no cap, not copied: %v", f.ProfileSampling)
185+ }
186+}
modified internal/agentinit/flow.go +1 -1
@@ -192,7 +192,7 @@ func (f *Flow) Step(key string) Step {
192192 break
193193 }
194194 for _, m := range models {
195- st.Options = append(st.Options, Option{Value: m, Label: m})
195+ st.Options = append(st.Options, Option{Value: m, Label: m, Detail: modelDetail(f.c.Provider, m)})
196196 }
197197 st.Default = models[0]
198198 case "family":
@@ -192,7 +192,7 @@ func (f *Flow) Step(key string) Step {
192 break192 break
193 }193 }
194 for _, m := range models {194 for _, m := range models {
195- st.Options = append(st.Options, Option{Value: m, Label: m})195+ st.Options = append(st.Options, Option{Value: m, Label: m, Detail: modelDetail(f.c.Provider, m)})
196 }196 }
197 st.Default = models[0]197 st.Default = models[0]
198 case "family":198 case "family":
modified internal/agentinit/render.go +9 -0
@@ -189,6 +189,15 @@ func factsLine(s Settings) string {
189189 }
190190 parts = append(parts, "GGUF sampling "+strings.Join(keys, ", "))
191191 }
192+ if f.Profile != "" {
193+ keys := make([]string, 0, len(f.ProfileSampling))
194+ for _, k := range []string{"temperature", "top_p", "top_k", "min_p", "max_tokens"} {
195+ if v, ok := f.ProfileSampling[k]; ok {
196+ keys = append(keys, k+" "+number(k, v))
197+ }
198+ }
199+ parts = append(parts, "Kronk profile "+f.Profile+" sampling "+strings.Join(keys, ", "))
200+ }
192201 if f.Thinking.Switch != "" {
193202 sw := "reasoning switch " + f.Thinking.Switch
194203 if f.Thinking.Default != "" {
@@ -189,6 +189,15 @@ func factsLine(s Settings) string {
189 }189 }
190 parts = append(parts, "GGUF sampling "+strings.Join(keys, ", "))190 parts = append(parts, "GGUF sampling "+strings.Join(keys, ", "))
191 }191 }
192+ if f.Profile != "" {
193+ keys := make([]string, 0, len(f.ProfileSampling))
194+ for _, k := range []string{"temperature", "top_p", "top_k", "min_p", "max_tokens"} {
195+ if v, ok := f.ProfileSampling[k]; ok {
196+ keys = append(keys, k+" "+number(k, v))
197+ }
198+ }
199+ parts = append(parts, "Kronk profile "+f.Profile+" sampling "+strings.Join(keys, ", "))
200+ }
192 if f.Thinking.Switch != "" {201 if f.Thinking.Switch != "" {
193 sw := "reasoning switch " + f.Thinking.Switch202 sw := "reasoning switch " + f.Thinking.Switch
194 if f.Thinking.Default != "" {203 if f.Thinking.Default != "" {
modified internal/agentinit/wizard.go +11 -2
@@ -19,12 +19,12 @@ import (
1919 )
2020
2121 // providers are the servers mm speaks to, in the order they are offered.
22-var providers = []string{"llmman", "ollama", "dmr", "llamacpp", "openai"}
22+var providers = []string{"llmman", "ollama", "dmr", "llamacpp", "kronk", "openai"}
2323
2424 // local are the providers probed for the first question's default: never
2525 // `openai`, whose default address is the OpenAI API — mm init must not call
2626 // a remote service nobody asked for.
27-var local = []string{"llmman", "ollama", "dmr", "llamacpp"}
27+var local = []string{"llmman", "ollama", "dmr", "llamacpp", "kronk"}
2828
2929 // Window asks the flow's questions in a full-screen window (internal/tui's
3030 // RunInit): it returns false when the user quit without writing. main hands
@@ -236,6 +236,15 @@ func (w *lineWizard) ask(st Step) string {
236236 fmt.Fprintf(w.out, " %-12s %s\n", o.Label, o.Detail)
237237 }
238238 }
239+ // A model rarely needs a word; when one does (a Kronk profile), the
240+ // note goes above the list, with the identifier it explains.
241+ if st.Key == "model" {
242+ for _, o := range st.Options {
243+ if o.Detail != "" {
244+ fmt.Fprintf(w.out, " %s — %s\n", o.Label, o.Detail)
245+ }
246+ }
247+ }
239248 for {
240249 lines := elicit.Lines(q)
241250 // Here /skip and an empty line take the default; nothing is declined.
@@ -19,12 +19,12 @@ import (
19 )19 )
20 20
21 // providers are the servers mm speaks to, in the order they are offered.21 // providers are the servers mm speaks to, in the order they are offered.
22-var providers = []string{"llmman", "ollama", "dmr", "llamacpp", "openai"}22+var providers = []string{"llmman", "ollama", "dmr", "llamacpp", "kronk", "openai"}
23 23
24 // local are the providers probed for the first question's default: never24 // local are the providers probed for the first question's default: never
25 // `openai`, whose default address is the OpenAI API — mm init must not call25 // `openai`, whose default address is the OpenAI API — mm init must not call
26 // a remote service nobody asked for.26 // a remote service nobody asked for.
27-var local = []string{"llmman", "ollama", "dmr", "llamacpp"}27+var local = []string{"llmman", "ollama", "dmr", "llamacpp", "kronk"}
28 28
29 // Window asks the flow's questions in a full-screen window (internal/tui's29 // Window asks the flow's questions in a full-screen window (internal/tui's
30 // RunInit): it returns false when the user quit without writing. main hands30 // RunInit): it returns false when the user quit without writing. main hands
@@ -236,6 +236,15 @@ func (w *lineWizard) ask(st Step) string {
236 fmt.Fprintf(w.out, " %-12s %s\n", o.Label, o.Detail)236 fmt.Fprintf(w.out, " %-12s %s\n", o.Label, o.Detail)
237 }237 }
238 }238 }
239+ // A model rarely needs a word; when one does (a Kronk profile), the
240+ // note goes above the list, with the identifier it explains.
241+ if st.Key == "model" {
242+ for _, o := range st.Options {
243+ if o.Detail != "" {
244+ fmt.Fprintf(w.out, " %s — %s\n", o.Label, o.Detail)
245+ }
246+ }
247+ }
239 for {248 for {
240 lines := elicit.Lines(q)249 lines := elicit.Lines(q)
241 // Here /skip and an empty line take the default; nothing is declined.250 // Here /skip and an empty line take the default; nothing is declined.
added internal/engine/kronk.go +184 -0
new file mode 100644
@@ -0,0 +1,184 @@
1+package engine
2+
3+import (
4+ "bufio"
5+ "context"
6+ "encoding/json"
7+ "errors"
8+ "fmt"
9+ "io"
10+ "net/http"
11+ "net/url"
12+ "strings"
13+)
14+
15+// Kronk (ticket 75, https://github.com/ardanlabs/kronk) is a Go server that
16+// embeds llama.cpp — no llama-server process — and speaks the OpenAI
17+// protocol under /v1. What differs from the other servers is on its own
18+// /v1/kronk/… routes. Measured on Kronk 1.33.2, 2026-11-11, from an sbx
19+// sandbox on the Mac's server (mradermacher/Qwopus3.5-4B-Coder.Q8_0):
20+//
21+// - model ids are `provider/modelID` and contain a "/": a route that takes
22+// the id in its path needs it escaped (%2F), a raw "/" gives 404 or 400;
23+// - GET /v1/kronk/models/<id> gives the window the model is served with,
24+// `model_config.context-window` — loaded or not, so it is known at
25+// start-up (the GGUF's trained length, in `metadata`, can be larger);
26+// - an installed model loads on the first request; a missing one is a 404
27+// `not_found`, so the probe warns and `autoPull` downloads it;
28+// - POST /v1/kronk/models/pull {"model_url": id} streams Server-Sent Events,
29+// HTTP 200 whatever happens (kronkPullStream reads the outcome);
30+// - POST /v1/kronk/models/unload {"id": id}: {"status":"unloaded"}, or a
31+// 404 when the model is not loaded;
32+// - no prompt_progress; reasoning in `reasoning_content`, switched off by
33+// chat_template_kwargs (reasoning_effort "none" is ignored).
34+
35+// kronkModelPath is the route of one model under /v1/kronk/models, with the
36+// id escaped as one path segment.
37+//
38+// kronkModelPath("http://127.0.0.1:11435/v1", "unsloth/Qwen3-0.6B-Q8_0")
39+// // "http://127.0.0.1:11435/v1/kronk/models/unsloth%2FQwen3-0.6B-Q8_0"
40+func kronkModelPath(baseURL, model string) string {
41+ return rootURL(baseURL) + "/v1/kronk/models/" + url.PathEscape(model)
42+}
43+
44+// probeKronk: reachable when /v1/models answers, a warning when the model is
45+// not installed, and the served window from the model's Kronk details. The
46+// key is sent: in Kronk's `authenticated` mode the model list needs it, and
47+// a refused key is worth saying before the first question, naming the
48+// variable it came from (b.APIKeyEnv, KRONK_TOKEN unless the yaml says).
49+func probeKronk(ctx context.Context, b Backend) Info {
50+ ctx, cancel := context.WithTimeout(ctx, probeClient.Timeout)
51+ defer cancel()
52+
53+ var list modelList
54+ err := getWithKey(ctx, b, strings.TrimSuffix(b.BaseURL, "/")+"/models", &list)
55+ var status *httpStatusError
56+ switch {
57+ case errors.As(err, &status) && (status.code == http.StatusUnauthorized || status.code == http.StatusForbidden):
58+ return Info{Reachable: true, Warnings: []string{
59+ fmt.Sprintf("kronk: the server refuses the key (%d) — is $%s set to a valid token?", status.code, b.APIKeyEnv),
60+ }}
61+ case err != nil:
62+ return Info{}
63+ }
64+
65+ info := Info{Reachable: true}
66+ if !list.has(b.Model) {
67+ info.Warnings = append(info.Warnings,
68+ fmt.Sprintf("kronk: model %q is not installed — kronk model pull %s (or autoPull: true)", b.Model, b.Model))
69+ return info
70+ }
71+ var details struct {
72+ Config struct {
73+ ContextWindow int `json:"context-window"`
74+ } `json:"model_config"`
75+ }
76+ if getWithKey(ctx, b, kronkModelPath(b.BaseURL, b.Model), &details) == nil && details.Config.ContextWindow > 0 {
77+ info.ContextWindow, info.ContextSource = details.Config.ContextWindow, "/v1/kronk/models"
78+ }
79+ return info
80+}
81+
82+// pullKronk downloads model with POST /v1/kronk/models/pull. The answer is
83+// always 200 once the stream has started; the stream itself says whether the
84+// download worked.
85+func pullKronk(ctx context.Context, b Backend, model string, progress func(PullProgress)) error {
86+ body, err := json.Marshal(map[string]string{"model_url": model})
87+ if err != nil {
88+ return err
89+ }
90+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, rootURL(b.BaseURL)+"/v1/kronk/models/pull", strings.NewReader(string(body)))
91+ if err != nil {
92+ return err
93+ }
94+ req.Header.Set("Content-Type", "application/json")
95+ if b.APIKey != "" {
96+ req.Header.Set("Authorization", "Bearer "+b.APIKey)
97+ }
98+ resp, err := pullClient.Do(req)
99+ if err != nil {
100+ return fmt.Errorf("the server cannot be reached: %w", err)
101+ }
102+ defer func() { _ = resp.Body.Close() }()
103+ if resp.StatusCode/100 != 2 {
104+ text, _ := io.ReadAll(io.LimitReader(resp.Body, 300))
105+ return &httpStatusError{code: resp.StatusCode, body: strings.TrimSpace(string(text))}
106+ }
107+ return readKronkPullStream(resp.Body, model, progress)
108+}
109+
110+// kronkPullStep is one `data:` record of the pull stream. Every record has a
111+// status; a file being downloaded adds `progress`; the last record of a
112+// download that worked says `"downloaded": true`.
113+type kronkPullStep struct {
114+ Status string `json:"status"`
115+ Downloaded bool `json:"downloaded"`
116+ Progress *struct {
117+ CurrentBytes int64 `json:"current_bytes"`
118+ TotalBytes int64 `json:"total_bytes"`
119+ } `json:"progress"`
120+}
121+
122+// readKronkPullStream reads the Server-Sent Events of a pull. Kronk reports a
123+// failure as a last status with no `downloaded` flag (HTTP 200 all the same):
124+// a stream that ends without `"downloaded": true` failed, and its last status
125+// is the server's reason.
126+//
127+// data: {"status":"download-model: Downloading … 120 MB of 4610 MB …","progress":{…}}
128+// data: {"status":"downloaded","model_files":["…"],"downloaded":true}
129+func readKronkPullStream(r io.Reader, model string, progress func(PullProgress)) error {
130+ scanner := bufio.NewScanner(r)
131+ scanner.Buffer(make([]byte, 64*1024), 1024*1024)
132+ last := ""
133+ for scanner.Scan() {
134+ line := strings.TrimSpace(scanner.Text())
135+ payload, ok := strings.CutPrefix(line, "data:")
136+ if !ok {
137+ continue
138+ }
139+ var step kronkPullStep
140+ if err := json.Unmarshal([]byte(strings.TrimSpace(payload)), &step); err != nil {
141+ return fmt.Errorf("unreadable pull progress: %q", line)
142+ }
143+ last = step.Status
144+ if step.Downloaded {
145+ if progress != nil {
146+ progress(PullProgress{Model: model, Status: "success"})
147+ }
148+ return nil
149+ }
150+ if progress != nil {
151+ p := PullProgress{Model: model, Status: step.Status}
152+ if step.Progress != nil {
153+ p.Completed, p.Total = step.Progress.CurrentBytes, step.Progress.TotalBytes
154+ }
155+ progress(p)
156+ }
157+ }
158+ if err := scanner.Err(); err != nil {
159+ return fmt.Errorf("the download was cut: %w", err)
160+ }
161+ if last == "" {
162+ return errors.New("the download ended without success")
163+ }
164+ return fmt.Errorf("the server says: %s", last)
165+}
166+
167+// unloadKronk unloads model with POST /v1/kronk/models/unload. A model that
168+// is not loaded answers 404: that is "nothing to unload", not a failure.
169+func unloadKronk(ctx context.Context, b Backend, model string) (string, error) {
170+ var reply struct {
171+ Status string `json:"status"`
172+ }
173+ err := postJSON(ctx, b, rootURL(b.BaseURL)+"/v1/kronk/models/unload", map[string]string{"id": model}, &reply)
174+ var status *httpStatusError
175+ switch {
176+ case errors.As(err, &status) && status.code == http.StatusNotFound:
177+ return fmt.Sprintf("%s was not loaded: nothing to unload", model), nil
178+ case err != nil:
179+ return "", err
180+ case reply.Status != "unloaded":
181+ return "", fmt.Errorf("the server did not confirm (status %q)", reply.Status)
182+ }
183+ return "unloaded " + model, nil
184+}
new file mode 100644
@@ -0,0 +1,184 @@
1+package engine
2+
3+import (
4+ "bufio"
5+ "context"
6+ "encoding/json"
7+ "errors"
8+ "fmt"
9+ "io"
10+ "net/http"
11+ "net/url"
12+ "strings"
13+)
14+
15+// Kronk (ticket 75, https://github.com/ardanlabs/kronk) is a Go server that
16+// embeds llama.cpp — no llama-server process — and speaks the OpenAI
17+// protocol under /v1. What differs from the other servers is on its own
18+// /v1/kronk/… routes. Measured on Kronk 1.33.2, 2026-11-11, from an sbx
19+// sandbox on the Mac's server (mradermacher/Qwopus3.5-4B-Coder.Q8_0):
20+//
21+// - model ids are `provider/modelID` and contain a "/": a route that takes
22+// the id in its path needs it escaped (%2F), a raw "/" gives 404 or 400;
23+// - GET /v1/kronk/models/<id> gives the window the model is served with,
24+// `model_config.context-window` — loaded or not, so it is known at
25+// start-up (the GGUF's trained length, in `metadata`, can be larger);
26+// - an installed model loads on the first request; a missing one is a 404
27+// `not_found`, so the probe warns and `autoPull` downloads it;
28+// - POST /v1/kronk/models/pull {"model_url": id} streams Server-Sent Events,
29+// HTTP 200 whatever happens (kronkPullStream reads the outcome);
30+// - POST /v1/kronk/models/unload {"id": id}: {"status":"unloaded"}, or a
31+// 404 when the model is not loaded;
32+// - no prompt_progress; reasoning in `reasoning_content`, switched off by
33+// chat_template_kwargs (reasoning_effort "none" is ignored).
34+
35+// kronkModelPath is the route of one model under /v1/kronk/models, with the
36+// id escaped as one path segment.
37+//
38+// kronkModelPath("http://127.0.0.1:11435/v1", "unsloth/Qwen3-0.6B-Q8_0")
39+// // "http://127.0.0.1:11435/v1/kronk/models/unsloth%2FQwen3-0.6B-Q8_0"
40+func kronkModelPath(baseURL, model string) string {
41+ return rootURL(baseURL) + "/v1/kronk/models/" + url.PathEscape(model)
42+}
43+
44+// probeKronk: reachable when /v1/models answers, a warning when the model is
45+// not installed, and the served window from the model's Kronk details. The
46+// key is sent: in Kronk's `authenticated` mode the model list needs it, and
47+// a refused key is worth saying before the first question, naming the
48+// variable it came from (b.APIKeyEnv, KRONK_TOKEN unless the yaml says).
49+func probeKronk(ctx context.Context, b Backend) Info {
50+ ctx, cancel := context.WithTimeout(ctx, probeClient.Timeout)
51+ defer cancel()
52+
53+ var list modelList
54+ err := getWithKey(ctx, b, strings.TrimSuffix(b.BaseURL, "/")+"/models", &list)
55+ var status *httpStatusError
56+ switch {
57+ case errors.As(err, &status) && (status.code == http.StatusUnauthorized || status.code == http.StatusForbidden):
58+ return Info{Reachable: true, Warnings: []string{
59+ fmt.Sprintf("kronk: the server refuses the key (%d) — is $%s set to a valid token?", status.code, b.APIKeyEnv),
60+ }}
61+ case err != nil:
62+ return Info{}
63+ }
64+
65+ info := Info{Reachable: true}
66+ if !list.has(b.Model) {
67+ info.Warnings = append(info.Warnings,
68+ fmt.Sprintf("kronk: model %q is not installed — kronk model pull %s (or autoPull: true)", b.Model, b.Model))
69+ return info
70+ }
71+ var details struct {
72+ Config struct {
73+ ContextWindow int `json:"context-window"`
74+ } `json:"model_config"`
75+ }
76+ if getWithKey(ctx, b, kronkModelPath(b.BaseURL, b.Model), &details) == nil && details.Config.ContextWindow > 0 {
77+ info.ContextWindow, info.ContextSource = details.Config.ContextWindow, "/v1/kronk/models"
78+ }
79+ return info
80+}
81+
82+// pullKronk downloads model with POST /v1/kronk/models/pull. The answer is
83+// always 200 once the stream has started; the stream itself says whether the
84+// download worked.
85+func pullKronk(ctx context.Context, b Backend, model string, progress func(PullProgress)) error {
86+ body, err := json.Marshal(map[string]string{"model_url": model})
87+ if err != nil {
88+ return err
89+ }
90+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, rootURL(b.BaseURL)+"/v1/kronk/models/pull", strings.NewReader(string(body)))
91+ if err != nil {
92+ return err
93+ }
94+ req.Header.Set("Content-Type", "application/json")
95+ if b.APIKey != "" {
96+ req.Header.Set("Authorization", "Bearer "+b.APIKey)
97+ }
98+ resp, err := pullClient.Do(req)
99+ if err != nil {
100+ return fmt.Errorf("the server cannot be reached: %w", err)
101+ }
102+ defer func() { _ = resp.Body.Close() }()
103+ if resp.StatusCode/100 != 2 {
104+ text, _ := io.ReadAll(io.LimitReader(resp.Body, 300))
105+ return &httpStatusError{code: resp.StatusCode, body: strings.TrimSpace(string(text))}
106+ }
107+ return readKronkPullStream(resp.Body, model, progress)
108+}
109+
110+// kronkPullStep is one `data:` record of the pull stream. Every record has a
111+// status; a file being downloaded adds `progress`; the last record of a
112+// download that worked says `"downloaded": true`.
113+type kronkPullStep struct {
114+ Status string `json:"status"`
115+ Downloaded bool `json:"downloaded"`
116+ Progress *struct {
117+ CurrentBytes int64 `json:"current_bytes"`
118+ TotalBytes int64 `json:"total_bytes"`
119+ } `json:"progress"`
120+}
121+
122+// readKronkPullStream reads the Server-Sent Events of a pull. Kronk reports a
123+// failure as a last status with no `downloaded` flag (HTTP 200 all the same):
124+// a stream that ends without `"downloaded": true` failed, and its last status
125+// is the server's reason.
126+//
127+// data: {"status":"download-model: Downloading … 120 MB of 4610 MB …","progress":{…}}
128+// data: {"status":"downloaded","model_files":["…"],"downloaded":true}
129+func readKronkPullStream(r io.Reader, model string, progress func(PullProgress)) error {
130+ scanner := bufio.NewScanner(r)
131+ scanner.Buffer(make([]byte, 64*1024), 1024*1024)
132+ last := ""
133+ for scanner.Scan() {
134+ line := strings.TrimSpace(scanner.Text())
135+ payload, ok := strings.CutPrefix(line, "data:")
136+ if !ok {
137+ continue
138+ }
139+ var step kronkPullStep
140+ if err := json.Unmarshal([]byte(strings.TrimSpace(payload)), &step); err != nil {
141+ return fmt.Errorf("unreadable pull progress: %q", line)
142+ }
143+ last = step.Status
144+ if step.Downloaded {
145+ if progress != nil {
146+ progress(PullProgress{Model: model, Status: "success"})
147+ }
148+ return nil
149+ }
150+ if progress != nil {
151+ p := PullProgress{Model: model, Status: step.Status}
152+ if step.Progress != nil {
153+ p.Completed, p.Total = step.Progress.CurrentBytes, step.Progress.TotalBytes
154+ }
155+ progress(p)
156+ }
157+ }
158+ if err := scanner.Err(); err != nil {
159+ return fmt.Errorf("the download was cut: %w", err)
160+ }
161+ if last == "" {
162+ return errors.New("the download ended without success")
163+ }
164+ return fmt.Errorf("the server says: %s", last)
165+}
166+
167+// unloadKronk unloads model with POST /v1/kronk/models/unload. A model that
168+// is not loaded answers 404: that is "nothing to unload", not a failure.
169+func unloadKronk(ctx context.Context, b Backend, model string) (string, error) {
170+ var reply struct {
171+ Status string `json:"status"`
172+ }
173+ err := postJSON(ctx, b, rootURL(b.BaseURL)+"/v1/kronk/models/unload", map[string]string{"id": model}, &reply)
174+ var status *httpStatusError
175+ switch {
176+ case errors.As(err, &status) && status.code == http.StatusNotFound:
177+ return fmt.Sprintf("%s was not loaded: nothing to unload", model), nil
178+ case err != nil:
179+ return "", err
180+ case reply.Status != "unloaded":
181+ return "", fmt.Errorf("the server did not confirm (status %q)", reply.Status)
182+ }
183+ return "unloaded " + model, nil
184+}
added internal/engine/kronk_test.go +240 -0
new file mode 100644
@@ -0,0 +1,240 @@
1+package engine
2+
3+import (
4+ "context"
5+ "errors"
6+ "fmt"
7+ "io"
8+ "net/http"
9+ "net/http/httptest"
10+ "strings"
11+ "sync"
12+ "testing"
13+)
14+
15+const kronkModel = "mradermacher/Qwopus3.5-4B-Coder.Q8_0"
16+
17+// fakeKronk replays the routes measured on Kronk 1.33.2 (kronk.go): the
18+// OpenAI model list, the model details (only under the escaped id, as the
19+// real server), the pull stream and the unload answer.
20+type fakeKronk struct {
21+ mu sync.Mutex
22+ installed map[string]bool
23+ loaded map[string]bool
24+ window int
25+ pullFails string // a status the pull ends with instead of "downloaded"
26+ token string // a required bearer token; "" = open mode
27+ gotPull string
28+ gotAuth string
29+}
30+
31+func newFakeKronk(t *testing.T, f *fakeKronk) (*fakeKronk, Backend) {
32+ t.Helper()
33+ if f.installed == nil {
34+ f.installed = map[string]bool{kronkModel: true}
35+ }
36+ if f.loaded == nil {
37+ f.loaded = map[string]bool{}
38+ }
39+ srv := httptest.NewServer(f)
40+ t.Cleanup(srv.Close)
41+ return f, Backend{Provider: "kronk", BaseURL: srv.URL + "/v1", Model: kronkModel, APIKey: "not-needed", APIKeyEnv: "KRONK_TOKEN"}
42+}
43+
44+func (f *fakeKronk) ServeHTTP(w http.ResponseWriter, r *http.Request) {
45+ f.mu.Lock()
46+ defer f.mu.Unlock()
47+ f.gotAuth = r.Header.Get("Authorization")
48+ if f.token != "" && f.gotAuth != "Bearer "+f.token {
49+ w.WriteHeader(http.StatusUnauthorized)
50+ _, _ = io.WriteString(w, `{"error":{"message":"unauthorized","type":"authentication_error","code":"unauthenticated"}}`)
51+ return
52+ }
53+ switch {
54+ case r.URL.Path == "/v1/models":
55+ ids := []string{}
56+ for id := range f.installed {
57+ ids = append(ids, fmt.Sprintf(`{"id":%q,"object":"model"}`, id))
58+ }
59+ _, _ = fmt.Fprintf(w, `{"object":"list","data":[%s]}`, strings.Join(ids, ","))
60+ case r.URL.Path == "/v1/kronk/models/pull":
61+ body, _ := io.ReadAll(r.Body)
62+ f.gotPull = string(body)
63+ w.Header().Set("Content-Type", "text/event-stream")
64+ _, _ = io.WriteString(w, "data: {\"status\":\"download-model: model file\"}\n\n")
65+ _, _ = io.WriteString(w, "data: {\"status\":\"download-model: Downloading x.gguf... 2 MB of 4 MB\",\"progress\":{\"src\":\"x.gguf\",\"current_bytes\":2000000,\"total_bytes\":4000000}}\n\n")
66+ if f.pullFails != "" {
67+ _, _ = fmt.Fprintf(w, "data: {\"status\":%q}\n\n", f.pullFails)
68+ return
69+ }
70+ f.installed["unsloth/Qwen3-0.6B-Q8_0"] = true
71+ _, _ = io.WriteString(w, "data: {\"status\":\"downloaded\",\"model_files\":[\"/m/x.gguf\"],\"downloaded\":true}\n\n")
72+ case r.URL.Path == "/v1/kronk/models/unload":
73+ body, _ := io.ReadAll(r.Body)
74+ id := strings.TrimSuffix(strings.TrimPrefix(string(body), `{"id":"`), `"}`)
75+ if !f.loaded[id] {
76+ w.WriteHeader(http.StatusNotFound)
77+ _, _ = fmt.Fprintf(w, `{"error":{"message":"model %q is not loaded","type":"not_found_error","code":"not_found"}}`, id)
78+ return
79+ }
80+ delete(f.loaded, id)
81+ _, _ = fmt.Fprintf(w, `{"status":"unloaded","id":%q}`, id)
82+ case strings.HasPrefix(r.URL.Path, "/v1/kronk/models/"):
83+ // The real server needs the id as ONE escaped segment: a raw "/"
84+ // splits it and the route answers 400.
85+ id := strings.TrimPrefix(r.URL.EscapedPath(), "/v1/kronk/models/")
86+ if strings.Contains(id, "/") {
87+ w.WriteHeader(http.StatusBadRequest)
88+ _, _ = io.WriteString(w, `{"error":{"message":"model parameter is required","code":"invalid_argument"}}`)
89+ return
90+ }
91+ _, _ = fmt.Fprintf(w, `{"id":%q,"model_config":{"context-window":%d}}`, strings.ReplaceAll(id, "%2F", "/"), f.window)
92+ default:
93+ http.NotFound(w, r)
94+ }
95+}
96+
97+// The probe reads the served window from the model's details, at start-up,
98+// whether the model is loaded or not.
99+func TestProbeKronkReadsTheServedWindow(t *testing.T) {
100+ _, b := newFakeKronk(t, &fakeKronk{window: 131072})
101+ p, _ := Lookup("kronk")
102+ info := p.Probe(context.Background(), b)
103+ if !info.Reachable || info.ContextWindow != 131072 || info.ContextSource != "/v1/kronk/models" || len(info.Warnings) != 0 {
104+ t.Errorf("Probe = %+v, want reachable, 131072 from /v1/kronk/models, no warning", info)
105+ }
106+}
107+
108+func TestProbeKronkWarnsAboutAModelNotInstalled(t *testing.T) {
109+ _, b := newFakeKronk(t, &fakeKronk{window: 131072})
110+ b.Model = "unsloth/Qwen3-0.6B-Q8_0"
111+ p, _ := Lookup("kronk")
112+ info := p.Probe(context.Background(), b)
113+ if !info.Reachable || info.ContextWindow != 0 || len(info.Warnings) != 1 || !strings.Contains(info.Warnings[0], "kronk model pull unsloth/Qwen3-0.6B-Q8_0") {
114+ t.Errorf("Probe = %+v, want reachable, window unknown, one warning naming kronk model pull", info)
115+ }
116+}
117+
118+// A protected server: the probe sends the key, and a refused one is a
119+// warning naming the variable, not "nothing answers".
120+func TestProbeKronkSendsTheKeyAndReportsARefusal(t *testing.T) {
121+ f, b := newFakeKronk(t, &fakeKronk{window: 4096, token: "jwt"})
122+ p, _ := Lookup("kronk")
123+
124+ info := p.Probe(context.Background(), b)
125+ if !info.Reachable || len(info.Warnings) != 1 || !strings.Contains(info.Warnings[0], "$KRONK_TOKEN") {
126+ t.Errorf("refused key: %+v, want reachable and one warning naming $KRONK_TOKEN", info)
127+ }
128+
129+ b.APIKey = "jwt"
130+ info = p.Probe(context.Background(), b)
131+ if info.ContextWindow != 4096 || len(info.Warnings) != 0 || f.gotAuth != "Bearer jwt" {
132+ t.Errorf("valid key: %+v (auth %q), want 4096 and no warning", info, f.gotAuth)
133+ }
134+}
135+
136+func TestProbeKronkServerDown(t *testing.T) {
137+ p, _ := Lookup("kronk")
138+ info := p.Probe(context.Background(), Backend{Provider: "kronk", BaseURL: closedURL(t) + "/v1", Model: kronkModel})
139+ if info.Reachable || len(info.Warnings) != 1 || !strings.Contains(info.Warnings[0], "kronk server start") {
140+ t.Errorf("Probe = %+v, want unreachable and the start hint", info)
141+ }
142+}
143+
144+func TestEnsureModelPullsAMissingKronkModel(t *testing.T) {
145+ f, b := newFakeKronk(t, &fakeKronk{})
146+ b.Model = "unsloth/Qwen3-0.6B-Q8_0"
147+ p, _ := Lookup("kronk")
148+
149+ var steps []PullProgress
150+ pulled, err := ensureModel(context.Background(), p, b, func(s PullProgress) { steps = append(steps, s) })
151+ if err != nil || !pulled {
152+ t.Fatalf("ensureModel = %v, %v, want pulled without error", pulled, err)
153+ }
154+ if f.gotPull != `{"model_url":"unsloth/Qwen3-0.6B-Q8_0"}` {
155+ t.Errorf("pull body = %s", f.gotPull)
156+ }
157+ if len(steps) != 3 || steps[1].Completed != 2000000 || steps[1].Total != 4000000 || steps[2].Status != "success" {
158+ t.Errorf("steps = %+v, want a status, a 2/4 MB step, then success", steps)
159+ }
160+}
161+
162+func TestEnsureModelLeavesAnInstalledKronkModelAlone(t *testing.T) {
163+ f, b := newFakeKronk(t, &fakeKronk{})
164+ p, _ := Lookup("kronk")
165+ pulled, err := ensureModel(context.Background(), p, b, nil)
166+ if err != nil || pulled || f.gotPull != "" {
167+ t.Errorf("ensureModel = %v, %v (pull %q), want nothing done", pulled, err, f.gotPull)
168+ }
169+}
170+
171+// Kronk answers 200 to a pull that fails: the last status is the reason.
172+func TestKronkPullFailureCarriesTheServerWords(t *testing.T) {
173+ _, b := newFakeKronk(t, &fakeKronk{pullFails: `download: resolve "nobody/x": not found at provider "nobody"`})
174+ err := pullKronk(context.Background(), b, "nobody/x", nil)
175+ if err == nil || !strings.Contains(err.Error(), `not found at provider "nobody"`) {
176+ t.Errorf("pullKronk = %v, want the server's last status", err)
177+ }
178+}
179+
180+func TestReadKronkPullStream(t *testing.T) {
181+ cases := map[string]struct {
182+ stream, want string
183+ }{
184+ "empty stream": {"", "ended without success"},
185+ "not json": {"data: nope\n", "unreadable pull progress"},
186+ "comment lines": {": ping\n\ndata: {\"status\":\"downloaded\",\"downloaded\":true}\n", ""},
187+ }
188+ for name, c := range cases {
189+ t.Run(name, func(t *testing.T) {
190+ err := readKronkPullStream(strings.NewReader(c.stream), "m/x", nil)
191+ switch {
192+ case c.want == "" && err != nil:
193+ t.Errorf("err = %v, want none", err)
194+ case c.want != "" && (err == nil || !strings.Contains(err.Error(), c.want)):
195+ t.Errorf("err = %v, want %q", err, c.want)
196+ }
197+ })
198+ }
199+}
200+
201+func TestUnloadKronk(t *testing.T) {
202+ _, b := newFakeKronk(t, &fakeKronk{loaded: map[string]bool{kronkModel: true}})
203+ p, _ := Lookup("kronk")
204+ u := p.(Unloader)
205+
206+ line, err := u.Unload(context.Background(), b, kronkModel)
207+ if err != nil || line != "unloaded "+kronkModel {
208+ t.Errorf("first unload = %q, %v", line, err)
209+ }
210+ line, err = u.Unload(context.Background(), b, kronkModel)
211+ if err != nil || !strings.Contains(line, "was not loaded: nothing to unload") {
212+ t.Errorf("second unload = %q, %v, want nothing to unload", line, err)
213+ }
214+}
215+
216+func TestKronkModelPathEscapesTheID(t *testing.T) {
217+ got := kronkModelPath("http://127.0.0.1:11435/v1/", "unsloth/Qwen3-0.6B-Q8_0")
218+ if got != "http://127.0.0.1:11435/v1/kronk/models/unsloth%2FQwen3-0.6B-Q8_0" {
219+ t.Errorf("kronkModelPath = %q", got)
220+ }
221+}
222+
223+// Explain words a missing model and a refused key in Kronk's terms.
224+func TestExplainKronk(t *testing.T) {
225+ p, _ := Lookup("kronk")
226+ b := Backend{Provider: "kronk", BaseURL: "http://127.0.0.1:11435/v1", Model: "unsloth/Qwen3-0.6B-Q8_0", APIKeyEnv: "KRONK_TOKEN"}
227+ // The OpenAI client's error text, as Genkit passes it on.
228+ status := func(code int) error {
229+ return fmt.Errorf(`POST "http://127.0.0.1:11435/v1/chat/completions": %d %s {"error":{"code":"not_found"}}`, code, http.StatusText(code))
230+ }
231+ if got := p.Explain(status(404), b); !strings.Contains(got, "kronk model pull unsloth/Qwen3-0.6B-Q8_0") {
232+ t.Errorf("404: %q", got)
233+ }
234+ if got := p.Explain(status(401), b); !strings.Contains(got, "$KRONK_TOKEN") {
235+ t.Errorf("401: %q", got)
236+ }
237+ if got := p.Explain(errors.New(`dial tcp 127.0.0.1:11435: connect: connection refused`), b); !strings.Contains(got, "kronk server start") {
238+ t.Errorf("refused: %q", got)
239+ }
240+}
new file mode 100644
@@ -0,0 +1,240 @@
1+package engine
2+
3+import (
4+ "context"
5+ "errors"
6+ "fmt"
7+ "io"
8+ "net/http"
9+ "net/http/httptest"
10+ "strings"
11+ "sync"
12+ "testing"
13+)
14+
15+const kronkModel = "mradermacher/Qwopus3.5-4B-Coder.Q8_0"
16+
17+// fakeKronk replays the routes measured on Kronk 1.33.2 (kronk.go): the
18+// OpenAI model list, the model details (only under the escaped id, as the
19+// real server), the pull stream and the unload answer.
20+type fakeKronk struct {
21+ mu sync.Mutex
22+ installed map[string]bool
23+ loaded map[string]bool
24+ window int
25+ pullFails string // a status the pull ends with instead of "downloaded"
26+ token string // a required bearer token; "" = open mode
27+ gotPull string
28+ gotAuth string
29+}
30+
31+func newFakeKronk(t *testing.T, f *fakeKronk) (*fakeKronk, Backend) {
32+ t.Helper()
33+ if f.installed == nil {
34+ f.installed = map[string]bool{kronkModel: true}
35+ }
36+ if f.loaded == nil {
37+ f.loaded = map[string]bool{}
38+ }
39+ srv := httptest.NewServer(f)
40+ t.Cleanup(srv.Close)
41+ return f, Backend{Provider: "kronk", BaseURL: srv.URL + "/v1", Model: kronkModel, APIKey: "not-needed", APIKeyEnv: "KRONK_TOKEN"}
42+}
43+
44+func (f *fakeKronk) ServeHTTP(w http.ResponseWriter, r *http.Request) {
45+ f.mu.Lock()
46+ defer f.mu.Unlock()
47+ f.gotAuth = r.Header.Get("Authorization")
48+ if f.token != "" && f.gotAuth != "Bearer "+f.token {
49+ w.WriteHeader(http.StatusUnauthorized)
50+ _, _ = io.WriteString(w, `{"error":{"message":"unauthorized","type":"authentication_error","code":"unauthenticated"}}`)
51+ return
52+ }
53+ switch {
54+ case r.URL.Path == "/v1/models":
55+ ids := []string{}
56+ for id := range f.installed {
57+ ids = append(ids, fmt.Sprintf(`{"id":%q,"object":"model"}`, id))
58+ }
59+ _, _ = fmt.Fprintf(w, `{"object":"list","data":[%s]}`, strings.Join(ids, ","))
60+ case r.URL.Path == "/v1/kronk/models/pull":
61+ body, _ := io.ReadAll(r.Body)
62+ f.gotPull = string(body)
63+ w.Header().Set("Content-Type", "text/event-stream")
64+ _, _ = io.WriteString(w, "data: {\"status\":\"download-model: model file\"}\n\n")
65+ _, _ = io.WriteString(w, "data: {\"status\":\"download-model: Downloading x.gguf... 2 MB of 4 MB\",\"progress\":{\"src\":\"x.gguf\",\"current_bytes\":2000000,\"total_bytes\":4000000}}\n\n")
66+ if f.pullFails != "" {
67+ _, _ = fmt.Fprintf(w, "data: {\"status\":%q}\n\n", f.pullFails)
68+ return
69+ }
70+ f.installed["unsloth/Qwen3-0.6B-Q8_0"] = true
71+ _, _ = io.WriteString(w, "data: {\"status\":\"downloaded\",\"model_files\":[\"/m/x.gguf\"],\"downloaded\":true}\n\n")
72+ case r.URL.Path == "/v1/kronk/models/unload":
73+ body, _ := io.ReadAll(r.Body)
74+ id := strings.TrimSuffix(strings.TrimPrefix(string(body), `{"id":"`), `"}`)
75+ if !f.loaded[id] {
76+ w.WriteHeader(http.StatusNotFound)
77+ _, _ = fmt.Fprintf(w, `{"error":{"message":"model %q is not loaded","type":"not_found_error","code":"not_found"}}`, id)
78+ return
79+ }
80+ delete(f.loaded, id)
81+ _, _ = fmt.Fprintf(w, `{"status":"unloaded","id":%q}`, id)
82+ case strings.HasPrefix(r.URL.Path, "/v1/kronk/models/"):
83+ // The real server needs the id as ONE escaped segment: a raw "/"
84+ // splits it and the route answers 400.
85+ id := strings.TrimPrefix(r.URL.EscapedPath(), "/v1/kronk/models/")
86+ if strings.Contains(id, "/") {
87+ w.WriteHeader(http.StatusBadRequest)
88+ _, _ = io.WriteString(w, `{"error":{"message":"model parameter is required","code":"invalid_argument"}}`)
89+ return
90+ }
91+ _, _ = fmt.Fprintf(w, `{"id":%q,"model_config":{"context-window":%d}}`, strings.ReplaceAll(id, "%2F", "/"), f.window)
92+ default:
93+ http.NotFound(w, r)
94+ }
95+}
96+
97+// The probe reads the served window from the model's details, at start-up,
98+// whether the model is loaded or not.
99+func TestProbeKronkReadsTheServedWindow(t *testing.T) {
100+ _, b := newFakeKronk(t, &fakeKronk{window: 131072})
101+ p, _ := Lookup("kronk")
102+ info := p.Probe(context.Background(), b)
103+ if !info.Reachable || info.ContextWindow != 131072 || info.ContextSource != "/v1/kronk/models" || len(info.Warnings) != 0 {
104+ t.Errorf("Probe = %+v, want reachable, 131072 from /v1/kronk/models, no warning", info)
105+ }
106+}
107+
108+func TestProbeKronkWarnsAboutAModelNotInstalled(t *testing.T) {
109+ _, b := newFakeKronk(t, &fakeKronk{window: 131072})
110+ b.Model = "unsloth/Qwen3-0.6B-Q8_0"
111+ p, _ := Lookup("kronk")
112+ info := p.Probe(context.Background(), b)
113+ if !info.Reachable || info.ContextWindow != 0 || len(info.Warnings) != 1 || !strings.Contains(info.Warnings[0], "kronk model pull unsloth/Qwen3-0.6B-Q8_0") {
114+ t.Errorf("Probe = %+v, want reachable, window unknown, one warning naming kronk model pull", info)
115+ }
116+}
117+
118+// A protected server: the probe sends the key, and a refused one is a
119+// warning naming the variable, not "nothing answers".
120+func TestProbeKronkSendsTheKeyAndReportsARefusal(t *testing.T) {
121+ f, b := newFakeKronk(t, &fakeKronk{window: 4096, token: "jwt"})
122+ p, _ := Lookup("kronk")
123+
124+ info := p.Probe(context.Background(), b)
125+ if !info.Reachable || len(info.Warnings) != 1 || !strings.Contains(info.Warnings[0], "$KRONK_TOKEN") {
126+ t.Errorf("refused key: %+v, want reachable and one warning naming $KRONK_TOKEN", info)
127+ }
128+
129+ b.APIKey = "jwt"
130+ info = p.Probe(context.Background(), b)
131+ if info.ContextWindow != 4096 || len(info.Warnings) != 0 || f.gotAuth != "Bearer jwt" {
132+ t.Errorf("valid key: %+v (auth %q), want 4096 and no warning", info, f.gotAuth)
133+ }
134+}
135+
136+func TestProbeKronkServerDown(t *testing.T) {
137+ p, _ := Lookup("kronk")
138+ info := p.Probe(context.Background(), Backend{Provider: "kronk", BaseURL: closedURL(t) + "/v1", Model: kronkModel})
139+ if info.Reachable || len(info.Warnings) != 1 || !strings.Contains(info.Warnings[0], "kronk server start") {
140+ t.Errorf("Probe = %+v, want unreachable and the start hint", info)
141+ }
142+}
143+
144+func TestEnsureModelPullsAMissingKronkModel(t *testing.T) {
145+ f, b := newFakeKronk(t, &fakeKronk{})
146+ b.Model = "unsloth/Qwen3-0.6B-Q8_0"
147+ p, _ := Lookup("kronk")
148+
149+ var steps []PullProgress
150+ pulled, err := ensureModel(context.Background(), p, b, func(s PullProgress) { steps = append(steps, s) })
151+ if err != nil || !pulled {
152+ t.Fatalf("ensureModel = %v, %v, want pulled without error", pulled, err)
153+ }
154+ if f.gotPull != `{"model_url":"unsloth/Qwen3-0.6B-Q8_0"}` {
155+ t.Errorf("pull body = %s", f.gotPull)
156+ }
157+ if len(steps) != 3 || steps[1].Completed != 2000000 || steps[1].Total != 4000000 || steps[2].Status != "success" {
158+ t.Errorf("steps = %+v, want a status, a 2/4 MB step, then success", steps)
159+ }
160+}
161+
162+func TestEnsureModelLeavesAnInstalledKronkModelAlone(t *testing.T) {
163+ f, b := newFakeKronk(t, &fakeKronk{})
164+ p, _ := Lookup("kronk")
165+ pulled, err := ensureModel(context.Background(), p, b, nil)
166+ if err != nil || pulled || f.gotPull != "" {
167+ t.Errorf("ensureModel = %v, %v (pull %q), want nothing done", pulled, err, f.gotPull)
168+ }
169+}
170+
171+// Kronk answers 200 to a pull that fails: the last status is the reason.
172+func TestKronkPullFailureCarriesTheServerWords(t *testing.T) {
173+ _, b := newFakeKronk(t, &fakeKronk{pullFails: `download: resolve "nobody/x": not found at provider "nobody"`})
174+ err := pullKronk(context.Background(), b, "nobody/x", nil)
175+ if err == nil || !strings.Contains(err.Error(), `not found at provider "nobody"`) {
176+ t.Errorf("pullKronk = %v, want the server's last status", err)
177+ }
178+}
179+
180+func TestReadKronkPullStream(t *testing.T) {
181+ cases := map[string]struct {
182+ stream, want string
183+ }{
184+ "empty stream": {"", "ended without success"},
185+ "not json": {"data: nope\n", "unreadable pull progress"},
186+ "comment lines": {": ping\n\ndata: {\"status\":\"downloaded\",\"downloaded\":true}\n", ""},
187+ }
188+ for name, c := range cases {
189+ t.Run(name, func(t *testing.T) {
190+ err := readKronkPullStream(strings.NewReader(c.stream), "m/x", nil)
191+ switch {
192+ case c.want == "" && err != nil:
193+ t.Errorf("err = %v, want none", err)
194+ case c.want != "" && (err == nil || !strings.Contains(err.Error(), c.want)):
195+ t.Errorf("err = %v, want %q", err, c.want)
196+ }
197+ })
198+ }
199+}
200+
201+func TestUnloadKronk(t *testing.T) {
202+ _, b := newFakeKronk(t, &fakeKronk{loaded: map[string]bool{kronkModel: true}})
203+ p, _ := Lookup("kronk")
204+ u := p.(Unloader)
205+
206+ line, err := u.Unload(context.Background(), b, kronkModel)
207+ if err != nil || line != "unloaded "+kronkModel {
208+ t.Errorf("first unload = %q, %v", line, err)
209+ }
210+ line, err = u.Unload(context.Background(), b, kronkModel)
211+ if err != nil || !strings.Contains(line, "was not loaded: nothing to unload") {
212+ t.Errorf("second unload = %q, %v, want nothing to unload", line, err)
213+ }
214+}
215+
216+func TestKronkModelPathEscapesTheID(t *testing.T) {
217+ got := kronkModelPath("http://127.0.0.1:11435/v1/", "unsloth/Qwen3-0.6B-Q8_0")
218+ if got != "http://127.0.0.1:11435/v1/kronk/models/unsloth%2FQwen3-0.6B-Q8_0" {
219+ t.Errorf("kronkModelPath = %q", got)
220+ }
221+}
222+
223+// Explain words a missing model and a refused key in Kronk's terms.
224+func TestExplainKronk(t *testing.T) {
225+ p, _ := Lookup("kronk")
226+ b := Backend{Provider: "kronk", BaseURL: "http://127.0.0.1:11435/v1", Model: "unsloth/Qwen3-0.6B-Q8_0", APIKeyEnv: "KRONK_TOKEN"}
227+ // The OpenAI client's error text, as Genkit passes it on.
228+ status := func(code int) error {
229+ return fmt.Errorf(`POST "http://127.0.0.1:11435/v1/chat/completions": %d %s {"error":{"code":"not_found"}}`, code, http.StatusText(code))
230+ }
231+ if got := p.Explain(status(404), b); !strings.Contains(got, "kronk model pull unsloth/Qwen3-0.6B-Q8_0") {
232+ t.Errorf("404: %q", got)
233+ }
234+ if got := p.Explain(status(401), b); !strings.Contains(got, "$KRONK_TOKEN") {
235+ t.Errorf("401: %q", got)
236+ }
237+ if got := p.Explain(errors.New(`dial tcp 127.0.0.1:11435: connect: connection refused`), b); !strings.Contains(got, "kronk server start") {
238+ t.Errorf("refused: %q", got)
239+ }
240+}
modified internal/engine/openai_compat.go +26 -3
@@ -20,9 +20,10 @@ import (
2020 )
2121
2222 // openaiCompat is the Provider for every server that speaks the OpenAI
23-// chat-completions protocol. Docker Model Runner, llama-server, llmman and
24-// Ollama's /v1 are four parametrisations of it; vLLM and the remote APIs would
25-// be more entries in init() below — the point of keeping the struct data-only.
23+// chat-completions protocol. Docker Model Runner, llama-server, llmman,
24+// Ollama's /v1 and Kronk are five parametrisations of it (`openai`, in
25+// openai.go, is the generic one); vLLM and the remote APIs would be more
26+// entries in init() below — the point of keeping the struct data-only.
2627 // Each server keeps its own entry even when two look alike today: a difference
2728 // found later (how to turn reasoning off, where the window is) is then one
2829 // field to change, not a provider to split.
@@ -153,6 +154,28 @@ func init() {
153154 unload: unloadKeepAlive,
154155 pull: pullOllamaAPI,
155156 })
157+ register(&openaiCompat{
158+ name: "kronk",
159+ // `kronk server start` binds the loopback only; from a sandbox, the
160+ // fallback answers once the host started it with
161+ // --api-host 0.0.0.0:11435 (kronk.go).
162+ baseURL: "http://127.0.0.1:11435/v1",
163+ fallback: "http://host.docker.internal:11435/v1",
164+ // The default `open` mode checks no key; the protected modes want a
165+ // bearer token, which Kronk's manual and clients read from KRONK_TOKEN.
166+ apiKeyEnv: "KRONK_TOKEN",
167+ dummyKey: "not-needed",
168+ startHint: "start it: kronk server start",
169+ notFoundHint: "kronk model pull %s",
170+ probe: probeKronk,
171+ // Measured 2026-11-11 on Kronk 1.33.2 (kronk.go): no prompt_progress,
172+ // chat_template_kwargs enable_thinking:false obeyed, reasoning_effort
173+ // "none" ignored.
174+ progress: false,
175+ thinkingOff: thinkingOffTemplate,
176+ unload: unloadKronk,
177+ pull: pullKronk,
178+ })
156179 }
157180
158181 func (p *openaiCompat) Name() string { return p.name }
@@ -20,9 +20,10 @@ import (
20 )20 )
21 21
22 // openaiCompat is the Provider for every server that speaks the OpenAI22 // openaiCompat is the Provider for every server that speaks the OpenAI
23-// chat-completions protocol. Docker Model Runner, llama-server, llmman and23+// chat-completions protocol. Docker Model Runner, llama-server, llmman,
24-// Ollama's /v1 are four parametrisations of it; vLLM and the remote APIs would24+// Ollama's /v1 and Kronk are five parametrisations of it (`openai`, in
25-// be more entries in init() below — the point of keeping the struct data-only.25+// openai.go, is the generic one); vLLM and the remote APIs would be more
26+// entries in init() below — the point of keeping the struct data-only.
26 // Each server keeps its own entry even when two look alike today: a difference27 // Each server keeps its own entry even when two look alike today: a difference
27 // found later (how to turn reasoning off, where the window is) is then one28 // found later (how to turn reasoning off, where the window is) is then one
28 // field to change, not a provider to split.29 // field to change, not a provider to split.
@@ -153,6 +154,28 @@ func init() {
153 unload: unloadKeepAlive,154 unload: unloadKeepAlive,
154 pull: pullOllamaAPI,155 pull: pullOllamaAPI,
155 })156 })
157+ register(&openaiCompat{
158+ name: "kronk",
159+ // `kronk server start` binds the loopback only; from a sandbox, the
160+ // fallback answers once the host started it with
161+ // --api-host 0.0.0.0:11435 (kronk.go).
162+ baseURL: "http://127.0.0.1:11435/v1",
163+ fallback: "http://host.docker.internal:11435/v1",
164+ // The default `open` mode checks no key; the protected modes want a
165+ // bearer token, which Kronk's manual and clients read from KRONK_TOKEN.
166+ apiKeyEnv: "KRONK_TOKEN",
167+ dummyKey: "not-needed",
168+ startHint: "start it: kronk server start",
169+ notFoundHint: "kronk model pull %s",
170+ probe: probeKronk,
171+ // Measured 2026-11-11 on Kronk 1.33.2 (kronk.go): no prompt_progress,
172+ // chat_template_kwargs enable_thinking:false obeyed, reasoning_effort
173+ // "none" ignored.
174+ progress: false,
175+ thinkingOff: thinkingOffTemplate,
176+ unload: unloadKronk,
177+ pull: pullKronk,
178+ })
156 }179 }
157 180
158 func (p *openaiCompat) Name() string { return p.name }181 func (p *openaiCompat) Name() string { return p.name }
modified internal/engine/provider_test.go +6 -2
@@ -75,14 +75,14 @@ func ptr(s string) *string { return &s }
7575 // where DMR_BASE_URL is often exported.
7676 func clearEnv(t *testing.T) {
7777 t.Helper()
78- for _, k := range []string{"AGENT_BASE_URL", "DMR_BASE_URL", "LLAMA_API_KEY", "LLMMAN_API_KEY", "OLLAMA_API_KEY", "OPENAI_API_KEY", "AGENT_PROVIDER", "AGENT_MODEL"} {
78+ for _, k := range []string{"AGENT_BASE_URL", "DMR_BASE_URL", "LLAMA_API_KEY", "LLMMAN_API_KEY", "OLLAMA_API_KEY", "KRONK_TOKEN", "OPENAI_API_KEY", "AGENT_PROVIDER", "AGENT_MODEL"} {
7979 t.Setenv(k, "")
8080 }
8181 }
8282
8383 func TestLookupListsKnownProviders(t *testing.T) {
8484 _, err := Lookup("vllm")
85- if err == nil || !strings.Contains(err.Error(), "dmr, llamacpp, llmman, ollama, openai") {
85+ if err == nil || !strings.Contains(err.Error(), "dmr, kronk, llamacpp, llmman, ollama, openai") {
8686 t.Fatalf("Lookup(vllm) = %v, want an error naming the known providers", err)
8787 }
8888 }
@@ -118,6 +118,9 @@ func TestResolveURLPrecedence(t *testing.T) {
118118 {"ollama falls back to the host's server", "ollama", config.Config{BaseURL: down}, nil, "http://host.docker.internal:11434/v1", "not-needed"},
119119 {"ollama keeps a reachable baseUrl", "ollama", config.Config{BaseURL: up.URL}, nil, up.URL, "not-needed"},
120120 {"ollama reads OLLAMA_API_KEY when set", "ollama", config.Config{BaseURL: up.URL}, map[string]string{"OLLAMA_API_KEY": "k4", "LLMMAN_API_KEY": "k3"}, up.URL, "k4"},
121+ {"kronk falls back to the host's server", "kronk", config.Config{BaseURL: down}, nil, "http://host.docker.internal:11435/v1", "not-needed"},
122+ {"kronk keeps a reachable baseUrl", "kronk", config.Config{BaseURL: up.URL}, nil, up.URL, "not-needed"},
123+ {"kronk reads KRONK_TOKEN when set", "kronk", config.Config{BaseURL: up.URL}, map[string]string{"KRONK_TOKEN": "jwt", "OLLAMA_API_KEY": "k4"}, up.URL, "jwt"},
121124 }
122125 for _, tc := range cases {
123126 t.Run(tc.name, func(t *testing.T) {
@@ -554,6 +557,7 @@ func TestDefaultURLs(t *testing.T) {
554557 cases := []struct{ provider, base, fallback string }{
555558 {"llmman", "http://127.0.0.1:17434/v1", "http://host.docker.internal:17434/v1"},
556559 {"ollama", "http://localhost:11434/v1", "http://host.docker.internal:11434/v1"},
560+ {"kronk", "http://127.0.0.1:11435/v1", "http://host.docker.internal:11435/v1"},
557561 {"dmr", "http://localhost:12434/engines/v1", "http://host.docker.internal:12434/engines/v1"},
558562 {"llamacpp", "http://127.0.0.1:8080/v1", ""},
559563 {"openai", "https://api.openai.com/v1", ""},
@@ -75,14 +75,14 @@ func ptr(s string) *string { return &s }
75 // where DMR_BASE_URL is often exported.75 // where DMR_BASE_URL is often exported.
76 func clearEnv(t *testing.T) {76 func clearEnv(t *testing.T) {
77 t.Helper()77 t.Helper()
78- for _, k := range []string{"AGENT_BASE_URL", "DMR_BASE_URL", "LLAMA_API_KEY", "LLMMAN_API_KEY", "OLLAMA_API_KEY", "OPENAI_API_KEY", "AGENT_PROVIDER", "AGENT_MODEL"} {78+ for _, k := range []string{"AGENT_BASE_URL", "DMR_BASE_URL", "LLAMA_API_KEY", "LLMMAN_API_KEY", "OLLAMA_API_KEY", "KRONK_TOKEN", "OPENAI_API_KEY", "AGENT_PROVIDER", "AGENT_MODEL"} {
79 t.Setenv(k, "")79 t.Setenv(k, "")
80 }80 }
81 }81 }
82 82
83 func TestLookupListsKnownProviders(t *testing.T) {83 func TestLookupListsKnownProviders(t *testing.T) {
84 _, err := Lookup("vllm")84 _, err := Lookup("vllm")
85- if err == nil || !strings.Contains(err.Error(), "dmr, llamacpp, llmman, ollama, openai") {85+ if err == nil || !strings.Contains(err.Error(), "dmr, kronk, llamacpp, llmman, ollama, openai") {
86 t.Fatalf("Lookup(vllm) = %v, want an error naming the known providers", err)86 t.Fatalf("Lookup(vllm) = %v, want an error naming the known providers", err)
87 }87 }
88 }88 }
@@ -118,6 +118,9 @@ func TestResolveURLPrecedence(t *testing.T) {
118 {"ollama falls back to the host's server", "ollama", config.Config{BaseURL: down}, nil, "http://host.docker.internal:11434/v1", "not-needed"},118 {"ollama falls back to the host's server", "ollama", config.Config{BaseURL: down}, nil, "http://host.docker.internal:11434/v1", "not-needed"},
119 {"ollama keeps a reachable baseUrl", "ollama", config.Config{BaseURL: up.URL}, nil, up.URL, "not-needed"},119 {"ollama keeps a reachable baseUrl", "ollama", config.Config{BaseURL: up.URL}, nil, up.URL, "not-needed"},
120 {"ollama reads OLLAMA_API_KEY when set", "ollama", config.Config{BaseURL: up.URL}, map[string]string{"OLLAMA_API_KEY": "k4", "LLMMAN_API_KEY": "k3"}, up.URL, "k4"},120 {"ollama reads OLLAMA_API_KEY when set", "ollama", config.Config{BaseURL: up.URL}, map[string]string{"OLLAMA_API_KEY": "k4", "LLMMAN_API_KEY": "k3"}, up.URL, "k4"},
121+ {"kronk falls back to the host's server", "kronk", config.Config{BaseURL: down}, nil, "http://host.docker.internal:11435/v1", "not-needed"},
122+ {"kronk keeps a reachable baseUrl", "kronk", config.Config{BaseURL: up.URL}, nil, up.URL, "not-needed"},
123+ {"kronk reads KRONK_TOKEN when set", "kronk", config.Config{BaseURL: up.URL}, map[string]string{"KRONK_TOKEN": "jwt", "OLLAMA_API_KEY": "k4"}, up.URL, "jwt"},
121 }124 }
122 for _, tc := range cases {125 for _, tc := range cases {
123 t.Run(tc.name, func(t *testing.T) {126 t.Run(tc.name, func(t *testing.T) {
@@ -554,6 +557,7 @@ func TestDefaultURLs(t *testing.T) {
554 cases := []struct{ provider, base, fallback string }{557 cases := []struct{ provider, base, fallback string }{
555 {"llmman", "http://127.0.0.1:17434/v1", "http://host.docker.internal:17434/v1"},558 {"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"},559 {"ollama", "http://localhost:11434/v1", "http://host.docker.internal:11434/v1"},
560+ {"kronk", "http://127.0.0.1:11435/v1", "http://host.docker.internal:11435/v1"},
557 {"dmr", "http://localhost:12434/engines/v1", "http://host.docker.internal:12434/engines/v1"},561 {"dmr", "http://localhost:12434/engines/v1", "http://host.docker.internal:12434/engines/v1"},
558 {"llamacpp", "http://127.0.0.1:8080/v1", ""},562 {"llamacpp", "http://127.0.0.1:8080/v1", ""},
559 {"openai", "https://api.openai.com/v1", ""},563 {"openai", "https://api.openai.com/v1", ""},
modified internal/tui/initwin.go +5 -2
@@ -423,8 +423,11 @@ func optionLabel(st agentinit.Step, o agentinit.Option) string {
423423 // model it is the full identifier: two short names can be the same, and the
424424 // identifier is what the file will hold.
425425 func optionDetail(st agentinit.Step, o agentinit.Option) string {
426- if st.Key == "model" && o.Detail == "" {
427- return o.Value
426+ if st.Key == "model" {
427+ if o.Detail == "" {
428+ return o.Value
429+ }
430+ return o.Value + " — " + o.Detail
428431 }
429432 return o.Detail
430433 }
@@ -423,8 +423,11 @@ func optionLabel(st agentinit.Step, o agentinit.Option) string {
423 // model it is the full identifier: two short names can be the same, and the423 // model it is the full identifier: two short names can be the same, and the
424 // identifier is what the file will hold.424 // identifier is what the file will hold.
425 func optionDetail(st agentinit.Step, o agentinit.Option) string {425 func optionDetail(st agentinit.Step, o agentinit.Option) string {
426- if st.Key == "model" && o.Detail == "" {426+ if st.Key == "model" {
427- return o.Value427+ if o.Detail == "" {
428+ return o.Value
429+ }
430+ return o.Value + " — " + o.Detail
428 }431 }
429 return o.Detail432 return o.Detail
430 }433 }
modified internal/tui/modelname.go +24 -0
@@ -50,6 +50,10 @@ func shortModel(id string) string {
5050 }
5151 // The registry host and the organisation are routing, not identity.
5252 if i := strings.LastIndex(id, "/"); i >= 0 {
53+ if base, profile, ok := profileOf(id); ok {
54+ // The profile alone ("AGENT") would name no model at all.
55+ return clipName(dropFormat(base), max(modelWidth-len([]rune(profile))-1, 8)) + "/" + profile
56+ }
5357 id = id[i+1:]
5458 }
5559 name, tag, hasTag := strings.Cut(id, ":")
@@ -63,6 +67,26 @@ func shortModel(id string) string {
6367 return clipName(name, max(modelWidth-len([]rune(tag))-1, 8)) + ":" + tag
6468 }
6569
70+// profileOf recognises a configuration profile as Kronk names one,
71+// `provider/modelID/PROFILE` (…/Qwopus3.5-4B-Coder.Q8_0/AGENT): a third
72+// segment made of letters only, which is a settings key, not a model. It
73+// returns the model's own last segment and the profile.
74+//
75+// profileOf("mradermacher/Qwopus3.5-4B-Coder.Q8_0/AGENT") // "Qwopus3.5-4B-Coder.Q8_0", "AGENT", true
76+// profileOf("hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M") // "", "", false
77+func profileOf(id string) (base, profile string, ok bool) {
78+ parts := strings.Split(id, "/")
79+ if len(parts) != 3 || parts[1] == "" || strings.Contains(parts[1], ":") {
80+ return "", "", false
81+ }
82+ for _, r := range parts[2] {
83+ if !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z') {
84+ return "", "", false
85+ }
86+ }
87+ return parts[1], parts[2], parts[2] != ""
88+}
89+
6690 // dropFormat removes a trailing "-gguf", and a trailing "-gguf-<quant>" where
6791 // the quantisation is already about to be shown as the tag.
6892 func dropFormat(name string) string {
@@ -50,6 +50,10 @@ func shortModel(id string) string {
50 }50 }
51 // The registry host and the organisation are routing, not identity.51 // The registry host and the organisation are routing, not identity.
52 if i := strings.LastIndex(id, "/"); i >= 0 {52 if i := strings.LastIndex(id, "/"); i >= 0 {
53+ if base, profile, ok := profileOf(id); ok {
54+ // The profile alone ("AGENT") would name no model at all.
55+ return clipName(dropFormat(base), max(modelWidth-len([]rune(profile))-1, 8)) + "/" + profile
56+ }
53 id = id[i+1:]57 id = id[i+1:]
54 }58 }
55 name, tag, hasTag := strings.Cut(id, ":")59 name, tag, hasTag := strings.Cut(id, ":")
@@ -63,6 +67,26 @@ func shortModel(id string) string {
63 return clipName(name, max(modelWidth-len([]rune(tag))-1, 8)) + ":" + tag67 return clipName(name, max(modelWidth-len([]rune(tag))-1, 8)) + ":" + tag
64 }68 }
65 69
70+// profileOf recognises a configuration profile as Kronk names one,
71+// `provider/modelID/PROFILE` (…/Qwopus3.5-4B-Coder.Q8_0/AGENT): a third
72+// segment made of letters only, which is a settings key, not a model. It
73+// returns the model's own last segment and the profile.
74+//
75+// profileOf("mradermacher/Qwopus3.5-4B-Coder.Q8_0/AGENT") // "Qwopus3.5-4B-Coder.Q8_0", "AGENT", true
76+// profileOf("hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M") // "", "", false
77+func profileOf(id string) (base, profile string, ok bool) {
78+ parts := strings.Split(id, "/")
79+ if len(parts) != 3 || parts[1] == "" || strings.Contains(parts[1], ":") {
80+ return "", "", false
81+ }
82+ for _, r := range parts[2] {
83+ if !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z') {
84+ return "", "", false
85+ }
86+ }
87+ return parts[1], parts[2], parts[2] != ""
88+}
89+
66 // dropFormat removes a trailing "-gguf", and a trailing "-gguf-<quant>" where90 // dropFormat removes a trailing "-gguf", and a trailing "-gguf-<quant>" where
67 // the quantisation is already about to be shown as the tag.91 // the quantisation is already about to be shown as the tag.
68 func dropFormat(name string) string {92 func dropFormat(name string) string {
modified internal/tui/modelname_test.go +3 -0
@@ -18,6 +18,9 @@ func TestShortModel(t *testing.T) {
1818 {"an id with no path at all", "gpt-4o", "gpt-4o"},
1919 {"no tag, no format", "huggingface.co/qwen/qwen3-embedding-0.6b", "qwen3-embedding-0.6b"},
2020 {"empty stays empty", "", ""},
21+ {"a Kronk profile keeps its model", "mradermacher/Qwopus3.5-4B-Coder.Q8_0/AGENT", "Qwopus3.5-4B-Coder.Q8…/AGENT"},
22+ {"a short Kronk profile is whole", "unsloth/Qwen3-0.6B-Q8_0/AGENT", "Qwen3-0.6B-Q8_0/AGENT"},
23+ {"a Kronk model without profile", "mradermacher/Qwopus3.5-4B-Coder.Q8_0", "Qwopus3.5-4B-Coder.Q8_0"},
2124 }
2225 for _, tc := range cases {
2326 t.Run(tc.name, func(t *testing.T) {
@@ -18,6 +18,9 @@ func TestShortModel(t *testing.T) {
18 {"an id with no path at all", "gpt-4o", "gpt-4o"},18 {"an id with no path at all", "gpt-4o", "gpt-4o"},
19 {"no tag, no format", "huggingface.co/qwen/qwen3-embedding-0.6b", "qwen3-embedding-0.6b"},19 {"no tag, no format", "huggingface.co/qwen/qwen3-embedding-0.6b", "qwen3-embedding-0.6b"},
20 {"empty stays empty", "", ""},20 {"empty stays empty", "", ""},
21+ {"a Kronk profile keeps its model", "mradermacher/Qwopus3.5-4B-Coder.Q8_0/AGENT", "Qwopus3.5-4B-Coder.Q8…/AGENT"},
22+ {"a short Kronk profile is whole", "unsloth/Qwen3-0.6B-Q8_0/AGENT", "Qwen3-0.6B-Q8_0/AGENT"},
23+ {"a Kronk model without profile", "mradermacher/Qwopus3.5-4B-Coder.Q8_0", "Qwopus3.5-4B-Coder.Q8_0"},
21 }24 }
22 for _, tc := range cases {25 for _, tc := range cases {
23 t.Run(tc.name, func(t *testing.T) {26 t.Run(tc.name, func(t *testing.T) {