Merge feature/kronk-provider into main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
modified
.tickets/epics.yaml +11 -1 | @@ -31,7 +31,7 @@ epics: | ||
| 31 | 31 | state: open |
| 32 | 32 | color: "#5319e7" |
| 33 | 33 | - title: ToDo |
| 34 | - state: open | |
| 34 | + state: closed | |
| 35 | 35 | color: "#c2e0c6" |
| 36 | 36 | - title: Questions |
| 37 | 37 | state: open |
| @@ -67,3 +67,13 @@ epics: | ||
| 67 | 67 | state: open |
| 68 | 68 | color: "#a2eeef" |
| 69 | 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" | |
| @@ -31,7 +31,7 @@ epics: | |||
| 31 | state: open | 31 | state: open |
| 32 | color: "#5319e7" | 32 | color: "#5319e7" |
| 33 | - title: ToDo | 33 | - title: ToDo |
| 34 | - state: open | 34 | + state: closed |
| 35 | color: "#c2e0c6" | 35 | color: "#c2e0c6" |
| 36 | - title: Questions | 36 | - title: Questions |
| 37 | state: open | 37 | state: open |
| @@ -67,3 +67,13 @@ epics: | |||
| 67 | state: open | 67 | 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: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-17T13:22:40.786Z |
| 9 | -updatedAt: 2026-10-06T13:05:39.475Z | |
| 9 | +updatedAt: 2026-11-11T06:51:08.000Z | |
| 10 | 10 | labels: |
| 11 | 11 | - priority::low |
| 12 | 12 | - kind::chore |
| 13 | 13 | - enhancement |
| 14 | -epic: Debug | |
| 14 | +epic: Auto improve | |
| 15 | 15 | body: | |
| 16 | 16 | - Use the sessions' logs |
| 17 | 17 | - Create a Claude Skill for this |
| @@ -6,12 +6,12 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-17T13:22:40.786Z | 8 | createdAt: 2026-09-17T13:22:40.786Z |
| 9 | -updatedAt: 2026-10-06T13:05:39.475Z | 9 | +updatedAt: 2026-11-11T06:51:08.000Z |
| 10 | labels: | 10 | labels: |
| 11 | - priority::low | 11 | - priority::low |
| 12 | - kind::chore | 12 | - kind::chore |
| 13 | - enhancement | 13 | - enhancement |
| 14 | -epic: Debug | 14 | +epic: Auto improve |
| 15 | body: | | 15 | body: | |
| 16 | - Use the sessions' logs | 16 | - Use the sessions' logs |
| 17 | - Create a Claude Skill for this | 17 | - 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 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 58 |
| 3 | 3 | title: Add more tools to the sbx template (redis, cli to parse source code ...) |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-01T04:39:22.452Z |
| 9 | -updatedAt: 2026-10-06T13:13:18.155Z | |
| 9 | +updatedAt: 2026-11-11T06:53:19.908Z | |
| 10 | 10 | labels: [] |
| 11 | 11 | epic: ToDo |
| 12 | 12 | 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: 58 | 2 | 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: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-01T04:39:22.452Z | 8 | createdAt: 2026-10-01T04:39:22.452Z |
| 9 | -updatedAt: 2026-10-06T13:13:18.155Z | 9 | +updatedAt: 2026-11-11T06:53:19.908Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | epic: ToDo | 11 | 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 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | 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 | |
| 4 | 4 | state: open |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-02T09:26:19.729Z |
| 9 | -updatedAt: 2026-10-20T07:43:15.320Z | |
| 9 | +updatedAt: 2026-11-11T06:55:34.384Z | |
| 10 | 10 | labels: [] |
| 11 | -epic: ToDo | |
| 11 | +milestone: v2.0.0 | |
| 12 | +epic: Search | |
| 12 | 13 | related: |
| 13 | 14 | - 104 |
| 14 | 15 | 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: 69 | 2 | 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 |
| 4 | state: open | 4 | state: open |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-02T09:26:19.729Z | 8 | createdAt: 2026-10-02T09:26:19.729Z |
| 9 | -updatedAt: 2026-10-20T07:43:15.320Z | 9 | +updatedAt: 2026-11-11T06:55:34.384Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | -epic: ToDo | 11 | +milestone: v2.0.0 |
| 12 | +epic: Search | ||
| 12 | related: | 13 | related: |
| 13 | - 104 | 14 | - 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: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-02T14:22:29.427Z |
| 9 | -updatedAt: 2026-10-06T13:11:38.195Z | |
| 9 | +updatedAt: 2026-11-11T06:57:56.200Z | |
| 10 | 10 | labels: [] |
| 11 | -epic: Questions | |
| 11 | +epic: Agent Identity | |
| 12 | 12 | body: "" |
| 13 | 13 | tasks: [] |
| 14 | 14 | comments: [] |
| @@ -6,9 +6,9 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-02T14:22:29.427Z | 8 | createdAt: 2026-10-02T14:22:29.427Z |
| 9 | -updatedAt: 2026-10-06T13:11:38.195Z | 9 | +updatedAt: 2026-11-11T06:57:56.200Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | -epic: Questions | 11 | +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: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-16T15:37:32.062Z |
| 9 | -updatedAt: 2026-11-01T20:44:45.000Z | |
| 9 | +updatedAt: 2026-11-11T06:57:30.799Z | |
| 10 | 10 | labels: [] |
| 11 | -epic: Questions | |
| 11 | +epic: Agent Identity | |
| 12 | 12 | body: | |
| 13 | 13 | Des idées pour gérer cela ? |
| 14 | 14 | 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: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-16T15:37:32.062Z | 8 | createdAt: 2026-10-16T15:37:32.062Z |
| 9 | -updatedAt: 2026-11-01T20:44:45.000Z | 9 | +updatedAt: 2026-11-11T06:57:30.799Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | -epic: Questions | 11 | +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 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 111 |
| 3 | 3 | title: Using the settings guide create a Claude skill |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-21T04:27:19.433Z |
| 9 | -updatedAt: 2026-11-01T20:44:45.000Z | |
| 9 | +updatedAt: 2026-11-11T06:52:11.123Z | |
| 10 | 10 | labels: [] |
| 11 | 11 | epic: Models |
| 12 | 12 | 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: 111 | 2 | id: 111 |
| 3 | title: Using the settings guide create a Claude skill | 3 | title: Using the settings guide create a Claude skill |
| 4 | -state: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-21T04:27:19.433Z | 8 | createdAt: 2026-10-21T04:27:19.433Z |
| 9 | -updatedAt: 2026-11-01T20:44:45.000Z | 9 | +updatedAt: 2026-11-11T06:52:11.123Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | epic: Models | 11 | 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 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 113 |
| 3 | 3 | title: "Mini Me as a service: turn mm into a dedicated app, or expose it as services — ideas" |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | 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 | |
| 11 | 12 | epic: Mini Me as a service |
| 12 | 13 | related: |
| 13 | 14 | - 71 |
| @@ -172,3 +173,10 @@ comments: | ||
| 172 | 173 | user's prompt (like mm -p); structured output; streaming and |
| 173 | 174 | non-streaming; a cancel API. The OpenAI-compatible HTTP façade |
| 174 | 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 | |
| @@ -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: 113 | 2 | 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: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-21T13:00:00.000Z | 8 | createdAt: 2026-10-21T13:00:00.000Z |
| 9 | -updatedAt: 2026-10-22T17:09:28.961Z | 9 | +updatedAt: 2026-11-11T07:02:05.377Z |
| 10 | -labels: [] | 10 | +labels: |
| 11 | + - wontfix | ||
| 11 | epic: Mini Me as a service | 12 | epic: Mini Me as a service |
| 12 | related: | 13 | related: |
| 13 | - 71 | 14 | - 71 |
| @@ -172,3 +173,10 @@ comments: | |||
| 172 | user's prompt (like mm -p); structured output; streaming and | 173 | user's prompt (like mm -p); structured output; streaming and |
| 173 | non-streaming; a cancel API. The OpenAI-compatible HTTP façade | 174 | 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 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 140 |
| 3 | 3 | title: Rephrase the question |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-24T04:11:57.315Z |
| 9 | -updatedAt: 2026-10-24T04:15:54.392Z | |
| 9 | +updatedAt: 2026-11-11T06:49:35.456Z | |
| 10 | 10 | labels: |
| 11 | 11 | - hooks |
| 12 | 12 | 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: 140 | 2 | id: 140 |
| 3 | title: Rephrase the question | 3 | title: Rephrase the question |
| 4 | -state: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-24T04:11:57.315Z | 8 | createdAt: 2026-10-24T04:11:57.315Z |
| 9 | -updatedAt: 2026-10-24T04:15:54.392Z | 9 | +updatedAt: 2026-11-11T06:49:35.456Z |
| 10 | labels: | 10 | labels: |
| 11 | - hooks | 11 | - hooks |
| 12 | epic: Demos | 12 | epic: Demos |
modified
.tickets/issues/0151-blog-post-to-explain-how-to-create-a-wasm-declar.yaml +2 -2 | @@ -1,12 +1,12 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 151 |
| 3 | 3 | title: Blog post to explain how to create a wasm declared tool |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-25T04:23:41.209Z |
| 9 | -updatedAt: 2026-10-25T04:23:41.209Z | |
| 9 | +updatedAt: 2026-11-11T07:03:15.692Z | |
| 10 | 10 | labels: [] |
| 11 | 11 | epic: WASM Module |
| 12 | 12 | 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: 151 | 2 | id: 151 |
| 3 | title: Blog post to explain how to create a wasm declared tool | 3 | title: Blog post to explain how to create a wasm declared tool |
| 4 | -state: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-25T04:23:41.209Z | 8 | createdAt: 2026-10-25T04:23:41.209Z |
| 9 | -updatedAt: 2026-10-25T04:23:41.209Z | 9 | +updatedAt: 2026-11-11T07:03:15.692Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | epic: WASM Module | 11 | epic: WASM Module |
| 12 | body: "" | 12 | body: "" |
modified
.tickets/issues/0152-dev-team.yaml +6 -7 | @@ -1,12 +1,12 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 152 |
| 3 | 3 | title: Dev team |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-25T04:44:30.351Z |
| 9 | -updatedAt: 2026-10-25T12:30:00.000Z | |
| 9 | +updatedAt: 2026-11-11T06:49:12.500Z | |
| 10 | 10 | labels: [] |
| 11 | 11 | epic: Demos |
| 12 | 12 | body: |+ |
| @@ -64,7 +64,7 @@ tasks: | ||
| 64 | 64 | createdAt: 2026-10-25T07:30:00.000Z |
| 65 | 65 | updatedAt: 2026-10-25T07:30:00.000Z |
| 66 | 66 | - 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 | 68 | state: closed |
| 69 | 69 | priority: none |
| 70 | 70 | author: |
| @@ -73,7 +73,7 @@ tasks: | ||
| 73 | 73 | createdAt: 2026-10-25T07:30:00.000Z |
| 74 | 74 | updatedAt: 2026-10-25T07:30:00.000Z |
| 75 | 75 | - 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 | 77 | state: closed |
| 78 | 78 | priority: none |
| 79 | 79 | author: |
| @@ -82,7 +82,7 @@ tasks: | ||
| 82 | 82 | createdAt: 2026-10-25T07:30:00.000Z |
| 83 | 83 | updatedAt: 2026-10-25T07:30:00.000Z |
| 84 | 84 | - 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 | 86 | state: closed |
| 87 | 87 | priority: none |
| 88 | 88 | author: |
| @@ -91,7 +91,7 @@ tasks: | ||
| 91 | 91 | createdAt: 2026-10-25T07:30:00.000Z |
| 92 | 92 | updatedAt: 2026-10-25T07:30:00.000Z |
| 93 | 93 | - 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 | 95 | state: closed |
| 96 | 96 | priority: none |
| 97 | 97 | author: |
| @@ -473,4 +473,3 @@ comments: | ||
| 473 | 473 | |
| 474 | 474 | Remarque : dev.team/go.mod n'était plus là (supprimé de ton côté ?) — |
| 475 | 475 | 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: 152 | 2 | id: 152 |
| 3 | title: Dev team | 3 | title: Dev team |
| 4 | -state: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-25T04:44:30.351Z | 8 | createdAt: 2026-10-25T04:44:30.351Z |
| 9 | -updatedAt: 2026-10-25T12:30:00.000Z | 9 | +updatedAt: 2026-11-11T06:49:12.500Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | epic: Demos | 11 | epic: Demos |
| 12 | body: |+ | 12 | body: |+ |
| @@ -64,7 +64,7 @@ tasks: | |||
| 64 | createdAt: 2026-10-25T07:30:00.000Z | 64 | createdAt: 2026-10-25T07:30:00.000Z |
| 65 | updatedAt: 2026-10-25T07:30:00.000Z | 65 | updatedAt: 2026-10-25T07:30:00.000Z |
| 66 | - id: 2 | 66 | - 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: closed | 68 | state: closed |
| 69 | priority: none | 69 | priority: none |
| 70 | author: | 70 | author: |
| @@ -73,7 +73,7 @@ tasks: | |||
| 73 | createdAt: 2026-10-25T07:30:00.000Z | 73 | createdAt: 2026-10-25T07:30:00.000Z |
| 74 | updatedAt: 2026-10-25T07:30:00.000Z | 74 | updatedAt: 2026-10-25T07:30:00.000Z |
| 75 | - id: 3 | 75 | - 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: closed | 77 | state: closed |
| 78 | priority: none | 78 | priority: none |
| 79 | author: | 79 | author: |
| @@ -82,7 +82,7 @@ tasks: | |||
| 82 | createdAt: 2026-10-25T07:30:00.000Z | 82 | createdAt: 2026-10-25T07:30:00.000Z |
| 83 | updatedAt: 2026-10-25T07:30:00.000Z | 83 | updatedAt: 2026-10-25T07:30:00.000Z |
| 84 | - id: 4 | 84 | - 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: closed | 86 | state: closed |
| 87 | priority: none | 87 | priority: none |
| 88 | author: | 88 | author: |
| @@ -91,7 +91,7 @@ tasks: | |||
| 91 | createdAt: 2026-10-25T07:30:00.000Z | 91 | createdAt: 2026-10-25T07:30:00.000Z |
| 92 | updatedAt: 2026-10-25T07:30:00.000Z | 92 | updatedAt: 2026-10-25T07:30:00.000Z |
| 93 | - id: 5 | 93 | - 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: closed | 95 | state: closed |
| 96 | priority: none | 96 | 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: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-26T06:30:00.000Z |
| 9 | -updatedAt: 2026-10-26T06:30:00.000Z | |
| 9 | +updatedAt: 2026-11-11T07:00:55.349Z | |
| 10 | 10 | labels: |
| 11 | 11 | - kind::feature |
| 12 | +milestone: v2.0.0 | |
| 12 | 13 | epic: SDK |
| 13 | 14 | related: |
| 14 | 15 | - 167 |
| @@ -42,7 +43,7 @@ tasks: | ||
| 42 | 43 | email: ph.charriere@gmail.com |
| 43 | 44 | createdAt: 2026-10-26T06:30:00.000Z |
| 44 | 45 | - id: 2 |
| 45 | - title: "Get Nushell (sandbox or the user's machine)" | |
| 46 | + title: Get Nushell (sandbox or the user's machine) | |
| 46 | 47 | state: open |
| 47 | 48 | priority: none |
| 48 | 49 | author: |
| @@ -58,7 +59,7 @@ tasks: | ||
| 58 | 59 | email: ph.charriere@gmail.com |
| 59 | 60 | createdAt: 2026-10-26T06:30:00.000Z |
| 60 | 61 | - id: 4 |
| 61 | - title: "Permission callback (session/request_permission), refuse by default" | |
| 62 | + title: Permission callback (session/request_permission), refuse by default | |
| 62 | 63 | state: open |
| 63 | 64 | priority: none |
| 64 | 65 | author: |
| @@ -66,7 +67,7 @@ tasks: | ||
| 66 | 67 | email: ph.charriere@gmail.com |
| 67 | 68 | createdAt: 2026-10-26T06:30:00.000Z |
| 68 | 69 | - 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 | 71 | state: open |
| 71 | 72 | priority: none |
| 72 | 73 | author: |
| @@ -74,7 +75,7 @@ tasks: | ||
| 74 | 75 | email: ph.charriere@gmail.com |
| 75 | 76 | createdAt: 2026-10-26T06:30:00.000Z |
| 76 | 77 | - id: 6 |
| 77 | - title: "README EN + FR with measured results and limits" | |
| 78 | + title: README EN + FR with measured results and limits | |
| 78 | 79 | state: open |
| 79 | 80 | priority: none |
| 80 | 81 | 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: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-26T06:30:00.000Z | 8 | createdAt: 2026-10-26T06:30:00.000Z |
| 9 | -updatedAt: 2026-10-26T06:30:00.000Z | 9 | +updatedAt: 2026-11-11T07:00:55.349Z |
| 10 | labels: | 10 | labels: |
| 11 | - kind::feature | 11 | - kind::feature |
| 12 | +milestone: v2.0.0 | ||
| 12 | epic: SDK | 13 | epic: SDK |
| 13 | related: | 14 | related: |
| 14 | - 167 | 15 | - 167 |
| @@ -42,7 +43,7 @@ tasks: | |||
| 42 | email: ph.charriere@gmail.com | 43 | email: ph.charriere@gmail.com |
| 43 | createdAt: 2026-10-26T06:30:00.000Z | 44 | createdAt: 2026-10-26T06:30:00.000Z |
| 44 | - id: 2 | 45 | - id: 2 |
| 45 | - title: "Get Nushell (sandbox or the user's machine)" | 46 | + title: Get Nushell (sandbox or the user's machine) |
| 46 | state: open | 47 | state: open |
| 47 | priority: none | 48 | priority: none |
| 48 | author: | 49 | author: |
| @@ -58,7 +59,7 @@ tasks: | |||
| 58 | email: ph.charriere@gmail.com | 59 | email: ph.charriere@gmail.com |
| 59 | createdAt: 2026-10-26T06:30:00.000Z | 60 | createdAt: 2026-10-26T06:30:00.000Z |
| 60 | - id: 4 | 61 | - id: 4 |
| 61 | - title: "Permission callback (session/request_permission), refuse by default" | 62 | + title: Permission callback (session/request_permission), refuse by default |
| 62 | state: open | 63 | state: open |
| 63 | priority: none | 64 | priority: none |
| 64 | author: | 65 | author: |
| @@ -66,7 +67,7 @@ tasks: | |||
| 66 | email: ph.charriere@gmail.com | 67 | email: ph.charriere@gmail.com |
| 67 | createdAt: 2026-10-26T06:30:00.000Z | 68 | createdAt: 2026-10-26T06:30:00.000Z |
| 68 | - id: 5 | 69 | - 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: open | 71 | state: open |
| 71 | priority: none | 72 | priority: none |
| 72 | author: | 73 | author: |
| @@ -74,7 +75,7 @@ tasks: | |||
| 74 | email: ph.charriere@gmail.com | 75 | email: ph.charriere@gmail.com |
| 75 | createdAt: 2026-10-26T06:30:00.000Z | 76 | createdAt: 2026-10-26T06:30:00.000Z |
| 76 | - id: 6 | 77 | - id: 6 |
| 77 | - title: "README EN + FR with measured results and limits" | 78 | + title: README EN + FR with measured results and limits |
| 78 | state: open | 79 | state: open |
| 79 | priority: none | 80 | 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 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | 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 | 4 | state: open |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-26T19:30:00.000Z |
| 9 | -updatedAt: 2026-10-26T19:30:00.000Z | |
| 9 | +updatedAt: 2026-11-11T06:48:27.885Z | |
| 10 | 10 | labels: |
| 11 | 11 | - kind::feature |
| 12 | +milestone: v2.0.0 | |
| 12 | 13 | epic: Providers (LLM Engine) |
| 13 | 14 | related: |
| 14 | 15 | - 176 |
| @@ -60,7 +61,7 @@ tasks: | ||
| 60 | 61 | email: ph.charriere@gmail.com |
| 61 | 62 | createdAt: 2026-10-26T19:30:00.000Z |
| 62 | 63 | - 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 | 65 | state: open |
| 65 | 66 | priority: none |
| 66 | 67 | author: |
| @@ -76,7 +77,7 @@ tasks: | ||
| 76 | 77 | email: ph.charriere@gmail.com |
| 77 | 78 | createdAt: 2026-10-26T19:30:00.000Z |
| 78 | 79 | - 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 | 81 | state: open |
| 81 | 82 | priority: none |
| 82 | 83 | author: |
| @@ -84,7 +85,7 @@ tasks: | ||
| 84 | 85 | email: ph.charriere@gmail.com |
| 85 | 86 | createdAt: 2026-10-26T19:30:00.000Z |
| 86 | 87 | - 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 | 89 | state: open |
| 89 | 90 | priority: none |
| 90 | 91 | author: |
| @@ -92,7 +93,7 @@ tasks: | ||
| 92 | 93 | email: ph.charriere@gmail.com |
| 93 | 94 | createdAt: 2026-10-26T19:30:00.000Z |
| 94 | 95 | - id: 6 |
| 95 | - title: "mm init and docs EN + FR" | |
| 96 | + title: mm init and docs EN + FR | |
| 96 | 97 | state: open |
| 97 | 98 | priority: none |
| 98 | 99 | 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: 178 | 2 | 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: open | 4 | state: open |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-26T19:30:00.000Z | 8 | createdAt: 2026-10-26T19:30:00.000Z |
| 9 | -updatedAt: 2026-10-26T19:30:00.000Z | 9 | +updatedAt: 2026-11-11T06:48:27.885Z |
| 10 | labels: | 10 | labels: |
| 11 | - kind::feature | 11 | - kind::feature |
| 12 | +milestone: v2.0.0 | ||
| 12 | epic: Providers (LLM Engine) | 13 | epic: Providers (LLM Engine) |
| 13 | related: | 14 | related: |
| 14 | - 176 | 15 | - 176 |
| @@ -60,7 +61,7 @@ tasks: | |||
| 60 | email: ph.charriere@gmail.com | 61 | email: ph.charriere@gmail.com |
| 61 | createdAt: 2026-10-26T19:30:00.000Z | 62 | createdAt: 2026-10-26T19:30:00.000Z |
| 62 | - id: 2 | 63 | - 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: open | 65 | state: open |
| 65 | priority: none | 66 | priority: none |
| 66 | author: | 67 | author: |
| @@ -76,7 +77,7 @@ tasks: | |||
| 76 | email: ph.charriere@gmail.com | 77 | email: ph.charriere@gmail.com |
| 77 | createdAt: 2026-10-26T19:30:00.000Z | 78 | createdAt: 2026-10-26T19:30:00.000Z |
| 78 | - id: 4 | 79 | - 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: open | 81 | state: open |
| 81 | priority: none | 82 | priority: none |
| 82 | author: | 83 | author: |
| @@ -84,7 +85,7 @@ tasks: | |||
| 84 | email: ph.charriere@gmail.com | 85 | email: ph.charriere@gmail.com |
| 85 | createdAt: 2026-10-26T19:30:00.000Z | 86 | createdAt: 2026-10-26T19:30:00.000Z |
| 86 | - id: 5 | 87 | - 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: open | 89 | state: open |
| 89 | priority: none | 90 | priority: none |
| 90 | author: | 91 | author: |
| @@ -92,7 +93,7 @@ tasks: | |||
| 92 | email: ph.charriere@gmail.com | 93 | email: ph.charriere@gmail.com |
| 93 | createdAt: 2026-10-26T19:30:00.000Z | 94 | createdAt: 2026-10-26T19:30:00.000Z |
| 94 | - id: 6 | 95 | - id: 6 |
| 95 | - title: "mm init and docs EN + FR" | 96 | + title: mm init and docs EN + FR |
| 96 | state: open | 97 | state: open |
| 97 | priority: none | 98 | 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: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-31T16:39:23.000Z |
| 9 | -updatedAt: 2026-11-01T20:42:22.000Z | |
| 9 | +updatedAt: 2026-11-11T00:28:50.000Z | |
| 10 | 10 | labels: |
| 11 | 11 | - enhancement |
| 12 | 12 | - kind::feature |
| 13 | 13 | - tools |
| 14 | -milestone: v2.0.0 | |
| 14 | +milestone: v1.0.6 | |
| 15 | 15 | epic: ACP integration |
| 16 | 16 | body: | |
| 17 | 17 | ## Context |
| @@ -6,12 +6,12 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-31T16:39:23.000Z | 8 | createdAt: 2026-10-31T16:39:23.000Z |
| 9 | -updatedAt: 2026-11-01T20:42:22.000Z | 9 | +updatedAt: 2026-11-11T00:28:50.000Z |
| 10 | labels: | 10 | labels: |
| 11 | - enhancement | 11 | - enhancement |
| 12 | - kind::feature | 12 | - kind::feature |
| 13 | - tools | 13 | - tools |
| 14 | -milestone: v2.0.0 | 14 | +milestone: v1.0.6 |
| 15 | epic: ACP integration | 15 | epic: ACP integration |
| 16 | body: | | 16 | body: | |
| 17 | ## Context | 17 | ## Context |
modified
.tickets/issues/0194-better-understanding-of-the-code.yaml +2 -1 | @@ -6,9 +6,10 @@ author: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-11-07T04:16:22.000Z |
| 9 | -updatedAt: 2026-11-07T04:16:44.000Z | |
| 9 | +updatedAt: 2026-11-11T00:29:35.282Z | |
| 10 | 10 | labels: [] |
| 11 | 11 | milestone: v1.0.6 |
| 12 | +epic: Documentation | |
| 12 | 13 | body: "" |
| 13 | 14 | tasks: |
| 14 | 15 | - id: 1 |
| @@ -6,9 +6,10 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-11-07T04:16:22.000Z | 8 | createdAt: 2026-11-07T04:16:22.000Z |
| 9 | -updatedAt: 2026-11-07T04:16:44.000Z | 9 | +updatedAt: 2026-11-11T00:29:35.282Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | milestone: v1.0.6 | 11 | milestone: v1.0.6 |
| 12 | +epic: Documentation | ||
| 12 | body: "" | 13 | body: "" |
| 13 | tasks: | 14 | tasks: |
| 14 | - id: 1 | 15 | - id: 1 |
modified
.tickets/issues/0197-how-to-use-mini-me-to-help-making-suggestions-wh.yaml +2 -1 | @@ -6,9 +6,10 @@ author: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-11-07T08:02:50.274Z |
| 9 | -updatedAt: 2026-11-07T08:02:50.274Z | |
| 9 | +updatedAt: 2026-11-11T00:30:34.700Z | |
| 10 | 10 | labels: [] |
| 11 | 11 | milestone: v1.0.6 |
| 12 | +epic: Documentation | |
| 12 | 13 | body: "" |
| 13 | 14 | tasks: [] |
| 14 | 15 | comments: [] |
| @@ -6,9 +6,10 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-11-07T08:02:50.274Z | 8 | createdAt: 2026-11-07T08:02:50.274Z |
| 9 | -updatedAt: 2026-11-07T08:02:50.274Z | 9 | +updatedAt: 2026-11-11T00:30:34.700Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | milestone: v1.0.6 | 11 | 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: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-11-10T21:56:01.000Z |
| 9 | -updatedAt: 2026-11-10T23:30:00.000Z | |
| 9 | +updatedAt: 2026-11-11T00:30:03.268Z | |
| 10 | 10 | labels: |
| 11 | 11 | - kind::feature |
| 12 | 12 | - tools |
| 13 | 13 | milestone: v1.0.6 |
| 14 | +epic: ACP integration | |
| 14 | 15 | body: | |
| 15 | 16 | In ACP mode (`mm -acp`), the agent must **discover** the custom extensions |
| 16 | 17 | the ACP client exposes, and **call them like any other tool**. Discovery |
| @@ -6,11 +6,12 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-11-10T21:56:01.000Z | 8 | createdAt: 2026-11-10T21:56:01.000Z |
| 9 | -updatedAt: 2026-11-10T23:30:00.000Z | 9 | +updatedAt: 2026-11-11T00:30:03.268Z |
| 10 | labels: | 10 | labels: |
| 11 | - kind::feature | 11 | - kind::feature |
| 12 | - tools | 12 | - tools |
| 13 | milestone: v1.0.6 | 13 | milestone: v1.0.6 |
| 14 | +epic: ACP integration | ||
| 14 | body: | | 15 | body: | |
| 15 | In ACP mode (`mm -acp`), the agent must **discover** the custom extensions | 16 | In ACP mode (`mm -acp`), the agent must **discover** the custom extensions |
| 16 | the ACP client exposes, and **call them like any other tool**. Discovery | 17 | 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 @@ | ||
| 1 | 1 | # 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? | |
| 4 | 4 | state: open |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | 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 | |
| 11 | 12 | milestone: v2.0.0 |
| 12 | -epic: Providers (LLM Engine) | |
| 13 | +epic: SDK | |
| 13 | 14 | body: "" |
| 14 | 15 | tasks: [] |
| 15 | 16 | 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: 75 | 2 | +id: 203 |
| 3 | -title: Add Gronk provider | 3 | +title: How to distribute the SDKs? |
| 4 | state: open | 4 | state: open |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | -createdAt: 2026-10-06T03:58:37.611Z | 8 | +createdAt: 2026-11-11T06:59:37.515Z |
| 9 | -updatedAt: 2026-11-01T21:05:08.000Z | 9 | +updatedAt: 2026-11-11T07:00:02.502Z |
| 10 | -labels: [] | 10 | +labels: |
| 11 | + - priority::high | ||
| 11 | milestone: v2.0.0 | 12 | 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 @@ | ||
| 1 | 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 | 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 | 15 | - [Run mini-me on llama.cpp instead of Docker Model Runner](how-to/run-with-llama-cpp.md) |
| 16 | 16 | - [Run mini-me on llmman](how-to/run-with-llmman.md) — `provider: llmman`, the demo config in `demos/llmman/` |
| 17 | 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 | 19 | - [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 | 20 | - [Ask another mm agent from your session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<name>`, `/ask-with-context-<name>` |
| 20 | 21 | - [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 gateway | 19 | - [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 jq | 21 | - [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 | ||
| 16 | 16 | |---|---|---| |
| 17 | 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 | 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 | 20 | | `dmr` | `/models` | the model list only | |
| 20 | 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 | 108 | ## 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 | 111 | - A `[card]` number was copied, not measured in this repository. |
| 111 | 112 | - The families are matched on words in the model id: a model renamed by its publisher may land in `other`. |
| 112 | 113 | |
| @@ -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 limits | 108 | ## 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 @@ | ||
| 2 | 2 | |
| 3 | 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 | 7 | ## Why it is designed this way |
| 8 | 8 | |
| @@ -20,6 +20,8 @@ | ||
| 20 | 20 | |
| 21 | 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 | 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 | 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 | 41 | - The two configuration files that parametrise each provider are listed in the [configuration reference](../reference/configuration.md). |
| 40 | 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 | 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). |
| @@ -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 way | 7 | ## 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: | ||
| 48 | 48 | |
| 49 | 49 | ## 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 | 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. |
| @@ -48,6 +48,6 @@ Three properties hold everywhere: | |||
| 48 | 48 | ||
| 49 | ## What this is not | 49 | ## 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" | ||
| 165 | 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 | 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 | 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 | 171 | ```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 | ```bash | 171 | ```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 | ||
| 8 | 8 | |
| 9 | 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 | 13 | ```yaml |
| 14 | 14 | sampling: |
| @@ -61,7 +61,7 @@ A thinking model (Qwen3.5, Qwen3.x, gpt-oss…) reasons before it answers. On a | ||
| 61 | 61 | - On `ollama`, `reasoning_effort` goes at the top level of `sampling`, not under `chat_template_kwargs`; only `none` has been measured there. |
| 62 | 62 | - Compaction summaries do not follow it — see below. |
| 63 | 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 | 66 | ## See also |
| 67 | 67 | |
| @@ -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 | ```yaml | 13 | ```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 also | 66 | ## See also |
| 67 | 67 | ||
modified
docs/en/how-to/unload-a-model.md +2 -2 | @@ -1,6 +1,6 @@ | ||
| 1 | 1 | # 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 | 5 | ## Unload by hand |
| 6 | 6 | |
| @@ -22,7 +22,7 @@ Name another model served by the same server to unload that one instead: | ||
| 22 | 22 | /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 | 27 | ## Let the agent unload a model |
| 28 | 28 | |
| @@ -1,6 +1,6 @@ | |||
| 1 | # How to free a model's memory | 1 | # 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 hand | 5 | ## 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_M | 22 | /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 model | 27 | ## 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. | ||
| 71 | 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 | 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 | 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 | 76 | ## 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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. | |
| @@ -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 output | 76 | ## 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 @@ | ||
| 6 | 6 | |
| 7 | 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 | 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 | 11 | | `baseUrl` | string | `""` (provider default) | OpenAI-compatible endpoint tried first. Overridden by `AGENT_BASE_URL`. | |
| 12 | 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 | 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 | 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 | 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). | | |
| 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`). | | |
| 37 | 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 | 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 | 40 | | `context` | map | see below | Automatic history compression. | |
| 41 | 41 | | `sessions` | map | see below | Persistent, per-project sessions. | |
| @@ -59,7 +59,7 @@ | ||
| 59 | 59 | | `threshold` | int | `75` | Percentage of the context window beyond which the history is compressed before the next question. Between 1 and 100. | |
| 60 | 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 | 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 | 63 | | `prompt` | string | `""` | Replaces the built-in summary prompt when not empty. | |
| 64 | 64 | | `showStats` | bool | `true` | Print the one-line `🗜️` report after each compression. | |
| 65 | 65 | |
| @@ -197,6 +197,7 @@ tools: | ||
| 197 | 197 | | `llamacpp` | `http://127.0.0.1:8080/v1` | none | `LLAMA_API_KEY` | no | |
| 198 | 198 | | `llmman` | `http://127.0.0.1:17434/v1` | `http://host.docker.internal:17434/v1` | `LLMMAN_API_KEY` | no | |
| 199 | 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 | 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 | 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 | 465 | | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | a name starting with `with-context` | |
| 465 | 466 | | `mmConfigs.<name>: the path must not be empty` | `<name>: ""`, or a long form without `path` | |
| 466 | 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 | 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 | |
| @@ -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 | ||
| 311 | 311 | | Aspect | Behaviour | |
| 312 | 312 | |--------|-----------| |
| 313 | 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 | 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 | 316 | | `dmr` | `cannot unload <model>: the dmr provider cannot unload a model`. | |
| 316 | 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). | |
| @@ -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 @@ | ||
| 1 | 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 | 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 | 15 | - [Faire tourner mini-me sur llama.cpp plutôt que Docker Model Runner](how-to/run-with-llama-cpp.md) |
| 16 | 16 | - [Faire tourner mini-me sur llmman](how-to/run-with-llmman.md) — `provider: llmman`, la config de démo dans `demos/llmman/` |
| 17 | 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 | 19 | - [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 | 20 | - [Interroger un autre agent mm depuis votre session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<nom>`, `/ask-with-context-<nom>` |
| 20 | 21 | - [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 passerelle | 19 | - [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 jq | 21 | - [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 | ||
| 16 | 16 | |---|---|---| |
| 17 | 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 | 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 | 20 | | `dmr` | `/models` | la liste des modèles seulement | |
| 20 | 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 | 108 | ## 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 | 111 | - Une valeur `[card]` a été recopiée, pas mesurée dans ce dépôt. |
| 111 | 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 | |
| @@ -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 connues | 108 | ## 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 @@ | ||
| 2 | 2 | |
| 3 | 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 | 7 | ## Pourquoi c'est conçu ainsi |
| 8 | 8 | |
| @@ -20,6 +20,8 @@ | ||
| 20 | 20 | |
| 21 | 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 | 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 | 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 | 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 | 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 | 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). |
| @@ -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 ainsi | 7 | ## 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 : | ||
| 48 | 48 | |
| 49 | 49 | ## 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 | 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. |
| @@ -48,6 +48,6 @@ Trois propriétés valent partout : | |||
| 48 | 48 | ||
| 49 | ## Ce que ce n'est pas | 49 | ## 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" | ||
| 165 | 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 | 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 | 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 | 171 | ```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 | ```bash | 171 | ```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 | ||
| 8 | 8 | |
| 9 | 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 | 13 | ```yaml |
| 14 | 14 | sampling: |
| @@ -61,7 +61,7 @@ Un modèle à raisonnement (Qwen3.5, Qwen3.x, gpt-oss…) réfléchit avant de r | ||
| 61 | 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 | 62 | - Les résumés de compaction ne le suivent pas — voir ci-dessous. |
| 63 | 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 | 66 | ## Voir aussi |
| 67 | 67 | |
| @@ -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 | ```yaml | 13 | ```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 aussi | 66 | ## Voir aussi |
| 67 | 67 | ||
modified
docs/fr/how-to/unload-a-model.md +2 -2 | @@ -1,6 +1,6 @@ | ||
| 1 | 1 | # 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 | 5 | ## 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 | 22 | /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 | 27 | ## Laisser l'agent décharger un modèle |
| 28 | 28 | |
| @@ -1,6 +1,6 @@ | |||
| 1 | # Libérer la mémoire d'un modèle | 1 | # 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 main | 5 | ## 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_M | 22 | /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èle | 27 | ## 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. | ||
| 71 | 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 | 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 | 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 | 76 | ## 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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. | |
| @@ -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émarrage | 76 | ## 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 @@ | ||
| 6 | 6 | |
| 7 | 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 | 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 | 11 | | `baseUrl` | chaîne | `""` (défaut du fournisseur) | Point d'accès compatible OpenAI essayé en premier. Surchargé par `AGENT_BASE_URL`. | |
| 12 | 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 | 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 | 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 | 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). | | |
| 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`). | | |
| 37 | 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 | 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 | 40 | | `context` | dictionnaire | voir ci-dessous | Compression automatique de l'historique. | |
| 41 | 41 | | `sessions` | dictionnaire | voir ci-dessous | Sessions persistantes, par projet. | |
| @@ -59,7 +59,7 @@ | ||
| 59 | 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 | 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 | 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 | 63 | | `prompt` | chaîne | `""` | Remplace le prompt de résumé intégré quand non vide. | |
| 64 | 64 | | `showStats` | booléen | `true` | Affiche le rapport `🗜️` d'une ligne après chaque compression. | |
| 65 | 65 | |
| @@ -197,6 +197,7 @@ tools: | ||
| 197 | 197 | | `llamacpp` | `http://127.0.0.1:8080/v1` | aucun | `LLAMA_API_KEY` | non | |
| 198 | 198 | | `llmman` | `http://127.0.0.1:17434/v1` | `http://host.docker.internal:17434/v1` | `LLMMAN_API_KEY` | non | |
| 199 | 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 | 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 | 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 | 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 | 466 | | `mmConfigs.<nom>: the path must not be empty` | `<nom>: ""`, ou une forme longue sans `path` | |
| 466 | 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 | 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 | |
| @@ -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 | ||
| 311 | 311 | | Aspect | Comportement | |
| 312 | 312 | |--------|--------------| |
| 313 | 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 | 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 | 316 | | `dmr` | `cannot unload <modèle>: the dmr provider cannot unload a model`. | |
| 316 | 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). | |
| @@ -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() { | ||
| 176 | 176 | if s.Thinking && values["temperature"] < 0.5 { |
| 177 | 177 | 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 | 185 | for _, k := range []string{"temperature", "top_p", "top_k", "min_p"} { |
| 180 | 186 | if v, ok := values[k]; ok { |
| 181 | 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 | 191 | if s.Thinking { |
| 186 | 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 | 198 | maxKey := "max_tokens" |
| 189 | 199 | if s.Family.MaxTokensKey != "" { |
| 190 | 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 | 203 | if s.anyTool() { |
| 194 | 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 | 278 | func number(key string, v float64) string { |
| 269 | - if key == "top_k" { | |
| 279 | + if key == "top_k" || key == "max_tokens" { | |
| 270 | 280 | return fmt.Sprint(int(v)) |
| 271 | 281 | } |
| 272 | 282 | 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", OriginRule | 177 | 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) { | ||
| 69 | 69 | map[string]string{"temperature": "1.0"}, map[string]string{"reasoning_effort": "medium"}}, |
| 70 | 70 | {"no switch: no kwargs, no reasoning", Choices{Model: "llama3.2:3b", Usage: "developer", Machine: "small"}, Facts{}, |
| 71 | 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 | 79 | for _, c := range cases { |
| 74 | 80 | 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 ( | ||
| 6 | 6 | "encoding/json" |
| 7 | 7 | "fmt" |
| 8 | 8 | "net/http" |
| 9 | + neturl "net/url" | |
| 9 | 10 | "os" |
| 10 | 11 | "regexp" |
| 11 | 12 | "sort" |
| @@ -23,6 +24,15 @@ type Facts struct { | ||
| 23 | 24 | ContextLength int // the model's native window, 0 when unknown |
| 24 | 25 | Sampling map[string]float64 // the GGUF's recommended sampling: temperature, top_p, top_k, min_p |
| 25 | 26 | 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 | 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 | 166 | // ModelFacts asks the server about one model. llmman and Ollama answer |
| 157 | 167 | // /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. | |
| 160 | 172 | func ModelFacts(ctx context.Context, provider, base, model string) Facts { |
| 161 | 173 | switch provider { |
| 162 | 174 | case "llmman", "ollama": |
| 163 | 175 | return showFacts(ctx, base, model) |
| 164 | 176 | case "llamacpp": |
| 165 | 177 | return propsFacts(ctx, base) |
| 178 | + case "kronk": | |
| 179 | + return kronkFacts(ctx, base, model) | |
| 166 | 180 | } |
| 167 | 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 | 263 | func showFacts(ctx context.Context, base, model string) Facts { |
| 171 | 264 | var show struct { |
| 172 | 265 | 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 unknown | 24 | 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_p | 25 | Sampling map[string]float64 // the GGUF's recommended sampling: temperature, top_p, top_k, min_p |
| 25 | Thinking Thinking | 26 | 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 answer | 166 | // ModelFacts asks the server about one model. llmman and Ollama answer |
| 157 | // /api/show (GGUF metadata, chat template); llama-server answers /props | 167 | // /api/show (GGUF metadata, chat template); llama-server answers /props |
| 158 | -// (window, chat template); Docker Model Runner says nothing more than the | 168 | +// (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) { | ||
| 112 | 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 | +} | |
| @@ -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 { | ||
| 192 | 192 | break |
| 193 | 193 | } |
| 194 | 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 | 197 | st.Default = models[0] |
| 198 | 198 | case "family": |
| @@ -192,7 +192,7 @@ func (f *Flow) Step(key string) Step { | |||
| 192 | break | 192 | 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 { | ||
| 189 | 189 | } |
| 190 | 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 | 201 | if f.Thinking.Switch != "" { |
| 193 | 202 | sw := "reasoning switch " + f.Thinking.Switch |
| 194 | 203 | 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.Switch | 202 | 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 ( | ||
| 19 | 19 | ) |
| 20 | 20 | |
| 21 | 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 | 24 | // local are the providers probed for the first question's default: never |
| 25 | 25 | // `openai`, whose default address is the OpenAI API — mm init must not call |
| 26 | 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 | 29 | // Window asks the flow's questions in a full-screen window (internal/tui's |
| 30 | 30 | // RunInit): it returns false when the user quit without writing. main hands |
| @@ -236,6 +236,15 @@ func (w *lineWizard) ask(st Step) string { | ||
| 236 | 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 | 248 | for { |
| 240 | 249 | lines := elicit.Lines(q) |
| 241 | 250 | // 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: never | 24 | // 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 call | 25 | // `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's | 29 | // 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 hands | 30 | // 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 ( | ||
| 20 | 20 | ) |
| 21 | 21 | |
| 22 | 22 | // 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. | |
| 26 | 27 | // Each server keeps its own entry even when two look alike today: a difference |
| 27 | 28 | // found later (how to turn reasoning off, where the window is) is then one |
| 28 | 29 | // field to change, not a provider to split. |
| @@ -153,6 +154,28 @@ func init() { | ||
| 153 | 154 | unload: unloadKeepAlive, |
| 154 | 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 | 181 | 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 OpenAI | 22 | // openaiCompat is the Provider for every server that speaks the OpenAI |
| 23 | -// chat-completions protocol. Docker Model Runner, llama-server, llmman and | 23 | +// chat-completions protocol. Docker Model Runner, llama-server, llmman, |
| 24 | -// Ollama's /v1 are four parametrisations of it; vLLM and the remote APIs would | 24 | +// 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 difference | 27 | // 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 one | 28 | // 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 } | ||
| 75 | 75 | // where DMR_BASE_URL is often exported. |
| 76 | 76 | func clearEnv(t *testing.T) { |
| 77 | 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 | 79 | t.Setenv(k, "") |
| 80 | 80 | } |
| 81 | 81 | } |
| 82 | 82 | |
| 83 | 83 | func TestLookupListsKnownProviders(t *testing.T) { |
| 84 | 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 | 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 | 118 | {"ollama falls back to the host's server", "ollama", config.Config{BaseURL: down}, nil, "http://host.docker.internal:11434/v1", "not-needed"}, |
| 119 | 119 | {"ollama keeps a reachable baseUrl", "ollama", config.Config{BaseURL: up.URL}, nil, up.URL, "not-needed"}, |
| 120 | 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 | 125 | for _, tc := range cases { |
| 123 | 126 | t.Run(tc.name, func(t *testing.T) { |
| @@ -554,6 +557,7 @@ func TestDefaultURLs(t *testing.T) { | ||
| 554 | 557 | cases := []struct{ provider, base, fallback string }{ |
| 555 | 558 | {"llmman", "http://127.0.0.1:17434/v1", "http://host.docker.internal:17434/v1"}, |
| 556 | 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 | 561 | {"dmr", "http://localhost:12434/engines/v1", "http://host.docker.internal:12434/engines/v1"}, |
| 558 | 562 | {"llamacpp", "http://127.0.0.1:8080/v1", ""}, |
| 559 | 563 | {"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 { | ||
| 423 | 423 | // model it is the full identifier: two short names can be the same, and the |
| 424 | 424 | // identifier is what the file will hold. |
| 425 | 425 | 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 | |
| 428 | 431 | } |
| 429 | 432 | return o.Detail |
| 430 | 433 | } |
| @@ -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 the | 423 | // 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.Value | 427 | + if o.Detail == "" { |
| 428 | + return o.Value | ||
| 429 | + } | ||
| 430 | + return o.Value + " — " + o.Detail | ||
| 428 | } | 431 | } |
| 429 | return o.Detail | 432 | return o.Detail |
| 430 | } | 433 | } |
modified
internal/tui/modelname.go +24 -0 | @@ -50,6 +50,10 @@ func shortModel(id string) string { | ||
| 50 | 50 | } |
| 51 | 51 | // The registry host and the organisation are routing, not identity. |
| 52 | 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 | 57 | id = id[i+1:] |
| 54 | 58 | } |
| 55 | 59 | name, tag, hasTag := strings.Cut(id, ":") |
| @@ -63,6 +67,26 @@ func shortModel(id string) string { | ||
| 63 | 67 | 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 | 90 | // dropFormat removes a trailing "-gguf", and a trailing "-gguf-<quant>" where |
| 67 | 91 | // the quantisation is already about to be shown as the tag. |
| 68 | 92 | 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)) + ":" + tag | 67 | 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>" where | 90 | // 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) { | ||
| 18 | 18 | {"an id with no path at all", "gpt-4o", "gpt-4o"}, |
| 19 | 19 | {"no tag, no format", "huggingface.co/qwen/qwen3-embedding-0.6b", "qwen3-embedding-0.6b"}, |
| 20 | 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 | 25 | for _, tc := range cases { |
| 23 | 26 | 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) { |