llama-mdl 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 diverse
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,364 @@
1
+ Metadata-Version: 2.4
2
+ Name: llama-mdl
3
+ Version: 0.2.0
4
+ Summary: Run one local llama.cpp server from a config file, with an optional terminal dashboard
5
+ Author: diverse
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 diverse
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/diverseau/llama-mdl
29
+ Project-URL: Repository, https://github.com/diverseau/llama-mdl
30
+ Project-URL: Issues, https://github.com/diverseau/llama-mdl/issues
31
+ Project-URL: Changelog, https://github.com/diverseau/llama-mdl/blob/main/CHANGELOG.md
32
+ Keywords: llama.cpp,llm,gguf,cli,tui,local-ai
33
+ Classifier: Environment :: Console
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Operating System :: OS Independent
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Programming Language :: Python :: 3.13
40
+ Classifier: Topic :: Utilities
41
+ Requires-Python: >=3.11
42
+ Description-Content-Type: text/markdown
43
+ License-File: LICENSE
44
+ Provides-Extra: ui
45
+ Requires-Dist: textual<9,>=3; extra == "ui"
46
+ Dynamic: license-file
47
+
48
+ # mdl
49
+
50
+ A small CLI for running one local [llama.cpp](https://github.com/ggml-org/llama.cpp)
51
+ server at a time from a config file, instead of pasting flag soup into your shell.
52
+
53
+ ```sh
54
+ llama-server -m /srv/models/Ornith-1.5-35B-A3B-Q4_K_M.gguf -ngl 99 \
55
+ --n-cpu-moe 24 -c 65536 -fa on --cache-type-k q8_0 \
56
+ --cache-type-v q8_0 -np 1 --port 8080
57
+ ```
58
+
59
+ ```sh
60
+ mdl run ornith
61
+ ```
62
+
63
+ ![the mdl dashboard](docs/screenshot.svg)
64
+
65
+ `mdl.py` is a single file, Python 3.11+ (needs `tomllib`), standard library
66
+ only. Runs on Linux, macOS and Windows.
67
+
68
+ `mdl_ui.py` adds an optional terminal dashboard (`mdl ui`). It is the only
69
+ part that needs a dependency — [Textual](https://textual.textualize.io/) —
70
+ and the CLI never imports it, so every command but `ui` stays
71
+ dependency-free.
72
+
73
+ ## Install
74
+
75
+ ```sh
76
+ pipx install llama-mdl # or: pip install llama-mdl
77
+ pipx install "llama-mdl[ui]" # with the terminal dashboard
78
+ ```
79
+
80
+ The package is `llama-mdl`; the command it installs is `mdl`. (Plain `mdl`
81
+ on PyPI is an unrelated project.) Nothing but the dashboard has a
82
+ dependency, and that is [Textual](https://textual.textualize.io/).
83
+
84
+ Or run it straight from a clone - it is two files and a standard library:
85
+
86
+ ```sh
87
+ git clone https://github.com/diverseau/llama-mdl ~/src/mdl
88
+ python ~/src/mdl/mdl.py --help
89
+ ```
90
+
91
+ Then create a starter config:
92
+
93
+ ```sh
94
+ mdl init
95
+ ```
96
+
97
+ That writes `~/.config/mdl/models.toml`, finds `llama-server` on your PATH if
98
+ it is there, and tells you what to edit.
99
+
100
+ ## Config
101
+
102
+ `~/.config/mdl/models.toml`. One table per model; the table name is what you
103
+ pass to `mdl run`.
104
+
105
+ ```toml
106
+ # Optional. Defaults to "llama-server" on $PATH.
107
+ # $MDL_LLAMA_SERVER overrides this.
108
+ llama_server = "/opt/llama.cpp/build/bin/llama-server"
109
+
110
+ [ornith]
111
+ model = "/srv/models/Ornith-1.5-35B-A3B-Q4_K_M.gguf"
112
+ ngl = 99
113
+ n_cpu_moe = 24
114
+ ctx = 65536
115
+ flash_attn = true
116
+ kv_type = "q8_0"
117
+ parallel = 1
118
+ port = 8080
119
+
120
+ [qwen-small]
121
+ model = "/srv/models/Qwen3-8B-Q5_K_M.gguf"
122
+ ngl = 99
123
+ ctx = 16384
124
+ port = 8080
125
+ ```
126
+
127
+ On Windows, write paths with forward slashes (`C:/models/foo.gguf`) or double
128
+ the backslashes, since TOML treats `\` as an escape character.
129
+
130
+ ### Keys
131
+
132
+ | Key | llama-server flag | Notes |
133
+ | --- | --- | --- |
134
+ | `model` | `-m` | Required. |
135
+ | `ngl` | `-ngl` | |
136
+ | `n_cpu_moe` | `--n-cpu-moe` | |
137
+ | `ctx` | `-c` | |
138
+ | `flash_attn` | `-fa on` | Only emitted when `true`. |
139
+ | `kv_type` | `--cache-type-k` and `--cache-type-v` | Both get the same value. |
140
+ | `parallel` | `-np` | |
141
+ | `port` | `--port` | Defaults to 8080. |
142
+ | `args` | passed through verbatim | Array of strings, appended last. |
143
+
144
+ Two top-level keys sit outside the model tables: `llama_server` (above) and
145
+ `ready_timeout`, the seconds `run` waits for `/health` before giving up.
146
+ It defaults to 300, which a 70B on a slow disk can exceed.
147
+
148
+ Anything else in a model table is an error, so a typo like `flash_atn` tells you
149
+ instead of silently doing nothing.
150
+
151
+ ## Commands
152
+
153
+ ```
154
+ mdl run <name> Start <name> in the background, tail its log until the
155
+ server answers /health, and exit. The server keeps running
156
+ after mdl exits.
157
+ mdl stop SIGTERM the running server, SIGKILL after 10s, clean up.
158
+ mdl ps [--json] name, pid, port and uptime, or "nothing running".
159
+ --json prints the state as JSON (null when idle) for
160
+ scripts and status bars.
161
+ mdl list The models defined in the config.
162
+ mdl add <gguf> Append an entry for a .gguf to the config, with sane
163
+ defaults. Takes an optional name and port.
164
+ mdl check Validate every model in the config without launching
165
+ anything. Exits non-zero if it finds a problem.
166
+ mdl init Write a starter config, if you do not have one.
167
+ mdl --version The version, for bug reports.
168
+ mdl ui The dashboard. Bare `mdl` opens it too.
169
+ mdl logs [-f] Print the running server's log; -f follows it.
170
+ Takes a model name to read a stopped one's log.
171
+ ```
172
+
173
+ Without textual installed, `mdl ui` fails with one line and bare `mdl` prints
174
+ the usage string, exactly as it always did.
175
+
176
+ ```console
177
+ $ mdl list
178
+ ornith /srv/models/Ornith-1.5-35B-A3B-Q4_K_M.gguf
179
+ qwen-small /srv/models/Qwen3-8B-Q5_K_M.gguf
180
+
181
+ $ mdl run ornith
182
+ starting ornith (pid 48812), log /home/leon/.local/state/mdl/ornith.log
183
+ load_tensors: offloaded 43/43 layers to GPU
184
+ llama_context: n_ctx = 65536
185
+ main: server is listening on http://127.0.0.1:8080
186
+ ready: ornith on http://127.0.0.1:8080 (pid 48812)
187
+
188
+ $ mdl ps
189
+ ornith pid 48812 port 8080 up 1h04m
190
+
191
+ $ mdl stop
192
+ stopped ornith (pid 48812)
193
+ ```
194
+
195
+ `add` and `check` are the two that save the most time:
196
+
197
+ ```console
198
+ $ mdl add ~/models/Qwen3-8B-Q5_K_M.gguf
199
+ added [qwen3-8b-q5-k-m] to /home/leon/.config/mdl/models.toml
200
+ Qwen3-8B-Q5_K_M.gguf (5.4G, 37 layers)
201
+ run it with: mdl run qwen3-8b-q5-k-m
202
+
203
+ $ mdl check
204
+ ornith ok
205
+ qwen-small model file not found
206
+ mdl: 1 problem(s) found
207
+ ```
208
+
209
+ `add` only appends, and `check` never launches anything, so both are safe
210
+ to run against a config you care about.
211
+
212
+ ## The UI
213
+
214
+ `mdl ui` (or just `mdl`) opens a dashboard over the same config and the same
215
+ state file. Anything you do in it is visible to the CLI and vice versa.
216
+
217
+ Idle, it lists your models with a status dot, shows the selected model's
218
+ parameters, and previews the exact `llama-server` command it would run.
219
+ `e` edits those parameters and saves them back to `models.toml`, leaving
220
+ your comments and layout alone.
221
+ Running, it swaps in live telemetry: VRAM, KV-cache use, a tokens/sec
222
+ sparkline, busy slots, and a colour-coded log tail.
223
+
224
+ ```
225
+ key does
226
+ up/down, j k select a model
227
+ enter, r run the selected model
228
+ s stop the running server
229
+ R restart
230
+ e edit ngl / ctx / kv_type / port, saved to models.toml
231
+ c copy the llama-server command
232
+ p prompt the running model without leaving the UI
233
+ l focus the log, / filters it
234
+ g reload the config
235
+ ? help
236
+ q quit the UI - the server keeps running
237
+ ```
238
+
239
+ Quitting never stops a server; `s` is the only thing that does.
240
+
241
+ The telemetry panels need llama.cpp's metrics endpoint, so add `--metrics`
242
+ to a model's `args` to light them up:
243
+
244
+ ```toml
245
+ args = ["--metrics"]
246
+ ```
247
+
248
+ Without it the dashboard still works, and those panels say `metrics off`
249
+ rather than failing. While a model is loading they say `loading` instead,
250
+ since nothing is listening yet.
251
+
252
+ ### Talking to the model
253
+
254
+ `p` opens a chat with whatever is running, without leaving the UI.
255
+
256
+ ![the chat pane](docs/chat.svg)
257
+
258
+ It keeps the conversation, so follow-up questions have context; `ctrl+l`
259
+ starts a fresh one. Reasoning is shown dimmed and timed
260
+ separately, whether the server hands it back in its own field or inline
261
+ as `<think>` tags. `esc` interrupts a running reply - it closes the
262
+ socket rather than waiting for the next token - and closes the pane once
263
+ nothing is streaming.
264
+
265
+ The rate is the server's own `tok/s` when it reports timings, and ours
266
+ otherwise. `ttft` is time to first token, which is the number that tells
267
+ you whether a long context is hurting.
268
+
269
+ ### Animation
270
+
271
+ The wordmark drifts its gradient by default. Set `ui_fx = "off"` at the
272
+ top level of the config to paint it flat, or pass `mdl ui --no-fx` for a
273
+ one-off.
274
+
275
+ ## Files
276
+
277
+ ```
278
+ ~/.config/mdl/models.toml your config
279
+ ~/.local/state/mdl/state.json name, pid, port and start time of the server
280
+ ~/.local/state/mdl/<name>.log server stdout+stderr, rotated on each run
281
+ ~/.local/state/mdl/<name>.log.1 the previous run, and .2 before that
282
+ ~/.local/state/mdl/ui-marks.json which models the UI has seen start or fail
283
+ ```
284
+
285
+ `$XDG_CONFIG_HOME` and `$XDG_STATE_HOME` are honoured if set. On Windows the
286
+ same layout lives under `%USERPROFILE%`.
287
+
288
+ ## Behaviour notes
289
+
290
+ - **One server at a time.** `run` while something is up is an error telling you
291
+ to `stop` first.
292
+ - **Readiness is an HTTP probe, not log scraping.** `run` polls `/health` on
293
+ the configured port. llama.cpp has reworded its startup line between builds;
294
+ this contract has not.
295
+ - **Obvious mistakes fail before launch.** A missing model file, a missing
296
+ binary or a busy port is one line in milliseconds, not a failed model load.
297
+ - **Stale state self-heals.** If the pid in `state.json` is gone (crash, reboot,
298
+ `kill -9`) the file is removed and `ps` reports nothing running.
299
+ - **If the server exits during startup,** `run` reports its exit status, removes
300
+ the state file, and exits 1. The log has the reason.
301
+ - **If it does not report ready in time,** `run` exits 1 but leaves the server
302
+ running, since it may still be loading. Check the log, or `mdl stop`. Raise
303
+ `ready_timeout` if 300s is genuinely not enough.
304
+ - **Pid reuse is guarded against.** The state file records the OS process
305
+ creation time, so a recycled pid is not mistaken for your server. macOS has
306
+ no cheap way to read that, so it falls back to the pid alone.
307
+ - **`stop` signals the process tree, not just the pid.** If your `llama_server`
308
+ is a wrapper script, killing the wrapper alone would orphan the real server
309
+ and leave the port held.
310
+ - **The last few logs are kept.** `<name>.log` shuffles along to `.1` and `.2`
311
+ on each run, so the crash you were not watching is still there.
312
+ - Errors are one line on stderr and a non-zero exit. No tracebacks.
313
+
314
+ ## Tests
315
+
316
+ ```sh
317
+ python tests/run.py # fast: no real model needed
318
+ python tests/run.py --live # also drives a real model through the UI
319
+ ```
320
+
321
+ The fast suites run against a temp config and a fake `llama-server`, so they
322
+ never touch `~/.config/mdl`. The POSIX process semantics (detaching, orphan
323
+ self-heal, SIGTERM escalating to SIGKILL) need Linux:
324
+
325
+ ```sh
326
+ docker run --rm -v "$PWD:/repo:ro" python:3.12-slim \
327
+ sh -c 'cp -r /repo /w && cd /w && python3 tests/integration_posix.py'
328
+ ```
329
+
330
+ ## Non-goals
331
+
332
+ These are deliberate, and issues asking for them will be closed with a link
333
+ here. `mdl` starts one server, stops it, and tells you what is running.
334
+
335
+ - **No daemon.** Nothing runs in the background except the server itself.
336
+ - **No multiple concurrent servers.** These models are measured in gigabytes
337
+ of VRAM; running two is usually a mistake, and refusing is a feature.
338
+ - **No model downloading.** Use `huggingface-cli`, or your browser.
339
+ - **No hot-swap or auto-unload.** `mdl stop && mdl run other` is two words.
340
+ - **No web UI.** llama-server already ships one.
341
+
342
+ If you want these, [llama-swap](https://github.com/mostlygeek/llama-swap)
343
+ and [Ollama](https://ollama.com) are good and solve different problems.
344
+
345
+ ## Contributing
346
+
347
+ See [CONTRIBUTING.md](CONTRIBUTING.md). Short version: open an issue first,
348
+ keep `mdl.py` free of dependencies, and run the tests.
349
+
350
+ ```sh
351
+ python tests/run.py
352
+ ruff check .
353
+ ```
354
+
355
+ CI runs the suites on Linux, macOS and Windows across Python 3.11-3.13, the
356
+ pinned Textual floor and the current release, Ruff, the POSIX process suite,
357
+ and a packaging check on both the wheel and the sdist.
358
+
359
+ Security issues go through [SECURITY.md](SECURITY.md), privately, rather
360
+ than the public tracker.
361
+
362
+ ## License
363
+
364
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,317 @@
1
+ # mdl
2
+
3
+ A small CLI for running one local [llama.cpp](https://github.com/ggml-org/llama.cpp)
4
+ server at a time from a config file, instead of pasting flag soup into your shell.
5
+
6
+ ```sh
7
+ llama-server -m /srv/models/Ornith-1.5-35B-A3B-Q4_K_M.gguf -ngl 99 \
8
+ --n-cpu-moe 24 -c 65536 -fa on --cache-type-k q8_0 \
9
+ --cache-type-v q8_0 -np 1 --port 8080
10
+ ```
11
+
12
+ ```sh
13
+ mdl run ornith
14
+ ```
15
+
16
+ ![the mdl dashboard](docs/screenshot.svg)
17
+
18
+ `mdl.py` is a single file, Python 3.11+ (needs `tomllib`), standard library
19
+ only. Runs on Linux, macOS and Windows.
20
+
21
+ `mdl_ui.py` adds an optional terminal dashboard (`mdl ui`). It is the only
22
+ part that needs a dependency — [Textual](https://textual.textualize.io/) —
23
+ and the CLI never imports it, so every command but `ui` stays
24
+ dependency-free.
25
+
26
+ ## Install
27
+
28
+ ```sh
29
+ pipx install llama-mdl # or: pip install llama-mdl
30
+ pipx install "llama-mdl[ui]" # with the terminal dashboard
31
+ ```
32
+
33
+ The package is `llama-mdl`; the command it installs is `mdl`. (Plain `mdl`
34
+ on PyPI is an unrelated project.) Nothing but the dashboard has a
35
+ dependency, and that is [Textual](https://textual.textualize.io/).
36
+
37
+ Or run it straight from a clone - it is two files and a standard library:
38
+
39
+ ```sh
40
+ git clone https://github.com/diverseau/llama-mdl ~/src/mdl
41
+ python ~/src/mdl/mdl.py --help
42
+ ```
43
+
44
+ Then create a starter config:
45
+
46
+ ```sh
47
+ mdl init
48
+ ```
49
+
50
+ That writes `~/.config/mdl/models.toml`, finds `llama-server` on your PATH if
51
+ it is there, and tells you what to edit.
52
+
53
+ ## Config
54
+
55
+ `~/.config/mdl/models.toml`. One table per model; the table name is what you
56
+ pass to `mdl run`.
57
+
58
+ ```toml
59
+ # Optional. Defaults to "llama-server" on $PATH.
60
+ # $MDL_LLAMA_SERVER overrides this.
61
+ llama_server = "/opt/llama.cpp/build/bin/llama-server"
62
+
63
+ [ornith]
64
+ model = "/srv/models/Ornith-1.5-35B-A3B-Q4_K_M.gguf"
65
+ ngl = 99
66
+ n_cpu_moe = 24
67
+ ctx = 65536
68
+ flash_attn = true
69
+ kv_type = "q8_0"
70
+ parallel = 1
71
+ port = 8080
72
+
73
+ [qwen-small]
74
+ model = "/srv/models/Qwen3-8B-Q5_K_M.gguf"
75
+ ngl = 99
76
+ ctx = 16384
77
+ port = 8080
78
+ ```
79
+
80
+ On Windows, write paths with forward slashes (`C:/models/foo.gguf`) or double
81
+ the backslashes, since TOML treats `\` as an escape character.
82
+
83
+ ### Keys
84
+
85
+ | Key | llama-server flag | Notes |
86
+ | --- | --- | --- |
87
+ | `model` | `-m` | Required. |
88
+ | `ngl` | `-ngl` | |
89
+ | `n_cpu_moe` | `--n-cpu-moe` | |
90
+ | `ctx` | `-c` | |
91
+ | `flash_attn` | `-fa on` | Only emitted when `true`. |
92
+ | `kv_type` | `--cache-type-k` and `--cache-type-v` | Both get the same value. |
93
+ | `parallel` | `-np` | |
94
+ | `port` | `--port` | Defaults to 8080. |
95
+ | `args` | passed through verbatim | Array of strings, appended last. |
96
+
97
+ Two top-level keys sit outside the model tables: `llama_server` (above) and
98
+ `ready_timeout`, the seconds `run` waits for `/health` before giving up.
99
+ It defaults to 300, which a 70B on a slow disk can exceed.
100
+
101
+ Anything else in a model table is an error, so a typo like `flash_atn` tells you
102
+ instead of silently doing nothing.
103
+
104
+ ## Commands
105
+
106
+ ```
107
+ mdl run <name> Start <name> in the background, tail its log until the
108
+ server answers /health, and exit. The server keeps running
109
+ after mdl exits.
110
+ mdl stop SIGTERM the running server, SIGKILL after 10s, clean up.
111
+ mdl ps [--json] name, pid, port and uptime, or "nothing running".
112
+ --json prints the state as JSON (null when idle) for
113
+ scripts and status bars.
114
+ mdl list The models defined in the config.
115
+ mdl add <gguf> Append an entry for a .gguf to the config, with sane
116
+ defaults. Takes an optional name and port.
117
+ mdl check Validate every model in the config without launching
118
+ anything. Exits non-zero if it finds a problem.
119
+ mdl init Write a starter config, if you do not have one.
120
+ mdl --version The version, for bug reports.
121
+ mdl ui The dashboard. Bare `mdl` opens it too.
122
+ mdl logs [-f] Print the running server's log; -f follows it.
123
+ Takes a model name to read a stopped one's log.
124
+ ```
125
+
126
+ Without textual installed, `mdl ui` fails with one line and bare `mdl` prints
127
+ the usage string, exactly as it always did.
128
+
129
+ ```console
130
+ $ mdl list
131
+ ornith /srv/models/Ornith-1.5-35B-A3B-Q4_K_M.gguf
132
+ qwen-small /srv/models/Qwen3-8B-Q5_K_M.gguf
133
+
134
+ $ mdl run ornith
135
+ starting ornith (pid 48812), log /home/leon/.local/state/mdl/ornith.log
136
+ load_tensors: offloaded 43/43 layers to GPU
137
+ llama_context: n_ctx = 65536
138
+ main: server is listening on http://127.0.0.1:8080
139
+ ready: ornith on http://127.0.0.1:8080 (pid 48812)
140
+
141
+ $ mdl ps
142
+ ornith pid 48812 port 8080 up 1h04m
143
+
144
+ $ mdl stop
145
+ stopped ornith (pid 48812)
146
+ ```
147
+
148
+ `add` and `check` are the two that save the most time:
149
+
150
+ ```console
151
+ $ mdl add ~/models/Qwen3-8B-Q5_K_M.gguf
152
+ added [qwen3-8b-q5-k-m] to /home/leon/.config/mdl/models.toml
153
+ Qwen3-8B-Q5_K_M.gguf (5.4G, 37 layers)
154
+ run it with: mdl run qwen3-8b-q5-k-m
155
+
156
+ $ mdl check
157
+ ornith ok
158
+ qwen-small model file not found
159
+ mdl: 1 problem(s) found
160
+ ```
161
+
162
+ `add` only appends, and `check` never launches anything, so both are safe
163
+ to run against a config you care about.
164
+
165
+ ## The UI
166
+
167
+ `mdl ui` (or just `mdl`) opens a dashboard over the same config and the same
168
+ state file. Anything you do in it is visible to the CLI and vice versa.
169
+
170
+ Idle, it lists your models with a status dot, shows the selected model's
171
+ parameters, and previews the exact `llama-server` command it would run.
172
+ `e` edits those parameters and saves them back to `models.toml`, leaving
173
+ your comments and layout alone.
174
+ Running, it swaps in live telemetry: VRAM, KV-cache use, a tokens/sec
175
+ sparkline, busy slots, and a colour-coded log tail.
176
+
177
+ ```
178
+ key does
179
+ up/down, j k select a model
180
+ enter, r run the selected model
181
+ s stop the running server
182
+ R restart
183
+ e edit ngl / ctx / kv_type / port, saved to models.toml
184
+ c copy the llama-server command
185
+ p prompt the running model without leaving the UI
186
+ l focus the log, / filters it
187
+ g reload the config
188
+ ? help
189
+ q quit the UI - the server keeps running
190
+ ```
191
+
192
+ Quitting never stops a server; `s` is the only thing that does.
193
+
194
+ The telemetry panels need llama.cpp's metrics endpoint, so add `--metrics`
195
+ to a model's `args` to light them up:
196
+
197
+ ```toml
198
+ args = ["--metrics"]
199
+ ```
200
+
201
+ Without it the dashboard still works, and those panels say `metrics off`
202
+ rather than failing. While a model is loading they say `loading` instead,
203
+ since nothing is listening yet.
204
+
205
+ ### Talking to the model
206
+
207
+ `p` opens a chat with whatever is running, without leaving the UI.
208
+
209
+ ![the chat pane](docs/chat.svg)
210
+
211
+ It keeps the conversation, so follow-up questions have context; `ctrl+l`
212
+ starts a fresh one. Reasoning is shown dimmed and timed
213
+ separately, whether the server hands it back in its own field or inline
214
+ as `<think>` tags. `esc` interrupts a running reply - it closes the
215
+ socket rather than waiting for the next token - and closes the pane once
216
+ nothing is streaming.
217
+
218
+ The rate is the server's own `tok/s` when it reports timings, and ours
219
+ otherwise. `ttft` is time to first token, which is the number that tells
220
+ you whether a long context is hurting.
221
+
222
+ ### Animation
223
+
224
+ The wordmark drifts its gradient by default. Set `ui_fx = "off"` at the
225
+ top level of the config to paint it flat, or pass `mdl ui --no-fx` for a
226
+ one-off.
227
+
228
+ ## Files
229
+
230
+ ```
231
+ ~/.config/mdl/models.toml your config
232
+ ~/.local/state/mdl/state.json name, pid, port and start time of the server
233
+ ~/.local/state/mdl/<name>.log server stdout+stderr, rotated on each run
234
+ ~/.local/state/mdl/<name>.log.1 the previous run, and .2 before that
235
+ ~/.local/state/mdl/ui-marks.json which models the UI has seen start or fail
236
+ ```
237
+
238
+ `$XDG_CONFIG_HOME` and `$XDG_STATE_HOME` are honoured if set. On Windows the
239
+ same layout lives under `%USERPROFILE%`.
240
+
241
+ ## Behaviour notes
242
+
243
+ - **One server at a time.** `run` while something is up is an error telling you
244
+ to `stop` first.
245
+ - **Readiness is an HTTP probe, not log scraping.** `run` polls `/health` on
246
+ the configured port. llama.cpp has reworded its startup line between builds;
247
+ this contract has not.
248
+ - **Obvious mistakes fail before launch.** A missing model file, a missing
249
+ binary or a busy port is one line in milliseconds, not a failed model load.
250
+ - **Stale state self-heals.** If the pid in `state.json` is gone (crash, reboot,
251
+ `kill -9`) the file is removed and `ps` reports nothing running.
252
+ - **If the server exits during startup,** `run` reports its exit status, removes
253
+ the state file, and exits 1. The log has the reason.
254
+ - **If it does not report ready in time,** `run` exits 1 but leaves the server
255
+ running, since it may still be loading. Check the log, or `mdl stop`. Raise
256
+ `ready_timeout` if 300s is genuinely not enough.
257
+ - **Pid reuse is guarded against.** The state file records the OS process
258
+ creation time, so a recycled pid is not mistaken for your server. macOS has
259
+ no cheap way to read that, so it falls back to the pid alone.
260
+ - **`stop` signals the process tree, not just the pid.** If your `llama_server`
261
+ is a wrapper script, killing the wrapper alone would orphan the real server
262
+ and leave the port held.
263
+ - **The last few logs are kept.** `<name>.log` shuffles along to `.1` and `.2`
264
+ on each run, so the crash you were not watching is still there.
265
+ - Errors are one line on stderr and a non-zero exit. No tracebacks.
266
+
267
+ ## Tests
268
+
269
+ ```sh
270
+ python tests/run.py # fast: no real model needed
271
+ python tests/run.py --live # also drives a real model through the UI
272
+ ```
273
+
274
+ The fast suites run against a temp config and a fake `llama-server`, so they
275
+ never touch `~/.config/mdl`. The POSIX process semantics (detaching, orphan
276
+ self-heal, SIGTERM escalating to SIGKILL) need Linux:
277
+
278
+ ```sh
279
+ docker run --rm -v "$PWD:/repo:ro" python:3.12-slim \
280
+ sh -c 'cp -r /repo /w && cd /w && python3 tests/integration_posix.py'
281
+ ```
282
+
283
+ ## Non-goals
284
+
285
+ These are deliberate, and issues asking for them will be closed with a link
286
+ here. `mdl` starts one server, stops it, and tells you what is running.
287
+
288
+ - **No daemon.** Nothing runs in the background except the server itself.
289
+ - **No multiple concurrent servers.** These models are measured in gigabytes
290
+ of VRAM; running two is usually a mistake, and refusing is a feature.
291
+ - **No model downloading.** Use `huggingface-cli`, or your browser.
292
+ - **No hot-swap or auto-unload.** `mdl stop && mdl run other` is two words.
293
+ - **No web UI.** llama-server already ships one.
294
+
295
+ If you want these, [llama-swap](https://github.com/mostlygeek/llama-swap)
296
+ and [Ollama](https://ollama.com) are good and solve different problems.
297
+
298
+ ## Contributing
299
+
300
+ See [CONTRIBUTING.md](CONTRIBUTING.md). Short version: open an issue first,
301
+ keep `mdl.py` free of dependencies, and run the tests.
302
+
303
+ ```sh
304
+ python tests/run.py
305
+ ruff check .
306
+ ```
307
+
308
+ CI runs the suites on Linux, macOS and Windows across Python 3.11-3.13, the
309
+ pinned Textual floor and the current release, Ruff, the POSIX process suite,
310
+ and a packaging check on both the wheel and the sdist.
311
+
312
+ Security issues go through [SECURITY.md](SECURITY.md), privately, rather
313
+ than the public tracker.
314
+
315
+ ## License
316
+
317
+ MIT. See [LICENSE](LICENSE).