mlx-bun 0.0.10 → 0.0.12
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.
- package/README.md +107 -32
- package/THIRD_PARTY_LICENSES.md +10 -0
- package/docs/archive/mlx-bun-lab-report.html +275 -0
- package/docs/archive/wwdc-mlx-bun.html +848 -0
- package/docs/design/audio-input-plan.md +356 -0
- package/docs/design/batching-perf-path.md +58 -35
- package/docs/design/batching-v2-plan.md +23 -9
- package/docs/design/bucketing-stage.md +2 -1
- package/docs/design/colibri-glm52-port.md +1479 -0
- package/docs/design/compat-cli-surface-design.md +4 -3
- package/docs/design/decode-speed-program.md +172 -0
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dspark-serving-program.md +371 -0
- package/docs/design/dspark-speculative-decoding.md +167 -5
- package/docs/design/faithful-l1-consolidation.md +310 -0
- package/docs/design/generic-model-support.md +8 -5
- package/docs/design/grammar-spec-batching-integration.md +301 -0
- package/docs/design/minicpm5-decode-megakernel.md +16 -11
- package/docs/design/mlx-lm-tool-parity-plan.md +11 -1
- package/docs/design/omlx-adoption-map.md +14 -6
- package/docs/design/orpo-training.md +34 -30
- package/docs/design/paged-kv-cache.md +214 -0
- package/docs/design/parallel-slots.md +82 -49
- package/docs/design/parity-tier-dag.md +43 -33
- package/docs/design/pre-colibri-stabilization.md +329 -0
- package/docs/design/repo-cleanup-plan.md +189 -0
- package/docs/design/runtime-isolation.md +19 -3
- package/docs/design/segmented-backward-training.md +3 -2
- package/docs/design/ssd-kv-cold-tier.md +130 -0
- package/docs/design/structured-output.md +484 -0
- package/docs/design/the-dreaming-master-plan.md +2 -3
- package/docs/design/turboquant-kv.md +235 -0
- package/docs/design/unified-engine-frontier-plan.md +774 -0
- package/docs/design/web-chat-beat-matrix.md +315 -0
- package/docs/design/web-chat-redesign.md +1112 -0
- package/docs/investigations/colibri-oracle-pin.md +1284 -0
- package/docs/investigations/dspark-handoff.md +182 -16
- package/docs/investigations/indexshare-performance-spike.md +357 -0
- package/docs/investigations/lab-build-journal.md +2 -2
- package/docs/investigations/openwiki-evaluation.md +249 -0
- package/docs/planning/IDEAS.md +14 -0
- package/docs/planning/PRODUCT_ROADMAP.md +2 -3
- package/docs/planning/chat-ui-vision-memory-first.md +199 -0
- package/docs/planning/chat-ui-vision.md +50 -0
- package/docs/planning/memory-docs-and-dag-plan.md +15 -6
- package/docs/planning/release-notes-v0.0.11.md +239 -0
- package/docs/planning/release-notes-v0.0.12.md +80 -0
- package/docs/reference/cli.md +63 -18
- package/docs/reference/distribution.md +37 -6
- package/docs/reference/embedding.md +40 -16
- package/docs/reference/features-matrix.md +99 -0
- package/docs/reference/library-api.md +69 -20
- package/docs/reference/memory.md +26 -6
- package/docs/reference/models.md +60 -3
- package/docs/reference/orpo-quickstart.md +3 -4
- package/docs/reference/server-api.md +717 -24
- package/docs/reference/server-config.md +265 -129
- package/docs/reference/training.md +24 -16
- package/package.json +5 -3
- package/src/anthropic.ts +67 -44
- package/src/audio/conformer.ts +647 -0
- package/src/audio/decode.ts +133 -0
- package/src/audio/features.ts +202 -0
- package/src/audio/transcode.ts +67 -0
- package/src/chat-template.ts +155 -10
- package/src/cli.ts +483 -113
- package/src/config.ts +116 -24
- package/src/diffusion/diffusion-generate.ts +14 -8
- package/src/download.ts +143 -12
- package/src/eval/README.md +7 -6
- package/src/eval/ifeval.ts +63 -120
- package/src/eval/kl.ts +59 -3
- package/src/eval/quality-db.ts +2 -2
- package/src/eval/runner.ts +17 -8
- package/src/eval/tasks/ifeval.ts +167 -31
- package/src/expert-io.ts +391 -0
- package/src/expert-offload.ts +15 -2
- package/src/expert-residency.ts +1134 -0
- package/src/expert-usage.ts +415 -0
- package/src/flags.ts +13 -0
- package/src/generate.ts +438 -35
- package/src/grammar.ts +585 -0
- package/src/hf-push.ts +1 -1
- package/src/hub-rest.ts +242 -0
- package/src/index.ts +14 -2
- package/src/jobs/runner.ts +37 -18
- package/src/kv-store.ts +586 -93
- package/src/lora.ts +85 -14
- package/src/md-modules.d.ts +42 -0
- package/src/media-fetch.ts +426 -0
- package/src/memory/rest.ts +245 -0
- package/src/memory/vault.ts +74 -0
- package/src/mlx/array.ts +149 -19
- package/src/mlx/autograd.ts +53 -16
- package/src/mlx/checkpoint.ts +46 -13
- package/src/mlx/custom-vjp.ts +83 -35
- package/src/mlx/ffi.ts +27 -3
- package/src/mlx/metal-kernel.ts +7 -4
- package/src/mlx/ops.ts +153 -23
- package/src/mlx/turboquant-ops.ts +831 -0
- package/src/mlx/turboquant-tables.ts +237 -0
- package/src/model/batched-mask.ts +62 -1
- package/src/model/batched-quant.ts +206 -0
- package/src/model/batched-rotating-quant.ts +346 -0
- package/src/model/batched-rotating.ts +46 -1
- package/src/model/compiled-decode.ts +4 -15
- package/src/model/factory.ts +103 -6
- package/src/model/gemma4-base.ts +395 -39
- package/src/model/gemma4.ts +122 -33
- package/src/model/generated/gemma4-12b.ts +9 -13
- package/src/model/generated/gemma4-26b.ts +10 -5
- package/src/model/generated/gemma4-e4b.ts +14 -9
- package/src/model/glm52-atlas.ts +511 -0
- package/src/model/glm52-cache.ts +706 -0
- package/src/model/glm52-config.ts +262 -0
- package/src/model/glm52-container.ts +305 -0
- package/src/model/glm52-coupling.ts +388 -0
- package/src/model/glm52-dsa.ts +602 -0
- package/src/model/glm52-expert-layout.ts +253 -0
- package/src/model/glm52-layout.ts +152 -0
- package/src/model/glm52-memory.ts +473 -0
- package/src/model/glm52-mla.ts +786 -0
- package/src/model/glm52-moe.ts +223 -0
- package/src/model/glm52-pilot.ts +537 -0
- package/src/model/glm52-reference.ts +445 -0
- package/src/model/glm52-residency.ts +557 -0
- package/src/model/glm52-resident-weights.ts +135 -0
- package/src/model/glm52-streamed-experts.ts +453 -0
- package/src/model/glm52-streamed-metal.ts +610 -0
- package/src/model/glm52-weights.ts +503 -0
- package/src/model/glm52.ts +1153 -0
- package/src/model/minicpm5.ts +58 -102
- package/src/model/paged-kv.ts +253 -0
- package/src/model/qwen3-delta.ts +76 -15
- package/src/model/qwen3-moe.ts +404 -0
- package/src/model/qwen3.ts +9 -8
- package/src/model/qwen3_5.ts +88 -23
- package/src/model/support.ts +19 -1
- package/src/model/universal/dense.ts +40 -5
- package/src/model/universal/rope.ts +36 -21
- package/src/native/expert_io.c +523 -0
- package/src/native-pack.ts +6 -4
- package/src/pi-web.ts +1299 -82
- package/src/preflight.ts +27 -8
- package/src/prompt-cache.ts +293 -33
- package/src/quantize/quantizer.ts +16 -1
- package/src/registry.ts +57 -2
- package/src/responses.ts +62 -49
- package/src/serve/batch-scheduler.ts +1050 -78
- package/src/serve/generation-gateway.ts +252 -24
- package/src/serve/isolate.ts +417 -0
- package/src/serve/lane-registry.ts +44 -0
- package/src/serve/session-search.ts +326 -0
- package/src/server.ts +1701 -175
- package/src/spec/assistant-source.ts +150 -0
- package/src/spec/deepspec-source.ts +143 -0
- package/src/spec/dflash-source.ts +124 -0
- package/src/spec/dspark/ab-stats.ts +136 -0
- package/src/spec/dspark/calibration.ts +112 -0
- package/src/spec/dspark/deepspec-module.ts +801 -0
- package/src/spec/dspark/generate-dflash.ts +72 -20
- package/src/spec/dspark/loader.ts +23 -0
- package/src/spec/dspark/module-dflash.ts +278 -36
- package/src/spec/dspark/quantize-drafter.ts +91 -0
- package/src/spec/generate.ts +59 -25
- package/src/spec/glm52-mtp-source.ts +392 -0
- package/src/spec/ngram-source.ts +132 -0
- package/src/spec/serve-loop.ts +617 -0
- package/src/spec/source.ts +118 -0
- package/src/spec/two-model.ts +171 -0
- package/src/ssd-cache.ts +111 -35
- package/src/tokenizer.ts +288 -2
- package/src/tool-approvals.ts +131 -0
- package/src/tool-call.ts +438 -22
- package/src/train/flash-cce.ts +2 -2
- package/src/train/job.ts +0 -12
- package/src/train/lora-params.ts +17 -6
- package/src/train/trainer.ts +60 -30
- package/src/vision/prompt.ts +217 -48
- package/src/web/app.html +1197 -1791
- package/src/web/app.js +5694 -0
- package/src/web/icon.svg +13 -0
- package/src/web/manifest.webmanifest +24 -0
- package/src/web/src/adapters-panel.ts +288 -0
- package/src/web/src/api.ts +57 -0
- package/src/web/src/assistant.ts +485 -0
- package/src/web/src/chat.ts +1050 -0
- package/src/web/src/composer.ts +959 -0
- package/src/web/src/dataset.ts +132 -0
- package/src/web/src/finetune.ts +267 -0
- package/src/web/src/hub.ts +428 -0
- package/src/web/src/main.ts +98 -0
- package/src/web/src/markdown.ts +0 -0
- package/src/web/src/memory-panel.ts +572 -0
- package/src/web/src/model-picker.ts +159 -0
- package/src/web/src/palette.ts +346 -0
- package/src/web/src/protocol.ts +62 -0
- package/src/web/src/quantize.ts +167 -0
- package/src/web/src/rag.ts +274 -0
- package/src/web/src/sessions.ts +469 -0
- package/src/web/src/shell.ts +804 -0
- package/src/web/src/status.ts +300 -0
- package/src/web/src/ui-catalog.ts +92 -0
- package/src/web/sw.js +71 -0
- package/src/web/vendor/README.md +61 -0
- package/src/web/vendor/hljs-LICENSE +29 -0
- package/src/web/vendor/hljs-theme.css +59 -0
- package/src/web/vendor/hljs.js +26 -0
- package/src/web-tools.ts +54 -2
- package/src/weights.ts +82 -19
- package/src/model/fused-decode-kernel.ts +0 -312
- package/src/model/fused-geglu-kernel.ts +0 -198
- package/src/model/fused-mlp-kernel.ts +0 -169
- package/src/model/fused-swiglu-kernel.ts +0 -240
- package/src/model/steel-linear-kernel.ts +0 -96
- package/src/status-page.html +0 -497
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Server API
|
|
2
2
|
|
|
3
|
-
`mlx-bun serve`
|
|
3
|
+
`mlx-bun serve` exposes an OpenAI-compatible
|
|
4
4
|
HTTP API on one model. The request's `model` field is ignored; the
|
|
5
5
|
loaded model's id is echoed back. By default generation is serialized
|
|
6
6
|
through a single queue (one GPU, batch = 1); `--batch N` switches the
|
|
@@ -50,12 +50,14 @@ Request body (OpenAI chat schema; unknown fields ignored):
|
|
|
50
50
|
"tools": [ /* OpenAI function tools */ ],
|
|
51
51
|
"tool_choice": "auto", // "none" disables tools
|
|
52
52
|
"chat_template_kwargs": { // forwarded to the chat template
|
|
53
|
-
"enable_thinking": false // MiniCPM5 / Qwen3.5:
|
|
53
|
+
"enable_thinking": false // MiniCPM5 / Qwen3.5 / GLM-5.2: think channel
|
|
54
54
|
},
|
|
55
55
|
"reasoning_effort": "medium", // "none"|"minimal"|"low"|"medium"|"high"
|
|
56
56
|
// gates enable_thinking on Qwen3.5/MiniCPM5:
|
|
57
|
-
// "none" → off, any other level → on.
|
|
58
|
-
//
|
|
57
|
+
// "none" → off, any other level → on. Only
|
|
58
|
+
// consulted when chat_template_kwargs
|
|
59
|
+
// .enable_thinking is not explicitly set —
|
|
60
|
+
// an explicit enable_thinking always wins.
|
|
59
61
|
"hlg": { // HLG tone-curve sampling (per request).
|
|
60
62
|
"enabled": true, // merged over --hlg-sampling server defaults.
|
|
61
63
|
"width": 4, // logit-width of the tone plateau
|
|
@@ -63,6 +65,11 @@ Request body (OpenAI chat schema; unknown fields ignored):
|
|
|
63
65
|
"toe": 6, // bottom toe width
|
|
64
66
|
"pivot_offset": 6 // pivot point offset from top
|
|
65
67
|
},
|
|
68
|
+
"response_format": // structured output (grammar-constrained
|
|
69
|
+
{ "type": "json_object" }, // decoding) — also guided_grammar,
|
|
70
|
+
// guided_regex, guided_choice,
|
|
71
|
+
// structured_outputs; see "Structured
|
|
72
|
+
// output" below
|
|
66
73
|
"adapter": "id" // LoRA: "id", stacked "a+b", or "none"
|
|
67
74
|
}
|
|
68
75
|
```
|
|
@@ -71,6 +78,11 @@ Sampling defaults follow the model author's `generation_config.json`
|
|
|
71
78
|
when a field is omitted (optiq serve's gen_config behavior); explicit
|
|
72
79
|
request values always win. MiniCPM5 defaults to the no-think direct
|
|
73
80
|
answer mode unless `chat_template_kwargs.enable_thinking` is `true`.
|
|
81
|
+
GLM-5.2 also defaults to no-think: its generation primer is
|
|
82
|
+
`<think></think>`. Enabling thinking renders `Reasoning Effort: Max` in the
|
|
83
|
+
system turn and leaves an open `<think>` primer for generation. The same
|
|
84
|
+
precedence applies through Chat Completions, Anthropic Messages, and Responses
|
|
85
|
+
because all three normalize into this one chat request path.
|
|
74
86
|
|
|
75
87
|
`stop` sequences are matched on **decoded text**, not token ids, so a
|
|
76
88
|
sequence that spans token boundaries still fires. Generation halts at
|
|
@@ -83,6 +95,65 @@ Message `content` is a string or an array of parts:
|
|
|
83
95
|
(http/https URLs also accepted; PNG, JPEG, HEIC, AVIF, WebP, TIFF, GIF,
|
|
84
96
|
BMP via native OS codecs; requires a model with the vision sidecar).
|
|
85
97
|
|
|
98
|
+
Remote (http/https) media URLs — image and audio alike — go through a
|
|
99
|
+
destination policy: **private/loopback/link-local hosts are refused by
|
|
100
|
+
default** (the request URL is attacker-controlled input — SSRF), every
|
|
101
|
+
redirect hop is re-validated, and the fetch has a **10 s timeout** and a
|
|
102
|
+
**64 MB response cap**. Violations are clean `400`s
|
|
103
|
+
(`prompt build failed: image url rejected: …`). Serving media from a LAN
|
|
104
|
+
host (a NAS, another machine) is the opt-in
|
|
105
|
+
[`--allow-private-media`](server-config.md#start-flags) flag. `data:`
|
|
106
|
+
URLs are decoded locally and never policy-checked.
|
|
107
|
+
|
|
108
|
+
### Audio input
|
|
109
|
+
|
|
110
|
+
Audio rides the same content-part array (OpenAI-canonical shape plus two
|
|
111
|
+
aliases):
|
|
112
|
+
|
|
113
|
+
```jsonc
|
|
114
|
+
{ "type": "input_audio", "input_audio": { "data": "<base64>", "format": "wav" } }
|
|
115
|
+
{ "type": "audio", "data": "<base64>" } // alias
|
|
116
|
+
{ "type": "audio_url", "audio_url": { "url": "data:…|http(s)://…" } } // alias
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
(`audio_url` http/https fetches follow the same destination policy,
|
|
120
|
+
timeout, and size cap as `image_url` — see above.)
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
curl -s http://localhost:8080/v1/chat/completions \
|
|
124
|
+
-H 'content-type: application/json' \
|
|
125
|
+
-d "$(jq -n --arg b64 "$(base64 -i clip.wav)" '{
|
|
126
|
+
messages: [{ role: "user", content: [
|
|
127
|
+
{ type: "input_audio", input_audio: { data: $b64, format: "wav" } },
|
|
128
|
+
{ type: "text", text: "Transcribe this audio." }
|
|
129
|
+
]}],
|
|
130
|
+
max_tokens: 64, temperature: 0
|
|
131
|
+
}')"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
- **Models**: any model whose `config.json` carries an `audio_config`
|
|
135
|
+
AND whose `optiq_vision.safetensors` sidecar ships the audio-tower
|
|
136
|
+
tensors — gemma-4 **e4b** today. Auto-detected, no flags (mirrors
|
|
137
|
+
vision). A request WITH audio on any other model gets an explicit
|
|
138
|
+
`400` naming the model — never a silent text-only answer.
|
|
139
|
+
- **Formats**: WAV is decoded natively (PCM 16/24/32-bit + float32, any
|
|
140
|
+
channel count / sample rate). mp3, m4a/AAC, FLAC, ogg(-CAF), AIFF and
|
|
141
|
+
anything else CoreAudio reads transcode via macOS `afconvert`;
|
|
142
|
+
undecodable bytes are a `400` (`audio transcode failed …`).
|
|
143
|
+
- **Internal format**: 16 kHz mono — multi-channel input is mean-mixed,
|
|
144
|
+
other rates resample. Clips truncate at **30 s** (the oracle
|
|
145
|
+
processor's 480 000-sample cap); one clip costs
|
|
146
|
+
`ceil(duration_ms / 40)` prompt tokens, at most **750**.
|
|
147
|
+
- Multiple clips per message and **mixed image+audio** requests work;
|
|
148
|
+
media splices in document order.
|
|
149
|
+
- **Routing**: audio requests always run on the **serial lane** (never
|
|
150
|
+
batched, even under `--batch N`) and skip the prompt cache — soft
|
|
151
|
+
tokens are identical placeholder ids, so prefix matching across
|
|
152
|
+
different clips would false-hit.
|
|
153
|
+
- The Anthropic surface (`/v1/messages`) has no audio block type in its
|
|
154
|
+
protocol; audio blocks there are a `400` pointing back to this
|
|
155
|
+
endpoint.
|
|
156
|
+
|
|
86
157
|
Non-streaming response:
|
|
87
158
|
|
|
88
159
|
```jsonc
|
|
@@ -103,11 +174,41 @@ Non-streaming response:
|
|
|
103
174
|
}],
|
|
104
175
|
"usage": {
|
|
105
176
|
"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0,
|
|
106
|
-
"prompt_tokens_details": { "cached_tokens": 0 }
|
|
177
|
+
"prompt_tokens_details": { "cached_tokens": 0 }, // prompt-cache reuse
|
|
178
|
+
"lane": "serial", // "serial" | "serial+spec" | "batched" —
|
|
179
|
+
// which execution lane served THIS
|
|
180
|
+
// request (GenerationGateway.willBatch
|
|
181
|
+
// + whether a draft actually contributed
|
|
182
|
+
// accepted tokens). Chat completions
|
|
183
|
+
// only (streaming: final usage chunk).
|
|
184
|
+
"speculation": { // only when a draft source is mounted
|
|
185
|
+
"drafted": 0, // draft tokens proposed
|
|
186
|
+
"accepted": 0, // drafts accepted by the verify step
|
|
187
|
+
"targetCalls": 0 // target-model forward calls
|
|
188
|
+
}
|
|
107
189
|
}
|
|
108
190
|
}
|
|
109
191
|
```
|
|
110
192
|
|
|
193
|
+
`usage.lane` is server-driven, never client-inferred (the web chat's perf-strip
|
|
194
|
+
lane badge reads it, not a heuristic): `"batched"` when `--batch N` picked up
|
|
195
|
+
the request, `"serial+spec"` when a mounted draft source actually
|
|
196
|
+
contributed accepted draft tokens to this reply, `"serial"` otherwise. It
|
|
197
|
+
rides the SAME usage block on both streaming (final chunk before `[DONE]`) and
|
|
198
|
+
non-streaming responses.
|
|
199
|
+
|
|
200
|
+
Speculative decoding is a **server-level mode** (`serve --draft-model`,
|
|
201
|
+
or the model-free `serve --draft-kind ngram`, which mounts no draft
|
|
202
|
+
model); there is no per-request draft field. The `speculation` usage
|
|
203
|
+
extension
|
|
204
|
+
appears on chat and text completions alike, non-streaming and on the
|
|
205
|
+
final stream chunk. Spec-eligible requests are text-only on base weights
|
|
206
|
+
(no adapter, no logprobs capture, bf16 KV); ineligible ones decode
|
|
207
|
+
normally and omit the field. The spec path bypasses the prompt cache
|
|
208
|
+
(`cached_tokens` 0), and while a draft is mounted every request routes
|
|
209
|
+
through the serial lane (mlx_lm.server parity: `is_batchable = draft is
|
|
210
|
+
None`) — speculation and `--batch N` are different modes.
|
|
211
|
+
|
|
111
212
|
### logprobs / top_logprobs
|
|
112
213
|
|
|
113
214
|
With `"logprobs": true` and/or `"top_logprobs": k` (1–11), the
|
|
@@ -170,12 +271,62 @@ markup still streams live, only the markup is withheld and converted to
|
|
|
170
271
|
(string-typed params stay strings); markup that fails to parse falls
|
|
171
272
|
back to plain content.
|
|
172
273
|
|
|
274
|
+
### Structured output (grammar-constrained decoding)
|
|
275
|
+
|
|
276
|
+
Both `/v1/chat/completions` and `/v1/completions` accept the
|
|
277
|
+
OpenAI/oMLX/vLLM structured-output fields (snake_case on the wire):
|
|
278
|
+
|
|
279
|
+
```jsonc
|
|
280
|
+
{
|
|
281
|
+
"response_format": // OpenAI: {type:"json_object"} = any valid
|
|
282
|
+
{ "type": "json_schema", // JSON; {type:"json_schema"} constrains
|
|
283
|
+
"json_schema": { // to the schema; {type:"text"} = no-op
|
|
284
|
+
"name": "…", "schema": { /* JSON schema */ }, "strict": true,
|
|
285
|
+
"any_whitespace": true } }, // false = compact separators (no
|
|
286
|
+
// whitespace freedom in the grammar —
|
|
287
|
+
// escape hatch for base models that
|
|
288
|
+
// greedily emit whitespace to max_tokens)
|
|
289
|
+
"guided_grammar": "root ::= …", // raw EBNF grammar string (vLLM/oMLX)
|
|
290
|
+
"guided_regex": "[A-Z][a-z]*", // regex — the regex∩EBNF subset ONLY
|
|
291
|
+
// (classes, |, *, +, ?, parens);
|
|
292
|
+
// regex-only syntax (\d, ., anchors)
|
|
293
|
+
// hits the degrade path — a known gap
|
|
294
|
+
"guided_choice": ["yes", "no"], // output is exactly one of these strings
|
|
295
|
+
"structured_outputs": { /* … */ } // bare JSON schema (oMLX/vLLM alias)
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Precedence when several are set mirrors oMLX: `guided_grammar` >
|
|
300
|
+
`response_format` json_schema > json_object > `structured_outputs` >
|
|
301
|
+
`guided_regex` > `guided_choice`.
|
|
302
|
+
|
|
303
|
+
Enforcement is a per-step token bitmask at the sampler (xgrammar):
|
|
304
|
+
invalid next tokens are masked to −inf **after** the logits processors,
|
|
305
|
+
so valid-token numerics are untouched. Generation halts as soon as the
|
|
306
|
+
grammar is satisfied (e.g. the closing `}` of a complete JSON object)
|
|
307
|
+
with `finish_reason: "stop"`. Works on both lanes — serial and
|
|
308
|
+
`--batch N` (per-row matchers).
|
|
309
|
+
|
|
310
|
+
Degrade path — a grammar that fails to compile is never rejected (no
|
|
311
|
+
400/500): chat prepends a string-valued system instruction for the
|
|
312
|
+
requested JSON schema, raw grammar, regex, or choice constraint, and the
|
|
313
|
+
response carries a `Warning` header (`grammar not enforced: …`);
|
|
314
|
+
`/v1/completions` has no chat template to inject into, so it emits the
|
|
315
|
+
`Warning` header only.
|
|
316
|
+
`MLX_BUN_GRAMMAR=0` disables mask compilation but does not silently
|
|
317
|
+
ignore a requested constraint: it uses this same degrade path (chat
|
|
318
|
+
prompt injection plus `Warning`; raw completions `Warning` only).
|
|
319
|
+
Design and fidelity notes:
|
|
320
|
+
[structured-output.md](../design/structured-output.md).
|
|
321
|
+
|
|
173
322
|
### Errors
|
|
174
323
|
|
|
175
324
|
All errors are `{ "error": { "message": …, ... } }`.
|
|
176
325
|
|
|
177
326
|
- `400` — malformed JSON, empty `messages`, unknown adapter id, vision
|
|
178
|
-
request on a model without a sidecar,
|
|
327
|
+
request on a model without a sidecar, audio request on a model
|
|
328
|
+
without an audio tower (message names the model), audio transcode
|
|
329
|
+
failures, prompt build failures,
|
|
179
330
|
non-numeric `logit_bias` keys/values (`logit_bias must be a dict of
|
|
180
331
|
int to float`, mlx-lm's coercion error), invalid logprobs params
|
|
181
332
|
(mlx-lm's exact validation: `logprobs must be of type bool`,
|
|
@@ -211,7 +362,12 @@ and adapter selection as chat.
|
|
|
211
362
|
// frequency_penalty, frequency_context_size, adapter,
|
|
212
363
|
// logprobs (BOOL, mlx-lm's type — not OpenAI's legacy int),
|
|
213
364
|
// top_logprobs (0-11 or -1) — same response block as chat, see
|
|
214
|
-
// "logprobs / top_logprobs" above (mlx-lm shares the builder)
|
|
365
|
+
// "logprobs / top_logprobs" above (mlx-lm shares the builder),
|
|
366
|
+
// response_format / guided_grammar / guided_regex / guided_choice /
|
|
367
|
+
// structured_outputs — structured output, same semantics and
|
|
368
|
+
// precedence as chat (see "Structured output" above); the degrade
|
|
369
|
+
// path here emits the Warning header only (no template to inject a
|
|
370
|
+
// system prompt into)
|
|
215
371
|
}
|
|
216
372
|
```
|
|
217
373
|
|
|
@@ -224,6 +380,7 @@ Response is the OpenAI text-completion object:
|
|
|
224
380
|
"choices": [{ "index": 0, "text": "…", "finish_reason": "stop" | "length" }],
|
|
225
381
|
"usage": { "prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0,
|
|
226
382
|
"prompt_tokens_details": { "cached_tokens": 0 } }
|
|
383
|
+
// + "speculation" under --draft-model, as on chat
|
|
227
384
|
}
|
|
228
385
|
```
|
|
229
386
|
|
|
@@ -244,7 +401,9 @@ Code works as a client this way.
|
|
|
244
401
|
content-block arrays; `tool_use` / `tool_result` blocks map to the
|
|
245
402
|
native gemma tool-calling path (better than the optiq shim, which
|
|
246
403
|
inlines them as text); `image` blocks (base64 or url source) hit the
|
|
247
|
-
vision path on sidecar models.
|
|
404
|
+
vision path on sidecar models. The protocol has no audio input block;
|
|
405
|
+
audio-shaped blocks (`input_audio` / `audio` / `audio_url`) are
|
|
406
|
+
rejected with a `400` pointing to `/v1/chat/completions`.
|
|
248
407
|
- `tools` (`{name, description, input_schema}`) map to function tools;
|
|
249
408
|
server-tool types (web_search, …) are dropped silently.
|
|
250
409
|
- `max_tokens`, `temperature`, `top_p`, `top_k`, `stop_sequences`,
|
|
@@ -322,13 +481,76 @@ curl localhost:8080/v1/embeddings -H 'content-type: application/json' \
|
|
|
322
481
|
`{ "object": "list", "data": [{ "id": "<model id>", "object": "model",
|
|
323
482
|
"created": <unix s>, … }] }`
|
|
324
483
|
|
|
325
|
-
The served model is FIRST
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
484
|
+
The served model is FIRST, with extra capability fields:
|
|
485
|
+
|
|
486
|
+
```jsonc
|
|
487
|
+
{
|
|
488
|
+
"id": "<served model id>", "object": "model", "created": 0,
|
|
489
|
+
"owned_by": "mlx-bun",
|
|
490
|
+
"context_window": 32768,
|
|
491
|
+
"reasoning": false, // switchable thinking channel (gates the
|
|
492
|
+
// web chat's thinking toggle)
|
|
493
|
+
"vision": false, // image input accepted (tower loaded or
|
|
494
|
+
// lazily loadable)
|
|
495
|
+
"audio": false, // input_audio content parts accepted
|
|
496
|
+
// (tower loaded or lazily loadable)
|
|
497
|
+
"batch_mode": "batch", // loaded cache capability; per-request
|
|
498
|
+
// usage.lane is authoritative
|
|
499
|
+
"tools": true,
|
|
500
|
+
"structured_output": true,
|
|
501
|
+
"embeddings": false,
|
|
502
|
+
"adapters": false,
|
|
503
|
+
"training": false,
|
|
504
|
+
"dsa": true, // GLM-5.2 only; false elsewhere
|
|
505
|
+
"mtp": true, // checkpoint-native GLM MTP is mounted
|
|
506
|
+
"capabilities": { // explicit discoverable surface
|
|
507
|
+
"chat_completions": true, "text_completions": true,
|
|
508
|
+
"anthropic_messages": true, "responses": true,
|
|
509
|
+
"streaming": true, "tools": true, "structured_output": true,
|
|
510
|
+
"logprobs": true, "embeddings": false,
|
|
511
|
+
"vision": false, "audio": false,
|
|
512
|
+
"adapters": false, "training": false
|
|
513
|
+
},
|
|
514
|
+
"gen_defaults": { // model-author sampling defaults
|
|
515
|
+
"temperature": 0.7, // (generation_config.json, with any
|
|
516
|
+
"top_p": 0.95, // --temperature/--top-p/--top-k server
|
|
517
|
+
"top_k": 0 // override applied); null when neither
|
|
518
|
+
// the model nor the server set one
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
Followed by every other servable model the local registry knows (mlx-lm
|
|
524
|
+
scans the HF cache here; the registry is that scan, filtered to supported
|
|
525
|
+
architectures, one row per repo via `listCanonical()` — duplicate snapshots
|
|
526
|
+
from upstream re-pushes are not separate entries):
|
|
527
|
+
|
|
528
|
+
```jsonc
|
|
529
|
+
{ "id": "<repo id>", "object": "model", "created": 0,
|
|
530
|
+
"vision": false, // visionCapable(): sidecar OR encoder-free
|
|
531
|
+
// unified vision config
|
|
532
|
+
"tier": "targeted" | "generic" // supportTier(): dedicated/generated forward
|
|
533
|
+
// ("targeted") vs the Tier-0 universal
|
|
534
|
+
// module ("generic"); entries with no tier
|
|
535
|
+
// (unsupported) are filtered out entirely
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
329
539
|
`GET /v1/models/<id>` filters the list to that id — same list shape,
|
|
330
540
|
matching `mlx_lm.server`.
|
|
331
541
|
|
|
542
|
+
`vision` / `audio` say whether the served model accepts `image_url` /
|
|
543
|
+
`input_audio` content parts (tower loaded or lazily loadable — see
|
|
544
|
+
*Audio input* above for what audio requires), so clients can discover
|
|
545
|
+
capabilities instead of probing for a `400`.
|
|
546
|
+
The served-model row also includes `batch_mode: "off" | "serial" | "batch"`:
|
|
547
|
+
the configured cap and the loaded model's cache capability together. Per-turn
|
|
548
|
+
`usage.lane` remains authoritative for a particular request; for example,
|
|
549
|
+
native-MTP requests route serial even when the served GLM model reports
|
|
550
|
+
`batch_mode: "batch"` for ordinary requests.
|
|
551
|
+
For GLM-5.2, the non-generative fields are deliberately false: embeddings,
|
|
552
|
+
vision/audio, adapters, and training are not emulated by the serving port.
|
|
553
|
+
|
|
332
554
|
## GET /health
|
|
333
555
|
|
|
334
556
|
`{"status": "ok"}` — byte-for-byte what `mlx_lm.server` returns.
|
|
@@ -340,19 +562,59 @@ matching `mlx_lm.server`.
|
|
|
340
562
|
"server": { "owner": "serve" | "pi-session" | "embedded", "model": "...", "started_at": 0 },
|
|
341
563
|
"prompt_cache": { "entries": 0, "bytes": 0, "max_bytes": 0, "hits": 0, "misses": 0 },
|
|
342
564
|
"response_store": { "entries": 0, "bytes": 0, "max_bytes": 33554432, "ttl_ms": 3600000 },
|
|
343
|
-
"kv_quant": { "mode": "mixed (kv_config.json)" | "uniform-kv8" | "bf16",
|
|
344
|
-
"layers": { "kv4": 8, "bf16": 40 },
|
|
565
|
+
"kv_quant": { "mode": "mixed (kv_config.json)" | "uniform-kv8" | "turbo k8v3" | "bf16",
|
|
566
|
+
"layers": { "kv4": 8, "bf16": 40 }, // turbo: { "turbo-k8v3": 8, "bf16": 40 }
|
|
345
567
|
"attention": { "global": 10, "sliding_window": 38 } },
|
|
568
|
+
// present only when --ssd-cache is on:
|
|
569
|
+
"ssd_cache": { "dir": "...", "entries": 0, "bytes": 0, "max_bytes": 0,
|
|
570
|
+
"restores": 0, "spills": 0, "restore_ms_last": 0, "demotions": 0,
|
|
571
|
+
// bounded write-behind queue (MLX_BUN_SSD_SPILL_QUEUE_GB):
|
|
572
|
+
"pending_spills": 0, "pending_spill_bytes": 0, "dropped_spills": 0 },
|
|
346
573
|
"admission": {
|
|
347
574
|
"max_safe_context": 0, // tokens; requests above this 400
|
|
348
575
|
"memory_budget_bytes": null, // explicit budget, or null (machine default)
|
|
349
576
|
"usable_bytes": 0,
|
|
350
577
|
"weights_bytes": 0
|
|
351
578
|
},
|
|
579
|
+
// present only for the direct Colibri GLM-5.2 runtime:
|
|
580
|
+
"glm52": {
|
|
581
|
+
"preset": "g5-32gb-quality",
|
|
582
|
+
"planned_process_bytes": 0,
|
|
583
|
+
"process_limit_bytes": 0,
|
|
584
|
+
"context_tokens": 4096,
|
|
585
|
+
"max_generation_tokens": 128,
|
|
586
|
+
"batch_size": 1,
|
|
587
|
+
"dsa": true,
|
|
588
|
+
"mtp": true,
|
|
589
|
+
"mtp_draft_tokens": 3,
|
|
590
|
+
"resident_weight_bytes": 0,
|
|
591
|
+
"main_expert_slab_bytes": 0,
|
|
592
|
+
"mtp_expert_slab_bytes": 0,
|
|
593
|
+
"expert_runtime": {
|
|
594
|
+
"main_residency": { "resident": 0, "pinned": 0, "working": 0,
|
|
595
|
+
"hits": 0, "misses": 0, "evictions": 0 },
|
|
596
|
+
"mtp_residency": { "resident": 0, "pinned": 0, "working": 0,
|
|
597
|
+
"hits": 0, "misses": 0, "evictions": 0 },
|
|
598
|
+
"last_turn": {
|
|
599
|
+
"main": { "demand": { "hits": 0, "misses": 0,
|
|
600
|
+
"readBytes": 0 } },
|
|
601
|
+
"mtp": { "demand": { "hits": 0, "misses": 0,
|
|
602
|
+
"readBytes": 0 } },
|
|
603
|
+
"pilot": null
|
|
604
|
+
},
|
|
605
|
+
"last_repin": []
|
|
606
|
+
}
|
|
607
|
+
},
|
|
352
608
|
"batch": {
|
|
353
609
|
"configured": 1, // the --batch N value
|
|
354
|
-
"
|
|
355
|
-
|
|
610
|
+
"mode": "off", // "off" | "serial" | "batch": configured
|
|
611
|
+
// and model-cache capability together
|
|
612
|
+
"batched": false, // true only when mode == "batch"
|
|
613
|
+
"active_rows": 0, // rows currently decoding in the batch
|
|
614
|
+
"pending_rows": 0, // queued + mid-prefill rows waiting
|
|
615
|
+
"submitted_rows": 0, // cumulative rows routed to the batch lane since start
|
|
616
|
+
"kv_bytes": 0, // projected aggregate KV bytes of admitted rows
|
|
617
|
+
"kv_budget_bytes": null // --kv-budget cap, or null (uncapped)
|
|
356
618
|
}
|
|
357
619
|
}
|
|
358
620
|
```
|
|
@@ -371,8 +633,13 @@ tensor bytes are read).
|
|
|
371
633
|
"model_type": "gemma3" | "minicpm5" | "qwen3" | …,
|
|
372
634
|
"size_bytes": 0,
|
|
373
635
|
"quant_bits": 4,
|
|
374
|
-
"vision": false, //
|
|
375
|
-
|
|
636
|
+
"vision": false, // visionCapable(): sidecar OR encoder-free
|
|
637
|
+
// unified vision config (not sidecar-only)
|
|
638
|
+
"audio": false, // audio-capable (audio_config + sidecar audio-tower tensors)
|
|
639
|
+
"supported": true, // supportTier() !== null (recognized family)
|
|
640
|
+
"support_tier": "targeted" | "generic" | null, // dedicated/generated
|
|
641
|
+
// forward vs the Tier-0 universal module;
|
|
642
|
+
// null only when supported is false
|
|
376
643
|
"serving": false, // currently loaded in this server
|
|
377
644
|
"assessment": { // null if config unreadable
|
|
378
645
|
"fits": true,
|
|
@@ -383,6 +650,173 @@ tensor bytes are read).
|
|
|
383
650
|
}
|
|
384
651
|
```
|
|
385
652
|
|
|
653
|
+
One row per repo (`listCanonical()`; duplicate snapshots from upstream
|
|
654
|
+
re-pushes surface only via the CLI's `mlx-bun ls --all-revisions`).
|
|
655
|
+
|
|
656
|
+
## GET /api/gc/plan · POST /api/gc/execute
|
|
657
|
+
|
|
658
|
+
Reclaim disk from superseded snapshots + orphaned blobs in the local HF
|
|
659
|
+
cache — the web-UI equivalent of `mlx-bun gc` (thin wrappers over
|
|
660
|
+
`src/registry.ts`'s `planGc`/`executeGc`; same planner the CLI uses). Every
|
|
661
|
+
downloaded revision the HF cache keeps a `snapshots/<commit>` dir for; a
|
|
662
|
+
snapshot no `refs/*` points at is superseded and, once no surviving snapshot
|
|
663
|
+
symlinks to a blob, that blob is dead too. Loopback-served admin routes, no
|
|
664
|
+
separate auth (same bind as every other `/api/*` route).
|
|
665
|
+
|
|
666
|
+
`GET /api/gc/plan` — read-only (config.json + safetensors index reads only,
|
|
667
|
+
no tensor bytes, no deletion):
|
|
668
|
+
|
|
669
|
+
```jsonc
|
|
670
|
+
{
|
|
671
|
+
"ok": true,
|
|
672
|
+
"superseded": [{
|
|
673
|
+
"repo_id": "…",
|
|
674
|
+
"prune_snapshots": 1, // unreferenced, safe to delete
|
|
675
|
+
"skipped_snapshots": 0, // unreferenced but hold files the kept
|
|
676
|
+
// revision lacks — needs --force via the
|
|
677
|
+
// CLI to actually prune; not deletable here
|
|
678
|
+
"dead_blobs": 3,
|
|
679
|
+
"reclaim_bytes": 0
|
|
680
|
+
}],
|
|
681
|
+
"reclaim_bytes": 0 // sum across every repo
|
|
682
|
+
}
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
`POST /api/gc/execute` with body `{"yes": true}` — deletes exactly what a
|
|
686
|
+
fresh plan would show (recomputed at execute time, not the caller's stale
|
|
687
|
+
plan); `{"yes": true}` is required or the request 400s, mirroring the CLI's
|
|
688
|
+
`--yes` gate so a stray call can't delete anything by accident:
|
|
689
|
+
|
|
690
|
+
```jsonc
|
|
691
|
+
{ "ok": true, "snapshots": 1, "blobs": 3, "reclaimed_bytes": 0 }
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
Errors follow the same `{ "ok": false, "error": "…" }` shape as the other
|
|
695
|
+
`/api/*` job routes.
|
|
696
|
+
|
|
697
|
+
## GET /api/memory/\* · POST /api/memory/init
|
|
698
|
+
|
|
699
|
+
Thin loopback JSON wrappers over `src/memory/vault.ts` for the web chat's
|
|
700
|
+
Memory panel (docs/design/web-chat-redesign.md §5.5) — same loopback-only
|
|
701
|
+
admin-route posture as `/api/gc/*`, no separate auth. Handlers live in
|
|
702
|
+
`src/memory/rest.ts`; they never touch the agent-tool surface
|
|
703
|
+
(`src/memory/tools.ts`), which stays read-only and unchanged. Every route
|
|
704
|
+
below degrades gracefully — a missing vault, an unknown article, or a bad
|
|
705
|
+
param returns `{ "ok": false, ... }` (with an appropriate HTTP status), never
|
|
706
|
+
a 500 or an unhandled throw.
|
|
707
|
+
|
|
708
|
+
When no vault exists yet, every `GET` route (except `/init`) returns:
|
|
709
|
+
|
|
710
|
+
```jsonc
|
|
711
|
+
{ "ok": false, "enabled": false, "error": "no memory vault yet", "root": "…" }
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
`GET /api/memory/status` — `vaultStatus()` as JSON:
|
|
715
|
+
|
|
716
|
+
```jsonc
|
|
717
|
+
{ "ok": true, "enabled": true, "status": { "root": "…", "exists": true, "articleCount": 12, "referenceCount": 9, "isGitRepo": true, "recentArticles": [{ "article": "…", "mtimeMs": 0 }] } }
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
`GET /api/memory/list` — article stems plus `Reference/*` doc ids, kept
|
|
721
|
+
separate (mirrors `memory_list`):
|
|
722
|
+
|
|
723
|
+
```jsonc
|
|
724
|
+
{ "ok": true, "articles": ["Alpha", "Beta"], "reference": ["Reference/mlx-bun_README"] }
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
`GET /api/memory/search?q=<query>&scope=all|articles|reference&limit=50` —
|
|
728
|
+
wraps `searchArticles`; same summaries/hits shape the `memory_search` tool
|
|
729
|
+
formats from:
|
|
730
|
+
|
|
731
|
+
```jsonc
|
|
732
|
+
{ "ok": true, "summaries": [{ "article": "…", "occurrences": 3, "matched_terms": ["…"] }], "hits": [{ "article": "…", "anchor": "…", "line": 10, "excerpt": "…" }] }
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
`GET /api/memory/article?name=<article>` — rendered source plus the
|
|
736
|
+
deterministic metadata the panel needs to render it (infobox, lead, series
|
|
737
|
+
banner, section skeleton) without re-parsing client-side; 404s
|
|
738
|
+
`{ "ok": false }` for an unknown name:
|
|
739
|
+
|
|
740
|
+
```jsonc
|
|
741
|
+
{ "ok": true, "name": "…", "path": "…", "content": "# …", "infobox": { "type": "…", "entityKind": "thing", "fields": [] }, "lead": "…", "series": null, "structure": [] }
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
`GET /api/memory/links?name=<article>` — inbound/outbound wikilinks (same
|
|
745
|
+
data `memory_links` resolves); articles only, not `Reference/*` (matching
|
|
746
|
+
the tool's own scope):
|
|
747
|
+
|
|
748
|
+
```jsonc
|
|
749
|
+
{ "ok": true, "name": "…", "outbound": ["…"], "inbound": ["…"] }
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
`GET /api/memory/history?name=<article>&limit=50` — the article's git log
|
|
753
|
+
(read-only `git log` plumbing on the vault's own repo, via `Bun.spawn` —
|
|
754
|
+
never a shell string):
|
|
755
|
+
|
|
756
|
+
```jsonc
|
|
757
|
+
{ "ok": true, "name": "…", "isGitRepo": true, "entries": [{ "hash": "…", "date": "YYYY-MM-DD", "subject": "…" }] }
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
`GET /api/memory/diff?name=<article>&rev=<hash>` — that commit's diff for
|
|
761
|
+
the article (`git show <rev> -- <path>`). `rev` must match
|
|
762
|
+
`/^[0-9a-f]{4,40}$/` (a bare commit hash — never a ref expression like
|
|
763
|
+
`HEAD~1` or `main^`) or the request 400s before touching git; `name` is
|
|
764
|
+
resolved through the same existence-checked normalization `readArticle`
|
|
765
|
+
uses, so a path-traversal or otherwise-invalid name 404s rather than
|
|
766
|
+
reaching the `git` argv:
|
|
767
|
+
|
|
768
|
+
```jsonc
|
|
769
|
+
{ "ok": true, "name": "…", "rev": "…", "diff": "diff --git a/articles/…" }
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
`POST /api/memory/init` with optional body `{"path"?: string}` — the
|
|
773
|
+
first-run consent-card backend. Delegates to the exact same `setupVault()`
|
|
774
|
+
the CLI's `mlx-bun memory init` calls (idempotent: create dirs, write
|
|
775
|
+
README + Meta pages only if missing, git init + initial commit); omits only
|
|
776
|
+
the CLI's interactive extras (seed-from-existing-vault prompt, nightly
|
|
777
|
+
schedule prompt), which stay TTY-only. `path`, if given, must resolve under
|
|
778
|
+
the default vault root or the OS temp directory — `setupVault()` mkdir's
|
|
779
|
+
into it and `git init`s it if it isn't already a repo, so anything outside
|
|
780
|
+
those trees 400s before touching the filesystem rather than letting a
|
|
781
|
+
caller redirect vault setup at an arbitrary directory:
|
|
782
|
+
|
|
783
|
+
```jsonc
|
|
784
|
+
{ "ok": true, "result": { "root": "…", "created": ["…"], "gitInitialized": true, "alreadySetUp": false }, "status": { "root": "…", "exists": true, "articleCount": 0, "referenceCount": 9, "isGitRepo": true, "recentArticles": [] } }
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
## GET /api/settings/tool-approvals · DELETE /api/settings/tool-approvals
|
|
788
|
+
|
|
789
|
+
The web chat's durable "always allow this tool" list for gated tools
|
|
790
|
+
(`bash`/`edit`/`write`) — docs/design/web-chat-redesign.md §5.4/§6.5/§9
|
|
791
|
+
Phase 2, risk #6. Backed by `~/.mlx-bun/tool-approvals.json`
|
|
792
|
+
(`src/tool-approvals.ts`), a versioned `{version, allows}` file (same
|
|
793
|
+
plain-JSON-at-0600 convention as `~/.mlx-bun/hf.json`) keyed by stable tool
|
|
794
|
+
NAME, not per-call arguments — "always allow bash" means every future
|
|
795
|
+
`bash` call skips the browser's approval card, on every chat and every
|
|
796
|
+
browser tab, until forgotten. These two routes are read/forget only:
|
|
797
|
+
granting an always-allow happens through the approval card itself (the
|
|
798
|
+
`approval` WS frame on `/ws/chat`, `alwaysAllow: true`), never a bare
|
|
799
|
+
settings-form POST.
|
|
800
|
+
|
|
801
|
+
`GET /api/settings/tool-approvals`:
|
|
802
|
+
|
|
803
|
+
```jsonc
|
|
804
|
+
{ "ok": true, "alwaysAllow": ["bash", "edit"] }
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
`DELETE /api/settings/tool-approvals` with body `{"tool": "bash"}` —
|
|
808
|
+
revokes one tool's always-allow (idempotent; forgetting an entry that
|
|
809
|
+
isn't present is not an error). Returns the updated set:
|
|
810
|
+
|
|
811
|
+
```jsonc
|
|
812
|
+
{ "ok": true, "alwaysAllow": ["edit"] }
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
Separately, `GET /api/settings/hf-token` / `POST /api/settings/hf-token`
|
|
816
|
+
manage the Hugging Face write token the same settings modal's other
|
|
817
|
+
section uses (see the `upload` CLI command in [cli.md](./cli.md) for the
|
|
818
|
+
push-to-hub flow this token drives).
|
|
819
|
+
|
|
386
820
|
## GET /downloads
|
|
387
821
|
|
|
388
822
|
Snapshot of the last 5 model downloads (active, completed, or errored)
|
|
@@ -406,6 +840,175 @@ initiated via `mlx-bun download` or the web library panel.
|
|
|
406
840
|
}
|
|
407
841
|
```
|
|
408
842
|
|
|
843
|
+
## Model Hub (`/api/hub/*`)
|
|
844
|
+
|
|
845
|
+
The web chat's Model Hub panel (docs/design/web-chat-redesign.md §9 Phase 3,
|
|
846
|
+
beat-matrix Axis 3): browse downloaded models, search Hugging Face, and kick
|
|
847
|
+
off downloads — all loopback-served, no separate auth, same posture as
|
|
848
|
+
`/api/gc/*` and `/api/memory/*`. Handlers live in `src/hub-rest.ts` (pure
|
|
849
|
+
functions, no loaded-model dependency); route dispatch in `src/server.ts`
|
|
850
|
+
just matches path + method.
|
|
851
|
+
|
|
852
|
+
`GET /api/hub/local` — every model in the local registry (same source
|
|
853
|
+
`/library` reads), each with a `/fit`-computed hardware verdict at a fixed
|
|
854
|
+
8k context — the beat matrix's "real hardware verdict per row" that optiq's
|
|
855
|
+
Hub lacks:
|
|
856
|
+
|
|
857
|
+
```jsonc
|
|
858
|
+
{
|
|
859
|
+
"ok": true,
|
|
860
|
+
"models": [{
|
|
861
|
+
"repo_id": "…",
|
|
862
|
+
"model_type": "gemma3" | "minicpm5" | "qwen3" | …,
|
|
863
|
+
"size_bytes": 0,
|
|
864
|
+
"quant_bits": 4,
|
|
865
|
+
"quant_group_size": 64,
|
|
866
|
+
"vision": false,
|
|
867
|
+
"supported": true,
|
|
868
|
+
"support_tier": "targeted" | "generic" | null,
|
|
869
|
+
"assessment": { "fits": true, "max_safe_context": 8192, "predicted_decode_tps": 0.0 } | null
|
|
870
|
+
}]
|
|
871
|
+
}
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
`GET /api/hub/search?q=<query>` — server-side Hugging Face model search,
|
|
875
|
+
filtered to the `mlx` library tag (the tag every mlx-community / MLX-format
|
|
876
|
+
repo carries) so results are MLX-compatible by construction, normalized to
|
|
877
|
+
the fields the Hub panel needs. Never downloads anything — search only.
|
|
878
|
+
Degrades gracefully on any network failure (DNS, timeout, non-2xx) to an
|
|
879
|
+
explicit offline state rather than a 500:
|
|
880
|
+
|
|
881
|
+
```jsonc
|
|
882
|
+
{ "ok": true, "offline": false, "results": [{ "id": "mlx-community/…", "downloads": 0, "likes": 0, "size_estimate": null }] }
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
```jsonc
|
|
886
|
+
{ "ok": true, "offline": true, "error": "…", "results": [] }
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
`size_estimate` is always `null` in v1 — the search endpoint doesn't return
|
|
890
|
+
per-repo file sizes and a second per-repo call would slow the search down;
|
|
891
|
+
the fit badge lands once a model is actually downloaded (`/api/hub/local`).
|
|
892
|
+
|
|
893
|
+
`POST /api/hub/download` with body `{"repo": "org/name"}` — starts a
|
|
894
|
+
background download via the existing `downloadModel()` +
|
|
895
|
+
`src/download.ts` process tracker and returns immediately; progress is
|
|
896
|
+
already visible via [`GET /downloads`](#get-downloads). Refuses a duplicate
|
|
897
|
+
kick-off for a repo that's already downloading:
|
|
898
|
+
|
|
899
|
+
```jsonc
|
|
900
|
+
{ "ok": true, "repo": "org/name", "started": true }
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
```jsonc
|
|
904
|
+
{ "ok": false, "error": "a download for org/name is already in progress" }
|
|
905
|
+
```
|
|
906
|
+
(409)
|
|
907
|
+
|
|
908
|
+
`POST /api/hub/serve` with body `{"model": "org/name"}` — **always answers
|
|
909
|
+
`restart_required`, never performs a live in-process swap.** Investigated
|
|
910
|
+
against `docs/design/runtime-isolation.md`: the web chat's `/ws/chat` path
|
|
911
|
+
is explicitly not proxied even under `--isolate` (`src/serve/isolate.ts`
|
|
912
|
+
answers it 501), and `--isolate` itself is opt-in, not the default — so the
|
|
913
|
+
process a web chat session is actually attached to always has exactly one
|
|
914
|
+
model loaded with no drop-weights-and-reload seam reachable from that path.
|
|
915
|
+
(The `--isolate` proxy's `ModelPool`, P2 of that doc, *is* a real
|
|
916
|
+
spawn-overlap live-swap mechanism — it just lives entirely on the isolated
|
|
917
|
+
`/v1/*` HTTP surface, not on `/ws/chat`.) Faking a swap here would leave the
|
|
918
|
+
server in a half-state; the honest answer is the restart command:
|
|
919
|
+
|
|
920
|
+
```jsonc
|
|
921
|
+
{ "ok": false, "restart_required": true, "command": "mlx-bun serve org/name" }
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
Wiring `/ws/chat` through the isolate proxy (or otherwise reaching
|
|
925
|
+
`ModelPool`'s spawn-overlap swap from the web chat) is the Hub's live-swap
|
|
926
|
+
follow-up, not yet scheduled.
|
|
927
|
+
|
|
928
|
+
## GET /api/sessions/search · GET /api/sessions/export
|
|
929
|
+
|
|
930
|
+
Full-text search across web-chat session message **bodies** and chat
|
|
931
|
+
export (docs/design/web-chat-redesign.md §9 Phase 3, beat-matrix Axis
|
|
932
|
+
10/11's full-text-search BEAT: Claude's own session search is title-only —
|
|
933
|
+
a widely-cited annoyance — and local storage removes any server-cost
|
|
934
|
+
excuse to skip real body search). Same loopback-only, no-separate-auth
|
|
935
|
+
posture as `/api/memory/*`/`/api/hub/*`. Handlers live in
|
|
936
|
+
`src/serve/session-search.ts` (pure, read-only, no loaded-model
|
|
937
|
+
dependency); route dispatch in `src/server.ts` just matches path + method.
|
|
938
|
+
Both routes read the same session directory the web chat's `PiWebSession`
|
|
939
|
+
writes to (`~/.mlx-bun/sessions`, pi's own JSONL-per-session format) and
|
|
940
|
+
are JSONL-tolerant — a corrupt or mid-write line is skipped, never thrown.
|
|
941
|
+
|
|
942
|
+
`GET /api/sessions/search?q=<query>` — case-insensitive substring scan (v1;
|
|
943
|
+
no index — personal-chat-corpus scale, see the module header comment for
|
|
944
|
+
the upgrade path if the corpus ever grows enough to matter) over every
|
|
945
|
+
session file's user/assistant message text, capped at 50 matches total (10
|
|
946
|
+
per session). Each match reports a plain-text ±60-char snippet plus the
|
|
947
|
+
match's own `[start,end)` offsets **into that snippet** — offsets, not
|
|
948
|
+
pre-rendered HTML highlighting, so the frontend escapes the snippet then
|
|
949
|
+
inserts its own `<mark>` (the same escape-then-restore discipline the
|
|
950
|
+
markdown renderer uses for code spans, applied here to search
|
|
951
|
+
highlighting instead):
|
|
952
|
+
|
|
953
|
+
```jsonc
|
|
954
|
+
{
|
|
955
|
+
"ok": true,
|
|
956
|
+
"results": [{
|
|
957
|
+
"sessionPath": "/Users/…/.mlx-bun/sessions/20260706_…jsonl",
|
|
958
|
+
"sessionTitle": "Chicken recipes",
|
|
959
|
+
"matches": [{ "snippet": "…tell me about rosemary and thyme…", "ranges": [[14, 22]], "role": "user" }]
|
|
960
|
+
}]
|
|
961
|
+
}
|
|
962
|
+
```
|
|
963
|
+
|
|
964
|
+
400s `{"ok": false, "error": "q is required"}` without a query.
|
|
965
|
+
|
|
966
|
+
`GET /api/sessions/export?path=<session file>` — the raw session JSONL,
|
|
967
|
+
parsed into an array of entries (one per line, in file order) — the
|
|
968
|
+
frontend's Markdown/JSON export renders from this for a session that isn't
|
|
969
|
+
the currently-open one (the open session's own history is already loaded
|
|
970
|
+
client-side, but this endpoint is used uniformly for both cases so there's
|
|
971
|
+
one code path, not two). `path` must resolve under the session directory —
|
|
972
|
+
exactly the same guard shape as `PiWebSession`'s private
|
|
973
|
+
`isUnderSessionDir` (the root itself, or a path prefixed by `root + "/"`)
|
|
974
|
+
— reimplemented as a small standalone check in `session-search.ts` rather
|
|
975
|
+
than exported from that class:
|
|
976
|
+
|
|
977
|
+
```jsonc
|
|
978
|
+
{ "ok": true, "path": "…", "entries": [{ "type": "session", "id": "…", "cwd": "…" }, { "type": "message", "message": { "role": "user", "content": "…" } }] }
|
|
979
|
+
```
|
|
980
|
+
|
|
981
|
+
400s without `path`; 403s a `path` outside the session directory; 404s a
|
|
982
|
+
path inside the directory that doesn't exist on disk.
|
|
983
|
+
|
|
984
|
+
## Web app static routes + PWA installability
|
|
985
|
+
|
|
986
|
+
The web chat (`GET /`) and everything it loads same-origin, no CDN, ever:
|
|
987
|
+
|
|
988
|
+
| Route | Content | Notes |
|
|
989
|
+
| --- | --- | --- |
|
|
990
|
+
| `GET /` | `src/web/app.html` | The unified SPA shell. |
|
|
991
|
+
| `GET /assets/app.js` | `src/web/app.js` | GENERATED from `src/web/src/*.ts` by `bun scripts/build-web.ts` — see `tests/web-build.test.ts`'s freshness gate. |
|
|
992
|
+
| `GET /assets/hljs.js`, `GET /assets/hljs.css` | `src/web/vendor/hljs*` | Vendored syntax highlighting, no CDN (see that dir's README). |
|
|
993
|
+
| `GET /manifest.webmanifest` | `src/web/manifest.webmanifest` | PWA manifest — name/short_name/theme colors match the app's design tokens. |
|
|
994
|
+
| `GET /assets/icon.svg` | `src/web/icon.svg` | A single inline SVG app icon. No binary PNGs are shipped (the hygiene gate forbids tracked binaries beyond its allowlist); some browsers don't render an SVG as an install icon — accepted, noted in [features-matrix.md](features-matrix.md). |
|
|
995
|
+
| `GET /sw.js` | `src/web/sw.js` | Service worker, served `cache-control: no-store` at the root scope (required for a worker to control `/`). |
|
|
996
|
+
|
|
997
|
+
**PWA scope (beat-matrix Axis 10 "PWA installability"):** the manifest +
|
|
998
|
+
service worker exist for *installability and an instant static-shell
|
|
999
|
+
paint* — "Add to Home Screen" / a browser "Install app" prompt, and the
|
|
1000
|
+
next visit's HTML/JS/CSS loading from cache while the WebSocket connects.
|
|
1001
|
+
This is explicitly **not** offline chat: `src/web/sw.js`'s fetch handler
|
|
1002
|
+
only intercepts the exact shell files listed above (`/`, `/assets/app.js`,
|
|
1003
|
+
`/assets/hljs.js`, `/assets/hljs.css`) and lets every dynamic route
|
|
1004
|
+
(`/api/*`, `/v1/*`, `/ws/chat`, `/downloads`, …) fall straight through to
|
|
1005
|
+
the network unconditionally — caching a chat API response would be
|
|
1006
|
+
actively misleading given the app is a thin client over a local model
|
|
1007
|
+
process that has to actually be running. Registration
|
|
1008
|
+
(`src/web/src/shell.ts`'s `initServiceWorker()`) is guarded to secure
|
|
1009
|
+
contexts: `https:` or `localhost`/`127.0.0.1`, matching the browser's own
|
|
1010
|
+
service-worker eligibility rule.
|
|
1011
|
+
|
|
409
1012
|
## GET /fit
|
|
410
1013
|
|
|
411
1014
|
Fit assessment for the loaded model on this machine, plus a capability
|
|
@@ -415,6 +1018,15 @@ banner and `mlx-bun fit`. When the eval DB has a real measured decode
|
|
|
415
1018
|
rate for this model snapshot, it is included and takes precedence over
|
|
416
1019
|
the prediction.
|
|
417
1020
|
|
|
1021
|
+
For the direct Colibri GLM-5.2 runtime this endpoint switches to the exact
|
|
1022
|
+
serve-time memory plan. The response separates the full streamed artifact on
|
|
1023
|
+
disk from resident weights, main/MTP expert slabs, KV, and other reserves.
|
|
1024
|
+
`measured_decode_tps` is the G5 warm quality-preserving measurement;
|
|
1025
|
+
`glm52.direct_oracle_warm_decode_tps` is the same-machine direct Colibri
|
|
1026
|
+
control, while `glm52.aspirational_decode_tps` is a target, not a release
|
|
1027
|
+
gate. The single-row SKU result describes the current machine because the
|
|
1028
|
+
generic all-resident SKU estimator is inapplicable.
|
|
1029
|
+
|
|
418
1030
|
```jsonc
|
|
419
1031
|
{
|
|
420
1032
|
"machine": { "chip": "M4 Pro", "ram_bytes": 0, "bandwidth_gbs": 0.0 },
|
|
@@ -433,6 +1045,15 @@ the prediction.
|
|
|
433
1045
|
"max_safe_context": 8192,
|
|
434
1046
|
"predicted_decode_tps": 0.0
|
|
435
1047
|
},
|
|
1048
|
+
// GLM-5.2 only:
|
|
1049
|
+
"glm52": {
|
|
1050
|
+
"artifact_disk_bytes": 0, // exact registry size; null if unavailable
|
|
1051
|
+
"main_expert_slab_bytes": 0,
|
|
1052
|
+
"mtp_expert_slab_bytes": 0,
|
|
1053
|
+
"max_generation_tokens": 128,
|
|
1054
|
+
"direct_oracle_warm_decode_tps": 0.27,
|
|
1055
|
+
"aspirational_decode_tps": 2.0
|
|
1056
|
+
},
|
|
436
1057
|
"sku_matrix_ctx": 32768,
|
|
437
1058
|
"sku_matrix": [{
|
|
438
1059
|
"sku": "M4 Pro 24 GB", "ram_gb": 24,
|
|
@@ -443,15 +1064,37 @@ the prediction.
|
|
|
443
1064
|
|
|
444
1065
|
## Adapters (LoRA hot-swap)
|
|
445
1066
|
|
|
446
|
-
- `GET /v1/adapters` — `{ adapters: [{ id, path, rank, scale, size_bytes,
|
|
1067
|
+
- `GET /v1/adapters` — `{ adapters: [{ id, path, rank, scale, size_bytes,
|
|
1068
|
+
mounted_layers, ram_bytes }] }` — currently-mounted adapters only.
|
|
1069
|
+
`ram_bytes` is the actual resident size of the adapter's mounted
|
|
1070
|
+
`lora_a`/`lora_b` arrays (summed `MlxArray.nbytes` — real RAM cost while
|
|
1071
|
+
mounted, not a guess from the on-disk file size); the web chat's adapter
|
|
1072
|
+
routing table (§5.6 of `docs/design/web-chat-redesign.md`) shows this per
|
|
1073
|
+
loaded adapter.
|
|
1074
|
+
- `GET /v1/adapters/available` — `{ adapters: [{ id, path, rank, scale,
|
|
1075
|
+
base_model, mounted, compatible }] }` — every adapter found on disk
|
|
1076
|
+
(`~/.cache/mlx-bun-finetunes`, `~/.cache/mlx-bun/adapters`), unfiltered.
|
|
1077
|
+
`compatible` is true when the adapter's recorded base model matches the
|
|
1078
|
+
currently-served model (compared by bare repo name) or the adapter
|
|
1079
|
+
recorded no base at all; `mounted` is true when it's already loaded.
|
|
1080
|
+
The web chat's adapter chip uses `compatible` to gray out entries it
|
|
1081
|
+
won't let you select rather than hiding them.
|
|
447
1082
|
- `POST /v1/adapters` — `{ "id": "...", "path": "/dir" }`; mounts
|
|
448
|
-
through the generation queue (never races a forward pass).
|
|
449
|
-
|
|
1083
|
+
through the generation queue (never races a forward pass). Response:
|
|
1084
|
+
`{ id, mounted_layers, rank, scale, ram_bytes }`. 400 on shape/compat
|
|
1085
|
+
mismatch — validation is all-or-nothing.
|
|
450
1086
|
- `DELETE /v1/adapters/<id>` — unmount; 404 if not mounted.
|
|
451
1087
|
|
|
452
|
-
Select per request with the `adapter` body field
|
|
453
|
-
|
|
454
|
-
|
|
1088
|
+
Select per request with the `adapter` body field, which also accepts a
|
|
1089
|
+
composed spec — `"a+b"` (or `"a,b"`) stacks two mounted adapters, their
|
|
1090
|
+
LoRA residuals summed in order (`AdapterManager.resolveSpec` /
|
|
1091
|
+
`parseAdapterSpec` in `src/lora.ts`); every named id must already be
|
|
1092
|
+
mounted or the request 400s with the unknown id named. The web chat's
|
|
1093
|
+
adapter routing table exposes this as a "stack" action on top of the
|
|
1094
|
+
composer's single-select quick-switcher. Prompt-cache entries are
|
|
1095
|
+
namespaced per adapter spec (including composed ones), so switching
|
|
1096
|
+
adapters — or switching which ones are stacked — never reuses another
|
|
1097
|
+
combination's KV.
|
|
455
1098
|
|
|
456
1099
|
`serve --adapter <dir>` (alias `--adapter-path`, mlx_lm.server's
|
|
457
1100
|
spelling) mounts an adapter at startup through this same machinery and
|
|
@@ -459,6 +1102,56 @@ makes it the default for requests that send no `adapter` field; an
|
|
|
459
1102
|
explicit `adapter` (including `"none"`) always wins, and hot-swap via
|
|
460
1103
|
these endpoints is unchanged.
|
|
461
1104
|
|
|
1105
|
+
## App-aware assistant (`/ws/chat`)
|
|
1106
|
+
|
|
1107
|
+
The web chat agent can see and act on the app it lives in —
|
|
1108
|
+
`docs/design/web-chat-redesign.md` §6.6, beat matrix Axis 12. No
|
|
1109
|
+
screenshots and no vision model are involved: the browser sends a
|
|
1110
|
+
structured DOM snapshot as a WS frame, and the model gets three tools that
|
|
1111
|
+
read/navigate/highlight that snapshot.
|
|
1112
|
+
|
|
1113
|
+
Three tools join the web chat's tool allowlist unconditionally (same
|
|
1114
|
+
class as `web_search`/`read` — read-only-on-the-machine, never gated by
|
|
1115
|
+
the approval card, regardless of memory or `codingTools` state; see
|
|
1116
|
+
`APP_AWARE_TOOL_NAMES` in `src/pi-web.ts`):
|
|
1117
|
+
|
|
1118
|
+
- **`get_current_app_context`** — returns the last context the browser
|
|
1119
|
+
pushed: `{ route, view?, step?, snapshot }`. `snapshot` is a capped
|
|
1120
|
+
(~120) list of visible interactive elements as `{ref, label, kind,
|
|
1121
|
+
role?, selector, spotlightId?}`, agent chrome excluded.
|
|
1122
|
+
- **`navigate_app({ route | page })`** — validates against the route
|
|
1123
|
+
catalog (`chat`, `quantize`, `finetune`, `dataset`, `status`; unknown
|
|
1124
|
+
routes 400 as a tool error, never silently sent to the browser) and
|
|
1125
|
+
emits a `ui_navigate` frame. Reversible; no approval needed.
|
|
1126
|
+
- **`spotlight_ui({ ref | label | selector | target, route?, message?
|
|
1127
|
+
})`** — emits a `ui_spotlight` frame; the browser resolves the target
|
|
1128
|
+
(ref from its last snapshot, then live selector, then fuzzy label
|
|
1129
|
+
match, then a curated catalog id) and shows a brief highlight. Never
|
|
1130
|
+
blocks: the overlay traps no focus and passes clicks straight through
|
|
1131
|
+
to the highlighted control, auto-dismisses in ~3s, and any keystroke,
|
|
1132
|
+
click, or scroll dismisses it instantly.
|
|
1133
|
+
|
|
1134
|
+
Client → server WS frame (pushed by the browser on every route change and
|
|
1135
|
+
every wizard-step change, never on a timer):
|
|
1136
|
+
|
|
1137
|
+
```jsonc
|
|
1138
|
+
{ "type": "context", "context": { "route": "quantize", "step": { "index": 1, "count": 4, "label": "Configure" }, "snapshot": { "route": "quantize", "capturedAt": "…", "elements": [/* … */] } } }
|
|
1139
|
+
```
|
|
1140
|
+
|
|
1141
|
+
Server → client frames the two action tools produce:
|
|
1142
|
+
|
|
1143
|
+
```jsonc
|
|
1144
|
+
{ "type": "ui_navigate", "route": "quantize" }
|
|
1145
|
+
{ "type": "ui_spotlight", "target": "quantize-source", "message": "Paste a Hugging Face repo id or local path here" }
|
|
1146
|
+
```
|
|
1147
|
+
|
|
1148
|
+
The server also auto-prepends a compact one-line ambient context (e.g.
|
|
1149
|
+
`[user is on: Quantize · step 2/4]`) to the system prompt on every turn —
|
|
1150
|
+
never a full snapshot dump — so the model is never answering completely
|
|
1151
|
+
blind even when it doesn't call a tool. `ui_act` (approval-gated form-fill
|
|
1152
|
+
and job-start actions) is explicitly out of scope for this v1; only
|
|
1153
|
+
navigate/spotlight are wired.
|
|
1154
|
+
|
|
462
1155
|
## Client setup: pi
|
|
463
1156
|
|
|
464
1157
|
The supported one-command path:
|