mlx-bun 0.0.10 → 0.0.11
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 +66 -21
- package/THIRD_PARTY_LICENSES.md +1 -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/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/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/dspark-handoff.md +182 -16
- 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/reference/cli.md +35 -16
- package/docs/reference/distribution.md +13 -6
- package/docs/reference/embedding.md +35 -14
- package/docs/reference/features-matrix.md +97 -0
- package/docs/reference/library-api.md +30 -9
- package/docs/reference/memory.md +26 -6
- package/docs/reference/models.md +11 -1
- package/docs/reference/orpo-quickstart.md +3 -4
- package/docs/reference/server-api.md +634 -22
- package/docs/reference/server-config.md +250 -121
- package/docs/reference/training.md +14 -15
- package/package.json +5 -3
- package/src/anthropic.ts +13 -0
- 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/cli.ts +356 -105
- package/src/config.ts +49 -2
- package/src/download.ts +40 -0
- package/src/eval/README.md +7 -6
- package/src/eval/kl.ts +59 -3
- package/src/eval/quality-db.ts +2 -2
- package/src/eval/runner.ts +3 -3
- package/src/expert-offload.ts +15 -2
- package/src/flags.ts +13 -0
- package/src/generate.ts +330 -24
- package/src/grammar.ts +585 -0
- package/src/hub-rest.ts +242 -0
- package/src/kv-store.ts +442 -88
- package/src/lora.ts +8 -0
- package/src/md-modules.d.ts +42 -0
- package/src/media-fetch.ts +284 -0
- package/src/memory/rest.ts +245 -0
- package/src/memory/vault.ts +74 -0
- package/src/mlx/array.ts +106 -19
- package/src/mlx/autograd.ts +2 -2
- 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 +10 -4
- package/src/model/gemma4-base.ts +363 -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/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 +12 -1
- package/src/model/universal/dense.ts +40 -5
- package/src/model/universal/rope.ts +36 -21
- 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/serve/batch-scheduler.ts +965 -72
- package/src/serve/generation-gateway.ts +214 -11
- 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 +1380 -135
- 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/ngram-source.ts +132 -0
- package/src/spec/serve-loop.ts +536 -0
- package/src/spec/source.ts +93 -0
- package/src/spec/two-model.ts +171 -0
- package/src/ssd-cache.ts +107 -35
- package/src/tokenizer.ts +288 -2
- package/src/tool-approvals.ts +131 -0
- package/src/tool-call.ts +408 -20
- package/src/train/flash-cce.ts +2 -2
- package/src/train/job.ts +0 -2
- package/src/train/trainer.ts +1 -4
- package/src/vision/prompt.ts +217 -48
- package/src/web/app.html +1184 -1789
- package/src/web/app.js +5667 -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 +252 -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/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
|
|
@@ -54,8 +54,10 @@ Request body (OpenAI chat schema; unknown fields ignored):
|
|
|
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
|
```
|
|
@@ -83,6 +90,65 @@ Message `content` is a string or an array of parts:
|
|
|
83
90
|
(http/https URLs also accepted; PNG, JPEG, HEIC, AVIF, WebP, TIFF, GIF,
|
|
84
91
|
BMP via native OS codecs; requires a model with the vision sidecar).
|
|
85
92
|
|
|
93
|
+
Remote (http/https) media URLs — image and audio alike — go through a
|
|
94
|
+
destination policy: **private/loopback/link-local hosts are refused by
|
|
95
|
+
default** (the request URL is attacker-controlled input — SSRF), every
|
|
96
|
+
redirect hop is re-validated, and the fetch has a **10 s timeout** and a
|
|
97
|
+
**64 MB response cap**. Violations are clean `400`s
|
|
98
|
+
(`prompt build failed: image url rejected: …`). Serving media from a LAN
|
|
99
|
+
host (a NAS, another machine) is the opt-in
|
|
100
|
+
[`--allow-private-media`](server-config.md#start-flags) flag. `data:`
|
|
101
|
+
URLs are decoded locally and never policy-checked.
|
|
102
|
+
|
|
103
|
+
### Audio input
|
|
104
|
+
|
|
105
|
+
Audio rides the same content-part array (OpenAI-canonical shape plus two
|
|
106
|
+
aliases):
|
|
107
|
+
|
|
108
|
+
```jsonc
|
|
109
|
+
{ "type": "input_audio", "input_audio": { "data": "<base64>", "format": "wav" } }
|
|
110
|
+
{ "type": "audio", "data": "<base64>" } // alias
|
|
111
|
+
{ "type": "audio_url", "audio_url": { "url": "data:…|http(s)://…" } } // alias
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
(`audio_url` http/https fetches follow the same destination policy,
|
|
115
|
+
timeout, and size cap as `image_url` — see above.)
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
curl -s http://localhost:8080/v1/chat/completions \
|
|
119
|
+
-H 'content-type: application/json' \
|
|
120
|
+
-d "$(jq -n --arg b64 "$(base64 -i clip.wav)" '{
|
|
121
|
+
messages: [{ role: "user", content: [
|
|
122
|
+
{ type: "input_audio", input_audio: { data: $b64, format: "wav" } },
|
|
123
|
+
{ type: "text", text: "Transcribe this audio." }
|
|
124
|
+
]}],
|
|
125
|
+
max_tokens: 64, temperature: 0
|
|
126
|
+
}')"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
- **Models**: any model whose `config.json` carries an `audio_config`
|
|
130
|
+
AND whose `optiq_vision.safetensors` sidecar ships the audio-tower
|
|
131
|
+
tensors — gemma-4 **e4b** today. Auto-detected, no flags (mirrors
|
|
132
|
+
vision). A request WITH audio on any other model gets an explicit
|
|
133
|
+
`400` naming the model — never a silent text-only answer.
|
|
134
|
+
- **Formats**: WAV is decoded natively (PCM 16/24/32-bit + float32, any
|
|
135
|
+
channel count / sample rate). mp3, m4a/AAC, FLAC, ogg(-CAF), AIFF and
|
|
136
|
+
anything else CoreAudio reads transcode via macOS `afconvert`;
|
|
137
|
+
undecodable bytes are a `400` (`audio transcode failed …`).
|
|
138
|
+
- **Internal format**: 16 kHz mono — multi-channel input is mean-mixed,
|
|
139
|
+
other rates resample. Clips truncate at **30 s** (the oracle
|
|
140
|
+
processor's 480 000-sample cap); one clip costs
|
|
141
|
+
`ceil(duration_ms / 40)` prompt tokens, at most **750**.
|
|
142
|
+
- Multiple clips per message and **mixed image+audio** requests work;
|
|
143
|
+
media splices in document order.
|
|
144
|
+
- **Routing**: audio requests always run on the **serial lane** (never
|
|
145
|
+
batched, even under `--batch N`) and skip the prompt cache — soft
|
|
146
|
+
tokens are identical placeholder ids, so prefix matching across
|
|
147
|
+
different clips would false-hit.
|
|
148
|
+
- The Anthropic surface (`/v1/messages`) has no audio block type in its
|
|
149
|
+
protocol; audio blocks there are a `400` pointing back to this
|
|
150
|
+
endpoint.
|
|
151
|
+
|
|
86
152
|
Non-streaming response:
|
|
87
153
|
|
|
88
154
|
```jsonc
|
|
@@ -103,11 +169,41 @@ Non-streaming response:
|
|
|
103
169
|
}],
|
|
104
170
|
"usage": {
|
|
105
171
|
"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0,
|
|
106
|
-
"prompt_tokens_details": { "cached_tokens": 0 }
|
|
172
|
+
"prompt_tokens_details": { "cached_tokens": 0 }, // prompt-cache reuse
|
|
173
|
+
"lane": "serial", // "serial" | "serial+spec" | "batched" —
|
|
174
|
+
// which execution lane served THIS
|
|
175
|
+
// request (GenerationGateway.willBatch
|
|
176
|
+
// + whether a draft actually contributed
|
|
177
|
+
// accepted tokens). Chat completions
|
|
178
|
+
// only (streaming: final usage chunk).
|
|
179
|
+
"speculation": { // only when a draft source is mounted
|
|
180
|
+
"drafted": 0, // draft tokens proposed
|
|
181
|
+
"accepted": 0, // drafts accepted by the verify step
|
|
182
|
+
"targetCalls": 0 // target-model forward calls
|
|
183
|
+
}
|
|
107
184
|
}
|
|
108
185
|
}
|
|
109
186
|
```
|
|
110
187
|
|
|
188
|
+
`usage.lane` is server-driven, never client-inferred (the web chat's perf-strip
|
|
189
|
+
lane badge reads it, not a heuristic): `"batched"` when `--batch N` picked up
|
|
190
|
+
the request, `"serial+spec"` when a mounted draft source actually
|
|
191
|
+
contributed accepted draft tokens to this reply, `"serial"` otherwise. It
|
|
192
|
+
rides the SAME usage block on both streaming (final chunk before `[DONE]`) and
|
|
193
|
+
non-streaming responses.
|
|
194
|
+
|
|
195
|
+
Speculative decoding is a **server-level mode** (`serve --draft-model`,
|
|
196
|
+
or the model-free `serve --draft-kind ngram`, which mounts no draft
|
|
197
|
+
model); there is no per-request draft field. The `speculation` usage
|
|
198
|
+
extension
|
|
199
|
+
appears on chat and text completions alike, non-streaming and on the
|
|
200
|
+
final stream chunk. Spec-eligible requests are text-only on base weights
|
|
201
|
+
(no adapter, no logprobs capture, bf16 KV); ineligible ones decode
|
|
202
|
+
normally and omit the field. The spec path bypasses the prompt cache
|
|
203
|
+
(`cached_tokens` 0), and while a draft is mounted every request routes
|
|
204
|
+
through the serial lane (mlx_lm.server parity: `is_batchable = draft is
|
|
205
|
+
None`) — speculation and `--batch N` are different modes.
|
|
206
|
+
|
|
111
207
|
### logprobs / top_logprobs
|
|
112
208
|
|
|
113
209
|
With `"logprobs": true` and/or `"top_logprobs": k` (1–11), the
|
|
@@ -170,12 +266,59 @@ markup still streams live, only the markup is withheld and converted to
|
|
|
170
266
|
(string-typed params stay strings); markup that fails to parse falls
|
|
171
267
|
back to plain content.
|
|
172
268
|
|
|
269
|
+
### Structured output (grammar-constrained decoding)
|
|
270
|
+
|
|
271
|
+
Both `/v1/chat/completions` and `/v1/completions` accept the
|
|
272
|
+
OpenAI/oMLX/vLLM structured-output fields (snake_case on the wire):
|
|
273
|
+
|
|
274
|
+
```jsonc
|
|
275
|
+
{
|
|
276
|
+
"response_format": // OpenAI: {type:"json_object"} = any valid
|
|
277
|
+
{ "type": "json_schema", // JSON; {type:"json_schema"} constrains
|
|
278
|
+
"json_schema": { // to the schema; {type:"text"} = no-op
|
|
279
|
+
"name": "…", "schema": { /* JSON schema */ }, "strict": true,
|
|
280
|
+
"any_whitespace": true } }, // false = compact separators (no
|
|
281
|
+
// whitespace freedom in the grammar —
|
|
282
|
+
// escape hatch for base models that
|
|
283
|
+
// greedily emit whitespace to max_tokens)
|
|
284
|
+
"guided_grammar": "root ::= …", // raw EBNF grammar string (vLLM/oMLX)
|
|
285
|
+
"guided_regex": "[A-Z][a-z]*", // regex — the regex∩EBNF subset ONLY
|
|
286
|
+
// (classes, |, *, +, ?, parens);
|
|
287
|
+
// regex-only syntax (\d, ., anchors)
|
|
288
|
+
// hits the degrade path — a known gap
|
|
289
|
+
"guided_choice": ["yes", "no"], // output is exactly one of these strings
|
|
290
|
+
"structured_outputs": { /* … */ } // bare JSON schema (oMLX/vLLM alias)
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Precedence when several are set mirrors oMLX: `guided_grammar` >
|
|
295
|
+
`response_format` json_schema > json_object > `structured_outputs` >
|
|
296
|
+
`guided_regex` > `guided_choice`.
|
|
297
|
+
|
|
298
|
+
Enforcement is a per-step token bitmask at the sampler (xgrammar):
|
|
299
|
+
invalid next tokens are masked to −inf **after** the logits processors,
|
|
300
|
+
so valid-token numerics are untouched. Generation halts as soon as the
|
|
301
|
+
grammar is satisfied (e.g. the closing `}` of a complete JSON object)
|
|
302
|
+
with `finish_reason: "stop"`. Works on both lanes — serial and
|
|
303
|
+
`--batch N` (per-row matchers).
|
|
304
|
+
|
|
305
|
+
Degrade path — a grammar that fails to compile is never rejected (no
|
|
306
|
+
400/500): chat prepends a system message instructing valid JSON output
|
|
307
|
+
(schema included when one was given) and the response carries a
|
|
308
|
+
`Warning` header (`grammar not enforced: …`); `/v1/completions` has no
|
|
309
|
+
chat template to inject into, so it emits the `Warning` header only.
|
|
310
|
+
`MLX_BUN_GRAMMAR=0` disables the feature entirely (the fields are
|
|
311
|
+
ignored: no enforcement, no injection, no Warning). Design and fidelity
|
|
312
|
+
notes: [structured-output.md](../design/structured-output.md).
|
|
313
|
+
|
|
173
314
|
### Errors
|
|
174
315
|
|
|
175
316
|
All errors are `{ "error": { "message": …, ... } }`.
|
|
176
317
|
|
|
177
318
|
- `400` — malformed JSON, empty `messages`, unknown adapter id, vision
|
|
178
|
-
request on a model without a sidecar,
|
|
319
|
+
request on a model without a sidecar, audio request on a model
|
|
320
|
+
without an audio tower (message names the model), audio transcode
|
|
321
|
+
failures, prompt build failures,
|
|
179
322
|
non-numeric `logit_bias` keys/values (`logit_bias must be a dict of
|
|
180
323
|
int to float`, mlx-lm's coercion error), invalid logprobs params
|
|
181
324
|
(mlx-lm's exact validation: `logprobs must be of type bool`,
|
|
@@ -211,7 +354,12 @@ and adapter selection as chat.
|
|
|
211
354
|
// frequency_penalty, frequency_context_size, adapter,
|
|
212
355
|
// logprobs (BOOL, mlx-lm's type — not OpenAI's legacy int),
|
|
213
356
|
// top_logprobs (0-11 or -1) — same response block as chat, see
|
|
214
|
-
// "logprobs / top_logprobs" above (mlx-lm shares the builder)
|
|
357
|
+
// "logprobs / top_logprobs" above (mlx-lm shares the builder),
|
|
358
|
+
// response_format / guided_grammar / guided_regex / guided_choice /
|
|
359
|
+
// structured_outputs — structured output, same semantics and
|
|
360
|
+
// precedence as chat (see "Structured output" above); the degrade
|
|
361
|
+
// path here emits the Warning header only (no template to inject a
|
|
362
|
+
// system prompt into)
|
|
215
363
|
}
|
|
216
364
|
```
|
|
217
365
|
|
|
@@ -224,6 +372,7 @@ Response is the OpenAI text-completion object:
|
|
|
224
372
|
"choices": [{ "index": 0, "text": "…", "finish_reason": "stop" | "length" }],
|
|
225
373
|
"usage": { "prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0,
|
|
226
374
|
"prompt_tokens_details": { "cached_tokens": 0 } }
|
|
375
|
+
// + "speculation" under --draft-model, as on chat
|
|
227
376
|
}
|
|
228
377
|
```
|
|
229
378
|
|
|
@@ -244,7 +393,9 @@ Code works as a client this way.
|
|
|
244
393
|
content-block arrays; `tool_use` / `tool_result` blocks map to the
|
|
245
394
|
native gemma tool-calling path (better than the optiq shim, which
|
|
246
395
|
inlines them as text); `image` blocks (base64 or url source) hit the
|
|
247
|
-
vision path on sidecar models.
|
|
396
|
+
vision path on sidecar models. The protocol has no audio input block;
|
|
397
|
+
audio-shaped blocks (`input_audio` / `audio` / `audio_url`) are
|
|
398
|
+
rejected with a `400` pointing to `/v1/chat/completions`.
|
|
248
399
|
- `tools` (`{name, description, input_schema}`) map to function tools;
|
|
249
400
|
server-tool types (web_search, …) are dropped silently.
|
|
250
401
|
- `max_tokens`, `temperature`, `top_p`, `top_k`, `stop_sequences`,
|
|
@@ -322,13 +473,52 @@ curl localhost:8080/v1/embeddings -H 'content-type: application/json' \
|
|
|
322
473
|
`{ "object": "list", "data": [{ "id": "<model id>", "object": "model",
|
|
323
474
|
"created": <unix s>, … }] }`
|
|
324
475
|
|
|
325
|
-
The served model is FIRST
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
476
|
+
The served model is FIRST, with extra capability fields:
|
|
477
|
+
|
|
478
|
+
```jsonc
|
|
479
|
+
{
|
|
480
|
+
"id": "<served model id>", "object": "model", "created": 0,
|
|
481
|
+
"owned_by": "mlx-bun",
|
|
482
|
+
"context_window": 32768,
|
|
483
|
+
"reasoning": false, // switchable thinking channel (gates the
|
|
484
|
+
// web chat's thinking toggle)
|
|
485
|
+
"vision": false, // image input accepted (tower loaded or
|
|
486
|
+
// lazily loadable)
|
|
487
|
+
"audio": false, // input_audio content parts accepted
|
|
488
|
+
// (tower loaded or lazily loadable)
|
|
489
|
+
"gen_defaults": { // model-author sampling defaults
|
|
490
|
+
"temperature": 0.7, // (generation_config.json, with any
|
|
491
|
+
"top_p": 0.95, // --temperature/--top-p/--top-k server
|
|
492
|
+
"top_k": 0 // override applied); null when neither
|
|
493
|
+
// the model nor the server set one
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
Followed by every other servable model the local registry knows (mlx-lm
|
|
499
|
+
scans the HF cache here; the registry is that scan, filtered to supported
|
|
500
|
+
architectures, one row per repo via `listCanonical()` — duplicate snapshots
|
|
501
|
+
from upstream re-pushes are not separate entries):
|
|
502
|
+
|
|
503
|
+
```jsonc
|
|
504
|
+
{ "id": "<repo id>", "object": "model", "created": 0,
|
|
505
|
+
"vision": false, // visionCapable(): sidecar OR encoder-free
|
|
506
|
+
// unified vision config
|
|
507
|
+
"tier": "targeted" | "generic" // supportTier(): dedicated/generated forward
|
|
508
|
+
// ("targeted") vs the Tier-0 universal
|
|
509
|
+
// module ("generic"); entries with no tier
|
|
510
|
+
// (unsupported) are filtered out entirely
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
329
514
|
`GET /v1/models/<id>` filters the list to that id — same list shape,
|
|
330
515
|
matching `mlx_lm.server`.
|
|
331
516
|
|
|
517
|
+
`vision` / `audio` say whether the served model accepts `image_url` /
|
|
518
|
+
`input_audio` content parts (tower loaded or lazily loadable — see
|
|
519
|
+
*Audio input* above for what audio requires), so clients can discover
|
|
520
|
+
capabilities instead of probing for a `400`.
|
|
521
|
+
|
|
332
522
|
## GET /health
|
|
333
523
|
|
|
334
524
|
`{"status": "ok"}` — byte-for-byte what `mlx_lm.server` returns.
|
|
@@ -340,9 +530,14 @@ matching `mlx_lm.server`.
|
|
|
340
530
|
"server": { "owner": "serve" | "pi-session" | "embedded", "model": "...", "started_at": 0 },
|
|
341
531
|
"prompt_cache": { "entries": 0, "bytes": 0, "max_bytes": 0, "hits": 0, "misses": 0 },
|
|
342
532
|
"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 },
|
|
533
|
+
"kv_quant": { "mode": "mixed (kv_config.json)" | "uniform-kv8" | "turbo k8v3" | "bf16",
|
|
534
|
+
"layers": { "kv4": 8, "bf16": 40 }, // turbo: { "turbo-k8v3": 8, "bf16": 40 }
|
|
345
535
|
"attention": { "global": 10, "sliding_window": 38 } },
|
|
536
|
+
// present only when --ssd-cache is on:
|
|
537
|
+
"ssd_cache": { "dir": "...", "entries": 0, "bytes": 0, "max_bytes": 0,
|
|
538
|
+
"restores": 0, "spills": 0, "restore_ms_last": 0, "demotions": 0,
|
|
539
|
+
// bounded write-behind queue (MLX_BUN_SSD_SPILL_QUEUE_GB):
|
|
540
|
+
"pending_spills": 0, "pending_spill_bytes": 0, "dropped_spills": 0 },
|
|
346
541
|
"admission": {
|
|
347
542
|
"max_safe_context": 0, // tokens; requests above this 400
|
|
348
543
|
"memory_budget_bytes": null, // explicit budget, or null (machine default)
|
|
@@ -352,7 +547,11 @@ matching `mlx_lm.server`.
|
|
|
352
547
|
"batch": {
|
|
353
548
|
"configured": 1, // the --batch N value
|
|
354
549
|
"batched": false, // batching enabled (N>1)
|
|
355
|
-
"active_rows": 0
|
|
550
|
+
"active_rows": 0, // rows currently decoding in the batch
|
|
551
|
+
"pending_rows": 0, // queued + mid-prefill rows waiting
|
|
552
|
+
"submitted_rows": 0, // cumulative rows routed to the batch lane since start
|
|
553
|
+
"kv_bytes": 0, // projected aggregate KV bytes of admitted rows
|
|
554
|
+
"kv_budget_bytes": null // --kv-budget cap, or null (uncapped)
|
|
356
555
|
}
|
|
357
556
|
}
|
|
358
557
|
```
|
|
@@ -371,8 +570,13 @@ tensor bytes are read).
|
|
|
371
570
|
"model_type": "gemma3" | "minicpm5" | "qwen3" | …,
|
|
372
571
|
"size_bytes": 0,
|
|
373
572
|
"quant_bits": 4,
|
|
374
|
-
"vision": false, //
|
|
375
|
-
|
|
573
|
+
"vision": false, // visionCapable(): sidecar OR encoder-free
|
|
574
|
+
// unified vision config (not sidecar-only)
|
|
575
|
+
"audio": false, // audio-capable (audio_config + sidecar audio-tower tensors)
|
|
576
|
+
"supported": true, // supportTier() !== null (recognized family)
|
|
577
|
+
"support_tier": "targeted" | "generic" | null, // dedicated/generated
|
|
578
|
+
// forward vs the Tier-0 universal module;
|
|
579
|
+
// null only when supported is false
|
|
376
580
|
"serving": false, // currently loaded in this server
|
|
377
581
|
"assessment": { // null if config unreadable
|
|
378
582
|
"fits": true,
|
|
@@ -383,6 +587,173 @@ tensor bytes are read).
|
|
|
383
587
|
}
|
|
384
588
|
```
|
|
385
589
|
|
|
590
|
+
One row per repo (`listCanonical()`; duplicate snapshots from upstream
|
|
591
|
+
re-pushes surface only via the CLI's `mlx-bun ls --all-revisions`).
|
|
592
|
+
|
|
593
|
+
## GET /api/gc/plan · POST /api/gc/execute
|
|
594
|
+
|
|
595
|
+
Reclaim disk from superseded snapshots + orphaned blobs in the local HF
|
|
596
|
+
cache — the web-UI equivalent of `mlx-bun gc` (thin wrappers over
|
|
597
|
+
`src/registry.ts`'s `planGc`/`executeGc`; same planner the CLI uses). Every
|
|
598
|
+
downloaded revision the HF cache keeps a `snapshots/<commit>` dir for; a
|
|
599
|
+
snapshot no `refs/*` points at is superseded and, once no surviving snapshot
|
|
600
|
+
symlinks to a blob, that blob is dead too. Loopback-served admin routes, no
|
|
601
|
+
separate auth (same bind as every other `/api/*` route).
|
|
602
|
+
|
|
603
|
+
`GET /api/gc/plan` — read-only (config.json + safetensors index reads only,
|
|
604
|
+
no tensor bytes, no deletion):
|
|
605
|
+
|
|
606
|
+
```jsonc
|
|
607
|
+
{
|
|
608
|
+
"ok": true,
|
|
609
|
+
"superseded": [{
|
|
610
|
+
"repo_id": "…",
|
|
611
|
+
"prune_snapshots": 1, // unreferenced, safe to delete
|
|
612
|
+
"skipped_snapshots": 0, // unreferenced but hold files the kept
|
|
613
|
+
// revision lacks — needs --force via the
|
|
614
|
+
// CLI to actually prune; not deletable here
|
|
615
|
+
"dead_blobs": 3,
|
|
616
|
+
"reclaim_bytes": 0
|
|
617
|
+
}],
|
|
618
|
+
"reclaim_bytes": 0 // sum across every repo
|
|
619
|
+
}
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
`POST /api/gc/execute` with body `{"yes": true}` — deletes exactly what a
|
|
623
|
+
fresh plan would show (recomputed at execute time, not the caller's stale
|
|
624
|
+
plan); `{"yes": true}` is required or the request 400s, mirroring the CLI's
|
|
625
|
+
`--yes` gate so a stray call can't delete anything by accident:
|
|
626
|
+
|
|
627
|
+
```jsonc
|
|
628
|
+
{ "ok": true, "snapshots": 1, "blobs": 3, "reclaimed_bytes": 0 }
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
Errors follow the same `{ "ok": false, "error": "…" }` shape as the other
|
|
632
|
+
`/api/*` job routes.
|
|
633
|
+
|
|
634
|
+
## GET /api/memory/\* · POST /api/memory/init
|
|
635
|
+
|
|
636
|
+
Thin loopback JSON wrappers over `src/memory/vault.ts` for the web chat's
|
|
637
|
+
Memory panel (docs/design/web-chat-redesign.md §5.5) — same loopback-only
|
|
638
|
+
admin-route posture as `/api/gc/*`, no separate auth. Handlers live in
|
|
639
|
+
`src/memory/rest.ts`; they never touch the agent-tool surface
|
|
640
|
+
(`src/memory/tools.ts`), which stays read-only and unchanged. Every route
|
|
641
|
+
below degrades gracefully — a missing vault, an unknown article, or a bad
|
|
642
|
+
param returns `{ "ok": false, ... }` (with an appropriate HTTP status), never
|
|
643
|
+
a 500 or an unhandled throw.
|
|
644
|
+
|
|
645
|
+
When no vault exists yet, every `GET` route (except `/init`) returns:
|
|
646
|
+
|
|
647
|
+
```jsonc
|
|
648
|
+
{ "ok": false, "enabled": false, "error": "no memory vault yet", "root": "…" }
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
`GET /api/memory/status` — `vaultStatus()` as JSON:
|
|
652
|
+
|
|
653
|
+
```jsonc
|
|
654
|
+
{ "ok": true, "enabled": true, "status": { "root": "…", "exists": true, "articleCount": 12, "referenceCount": 9, "isGitRepo": true, "recentArticles": [{ "article": "…", "mtimeMs": 0 }] } }
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
`GET /api/memory/list` — article stems plus `Reference/*` doc ids, kept
|
|
658
|
+
separate (mirrors `memory_list`):
|
|
659
|
+
|
|
660
|
+
```jsonc
|
|
661
|
+
{ "ok": true, "articles": ["Alpha", "Beta"], "reference": ["Reference/mlx-bun_README"] }
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
`GET /api/memory/search?q=<query>&scope=all|articles|reference&limit=50` —
|
|
665
|
+
wraps `searchArticles`; same summaries/hits shape the `memory_search` tool
|
|
666
|
+
formats from:
|
|
667
|
+
|
|
668
|
+
```jsonc
|
|
669
|
+
{ "ok": true, "summaries": [{ "article": "…", "occurrences": 3, "matched_terms": ["…"] }], "hits": [{ "article": "…", "anchor": "…", "line": 10, "excerpt": "…" }] }
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
`GET /api/memory/article?name=<article>` — rendered source plus the
|
|
673
|
+
deterministic metadata the panel needs to render it (infobox, lead, series
|
|
674
|
+
banner, section skeleton) without re-parsing client-side; 404s
|
|
675
|
+
`{ "ok": false }` for an unknown name:
|
|
676
|
+
|
|
677
|
+
```jsonc
|
|
678
|
+
{ "ok": true, "name": "…", "path": "…", "content": "# …", "infobox": { "type": "…", "entityKind": "thing", "fields": [] }, "lead": "…", "series": null, "structure": [] }
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
`GET /api/memory/links?name=<article>` — inbound/outbound wikilinks (same
|
|
682
|
+
data `memory_links` resolves); articles only, not `Reference/*` (matching
|
|
683
|
+
the tool's own scope):
|
|
684
|
+
|
|
685
|
+
```jsonc
|
|
686
|
+
{ "ok": true, "name": "…", "outbound": ["…"], "inbound": ["…"] }
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
`GET /api/memory/history?name=<article>&limit=50` — the article's git log
|
|
690
|
+
(read-only `git log` plumbing on the vault's own repo, via `Bun.spawn` —
|
|
691
|
+
never a shell string):
|
|
692
|
+
|
|
693
|
+
```jsonc
|
|
694
|
+
{ "ok": true, "name": "…", "isGitRepo": true, "entries": [{ "hash": "…", "date": "YYYY-MM-DD", "subject": "…" }] }
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
`GET /api/memory/diff?name=<article>&rev=<hash>` — that commit's diff for
|
|
698
|
+
the article (`git show <rev> -- <path>`). `rev` must match
|
|
699
|
+
`/^[0-9a-f]{4,40}$/` (a bare commit hash — never a ref expression like
|
|
700
|
+
`HEAD~1` or `main^`) or the request 400s before touching git; `name` is
|
|
701
|
+
resolved through the same existence-checked normalization `readArticle`
|
|
702
|
+
uses, so a path-traversal or otherwise-invalid name 404s rather than
|
|
703
|
+
reaching the `git` argv:
|
|
704
|
+
|
|
705
|
+
```jsonc
|
|
706
|
+
{ "ok": true, "name": "…", "rev": "…", "diff": "diff --git a/articles/…" }
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
`POST /api/memory/init` with optional body `{"path"?: string}` — the
|
|
710
|
+
first-run consent-card backend. Delegates to the exact same `setupVault()`
|
|
711
|
+
the CLI's `mlx-bun memory init` calls (idempotent: create dirs, write
|
|
712
|
+
README + Meta pages only if missing, git init + initial commit); omits only
|
|
713
|
+
the CLI's interactive extras (seed-from-existing-vault prompt, nightly
|
|
714
|
+
schedule prompt), which stay TTY-only. `path`, if given, must resolve under
|
|
715
|
+
the default vault root or the OS temp directory — `setupVault()` mkdir's
|
|
716
|
+
into it and `git init`s it if it isn't already a repo, so anything outside
|
|
717
|
+
those trees 400s before touching the filesystem rather than letting a
|
|
718
|
+
caller redirect vault setup at an arbitrary directory:
|
|
719
|
+
|
|
720
|
+
```jsonc
|
|
721
|
+
{ "ok": true, "result": { "root": "…", "created": ["…"], "gitInitialized": true, "alreadySetUp": false }, "status": { "root": "…", "exists": true, "articleCount": 0, "referenceCount": 9, "isGitRepo": true, "recentArticles": [] } }
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
## GET /api/settings/tool-approvals · DELETE /api/settings/tool-approvals
|
|
725
|
+
|
|
726
|
+
The web chat's durable "always allow this tool" list for gated tools
|
|
727
|
+
(`bash`/`edit`/`write`) — docs/design/web-chat-redesign.md §5.4/§6.5/§9
|
|
728
|
+
Phase 2, risk #6. Backed by `~/.mlx-bun/tool-approvals.json`
|
|
729
|
+
(`src/tool-approvals.ts`), a versioned `{version, allows}` file (same
|
|
730
|
+
plain-JSON-at-0600 convention as `~/.mlx-bun/hf.json`) keyed by stable tool
|
|
731
|
+
NAME, not per-call arguments — "always allow bash" means every future
|
|
732
|
+
`bash` call skips the browser's approval card, on every chat and every
|
|
733
|
+
browser tab, until forgotten. These two routes are read/forget only:
|
|
734
|
+
granting an always-allow happens through the approval card itself (the
|
|
735
|
+
`approval` WS frame on `/ws/chat`, `alwaysAllow: true`), never a bare
|
|
736
|
+
settings-form POST.
|
|
737
|
+
|
|
738
|
+
`GET /api/settings/tool-approvals`:
|
|
739
|
+
|
|
740
|
+
```jsonc
|
|
741
|
+
{ "ok": true, "alwaysAllow": ["bash", "edit"] }
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
`DELETE /api/settings/tool-approvals` with body `{"tool": "bash"}` —
|
|
745
|
+
revokes one tool's always-allow (idempotent; forgetting an entry that
|
|
746
|
+
isn't present is not an error). Returns the updated set:
|
|
747
|
+
|
|
748
|
+
```jsonc
|
|
749
|
+
{ "ok": true, "alwaysAllow": ["edit"] }
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
Separately, `GET /api/settings/hf-token` / `POST /api/settings/hf-token`
|
|
753
|
+
manage the Hugging Face write token the same settings modal's other
|
|
754
|
+
section uses (see the `upload` CLI command in [cli.md](./cli.md) for the
|
|
755
|
+
push-to-hub flow this token drives).
|
|
756
|
+
|
|
386
757
|
## GET /downloads
|
|
387
758
|
|
|
388
759
|
Snapshot of the last 5 model downloads (active, completed, or errored)
|
|
@@ -406,6 +777,175 @@ initiated via `mlx-bun download` or the web library panel.
|
|
|
406
777
|
}
|
|
407
778
|
```
|
|
408
779
|
|
|
780
|
+
## Model Hub (`/api/hub/*`)
|
|
781
|
+
|
|
782
|
+
The web chat's Model Hub panel (docs/design/web-chat-redesign.md §9 Phase 3,
|
|
783
|
+
beat-matrix Axis 3): browse downloaded models, search Hugging Face, and kick
|
|
784
|
+
off downloads — all loopback-served, no separate auth, same posture as
|
|
785
|
+
`/api/gc/*` and `/api/memory/*`. Handlers live in `src/hub-rest.ts` (pure
|
|
786
|
+
functions, no loaded-model dependency); route dispatch in `src/server.ts`
|
|
787
|
+
just matches path + method.
|
|
788
|
+
|
|
789
|
+
`GET /api/hub/local` — every model in the local registry (same source
|
|
790
|
+
`/library` reads), each with a `/fit`-computed hardware verdict at a fixed
|
|
791
|
+
8k context — the beat matrix's "real hardware verdict per row" that optiq's
|
|
792
|
+
Hub lacks:
|
|
793
|
+
|
|
794
|
+
```jsonc
|
|
795
|
+
{
|
|
796
|
+
"ok": true,
|
|
797
|
+
"models": [{
|
|
798
|
+
"repo_id": "…",
|
|
799
|
+
"model_type": "gemma3" | "minicpm5" | "qwen3" | …,
|
|
800
|
+
"size_bytes": 0,
|
|
801
|
+
"quant_bits": 4,
|
|
802
|
+
"quant_group_size": 64,
|
|
803
|
+
"vision": false,
|
|
804
|
+
"supported": true,
|
|
805
|
+
"support_tier": "targeted" | "generic" | null,
|
|
806
|
+
"assessment": { "fits": true, "max_safe_context": 8192, "predicted_decode_tps": 0.0 } | null
|
|
807
|
+
}]
|
|
808
|
+
}
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
`GET /api/hub/search?q=<query>` — server-side Hugging Face model search,
|
|
812
|
+
filtered to the `mlx` library tag (the tag every mlx-community / MLX-format
|
|
813
|
+
repo carries) so results are MLX-compatible by construction, normalized to
|
|
814
|
+
the fields the Hub panel needs. Never downloads anything — search only.
|
|
815
|
+
Degrades gracefully on any network failure (DNS, timeout, non-2xx) to an
|
|
816
|
+
explicit offline state rather than a 500:
|
|
817
|
+
|
|
818
|
+
```jsonc
|
|
819
|
+
{ "ok": true, "offline": false, "results": [{ "id": "mlx-community/…", "downloads": 0, "likes": 0, "size_estimate": null }] }
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
```jsonc
|
|
823
|
+
{ "ok": true, "offline": true, "error": "…", "results": [] }
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
`size_estimate` is always `null` in v1 — the search endpoint doesn't return
|
|
827
|
+
per-repo file sizes and a second per-repo call would slow the search down;
|
|
828
|
+
the fit badge lands once a model is actually downloaded (`/api/hub/local`).
|
|
829
|
+
|
|
830
|
+
`POST /api/hub/download` with body `{"repo": "org/name"}` — starts a
|
|
831
|
+
background download via the existing `downloadModel()` +
|
|
832
|
+
`src/download.ts` process tracker and returns immediately; progress is
|
|
833
|
+
already visible via [`GET /downloads`](#get-downloads). Refuses a duplicate
|
|
834
|
+
kick-off for a repo that's already downloading:
|
|
835
|
+
|
|
836
|
+
```jsonc
|
|
837
|
+
{ "ok": true, "repo": "org/name", "started": true }
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
```jsonc
|
|
841
|
+
{ "ok": false, "error": "a download for org/name is already in progress" }
|
|
842
|
+
```
|
|
843
|
+
(409)
|
|
844
|
+
|
|
845
|
+
`POST /api/hub/serve` with body `{"model": "org/name"}` — **always answers
|
|
846
|
+
`restart_required`, never performs a live in-process swap.** Investigated
|
|
847
|
+
against `docs/design/runtime-isolation.md`: the web chat's `/ws/chat` path
|
|
848
|
+
is explicitly not proxied even under `--isolate` (`src/serve/isolate.ts`
|
|
849
|
+
answers it 501), and `--isolate` itself is opt-in, not the default — so the
|
|
850
|
+
process a web chat session is actually attached to always has exactly one
|
|
851
|
+
model loaded with no drop-weights-and-reload seam reachable from that path.
|
|
852
|
+
(The `--isolate` proxy's `ModelPool`, P2 of that doc, *is* a real
|
|
853
|
+
spawn-overlap live-swap mechanism — it just lives entirely on the isolated
|
|
854
|
+
`/v1/*` HTTP surface, not on `/ws/chat`.) Faking a swap here would leave the
|
|
855
|
+
server in a half-state; the honest answer is the restart command:
|
|
856
|
+
|
|
857
|
+
```jsonc
|
|
858
|
+
{ "ok": false, "restart_required": true, "command": "mlx-bun serve org/name" }
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
Wiring `/ws/chat` through the isolate proxy (or otherwise reaching
|
|
862
|
+
`ModelPool`'s spawn-overlap swap from the web chat) is the Hub's live-swap
|
|
863
|
+
follow-up, not yet scheduled.
|
|
864
|
+
|
|
865
|
+
## GET /api/sessions/search · GET /api/sessions/export
|
|
866
|
+
|
|
867
|
+
Full-text search across web-chat session message **bodies** and chat
|
|
868
|
+
export (docs/design/web-chat-redesign.md §9 Phase 3, beat-matrix Axis
|
|
869
|
+
10/11's full-text-search BEAT: Claude's own session search is title-only —
|
|
870
|
+
a widely-cited annoyance — and local storage removes any server-cost
|
|
871
|
+
excuse to skip real body search). Same loopback-only, no-separate-auth
|
|
872
|
+
posture as `/api/memory/*`/`/api/hub/*`. Handlers live in
|
|
873
|
+
`src/serve/session-search.ts` (pure, read-only, no loaded-model
|
|
874
|
+
dependency); route dispatch in `src/server.ts` just matches path + method.
|
|
875
|
+
Both routes read the same session directory the web chat's `PiWebSession`
|
|
876
|
+
writes to (`~/.mlx-bun/sessions`, pi's own JSONL-per-session format) and
|
|
877
|
+
are JSONL-tolerant — a corrupt or mid-write line is skipped, never thrown.
|
|
878
|
+
|
|
879
|
+
`GET /api/sessions/search?q=<query>` — case-insensitive substring scan (v1;
|
|
880
|
+
no index — personal-chat-corpus scale, see the module header comment for
|
|
881
|
+
the upgrade path if the corpus ever grows enough to matter) over every
|
|
882
|
+
session file's user/assistant message text, capped at 50 matches total (10
|
|
883
|
+
per session). Each match reports a plain-text ±60-char snippet plus the
|
|
884
|
+
match's own `[start,end)` offsets **into that snippet** — offsets, not
|
|
885
|
+
pre-rendered HTML highlighting, so the frontend escapes the snippet then
|
|
886
|
+
inserts its own `<mark>` (the same escape-then-restore discipline the
|
|
887
|
+
markdown renderer uses for code spans, applied here to search
|
|
888
|
+
highlighting instead):
|
|
889
|
+
|
|
890
|
+
```jsonc
|
|
891
|
+
{
|
|
892
|
+
"ok": true,
|
|
893
|
+
"results": [{
|
|
894
|
+
"sessionPath": "/Users/…/.mlx-bun/sessions/20260706_…jsonl",
|
|
895
|
+
"sessionTitle": "Chicken recipes",
|
|
896
|
+
"matches": [{ "snippet": "…tell me about rosemary and thyme…", "ranges": [[14, 22]], "role": "user" }]
|
|
897
|
+
}]
|
|
898
|
+
}
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
400s `{"ok": false, "error": "q is required"}` without a query.
|
|
902
|
+
|
|
903
|
+
`GET /api/sessions/export?path=<session file>` — the raw session JSONL,
|
|
904
|
+
parsed into an array of entries (one per line, in file order) — the
|
|
905
|
+
frontend's Markdown/JSON export renders from this for a session that isn't
|
|
906
|
+
the currently-open one (the open session's own history is already loaded
|
|
907
|
+
client-side, but this endpoint is used uniformly for both cases so there's
|
|
908
|
+
one code path, not two). `path` must resolve under the session directory —
|
|
909
|
+
exactly the same guard shape as `PiWebSession`'s private
|
|
910
|
+
`isUnderSessionDir` (the root itself, or a path prefixed by `root + "/"`)
|
|
911
|
+
— reimplemented as a small standalone check in `session-search.ts` rather
|
|
912
|
+
than exported from that class:
|
|
913
|
+
|
|
914
|
+
```jsonc
|
|
915
|
+
{ "ok": true, "path": "…", "entries": [{ "type": "session", "id": "…", "cwd": "…" }, { "type": "message", "message": { "role": "user", "content": "…" } }] }
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
400s without `path`; 403s a `path` outside the session directory; 404s a
|
|
919
|
+
path inside the directory that doesn't exist on disk.
|
|
920
|
+
|
|
921
|
+
## Web app static routes + PWA installability
|
|
922
|
+
|
|
923
|
+
The web chat (`GET /`) and everything it loads same-origin, no CDN, ever:
|
|
924
|
+
|
|
925
|
+
| Route | Content | Notes |
|
|
926
|
+
| --- | --- | --- |
|
|
927
|
+
| `GET /` | `src/web/app.html` | The unified SPA shell. |
|
|
928
|
+
| `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. |
|
|
929
|
+
| `GET /assets/hljs.js`, `GET /assets/hljs.css` | `src/web/vendor/hljs*` | Vendored syntax highlighting, no CDN (see that dir's README). |
|
|
930
|
+
| `GET /manifest.webmanifest` | `src/web/manifest.webmanifest` | PWA manifest — name/short_name/theme colors match the app's design tokens. |
|
|
931
|
+
| `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). |
|
|
932
|
+
| `GET /sw.js` | `src/web/sw.js` | Service worker, served `cache-control: no-store` at the root scope (required for a worker to control `/`). |
|
|
933
|
+
|
|
934
|
+
**PWA scope (beat-matrix Axis 10 "PWA installability"):** the manifest +
|
|
935
|
+
service worker exist for *installability and an instant static-shell
|
|
936
|
+
paint* — "Add to Home Screen" / a browser "Install app" prompt, and the
|
|
937
|
+
next visit's HTML/JS/CSS loading from cache while the WebSocket connects.
|
|
938
|
+
This is explicitly **not** offline chat: `src/web/sw.js`'s fetch handler
|
|
939
|
+
only intercepts the exact shell files listed above (`/`, `/assets/app.js`,
|
|
940
|
+
`/assets/hljs.js`, `/assets/hljs.css`) and lets every dynamic route
|
|
941
|
+
(`/api/*`, `/v1/*`, `/ws/chat`, `/downloads`, …) fall straight through to
|
|
942
|
+
the network unconditionally — caching a chat API response would be
|
|
943
|
+
actively misleading given the app is a thin client over a local model
|
|
944
|
+
process that has to actually be running. Registration
|
|
945
|
+
(`src/web/src/shell.ts`'s `initServiceWorker()`) is guarded to secure
|
|
946
|
+
contexts: `https:` or `localhost`/`127.0.0.1`, matching the browser's own
|
|
947
|
+
service-worker eligibility rule.
|
|
948
|
+
|
|
409
949
|
## GET /fit
|
|
410
950
|
|
|
411
951
|
Fit assessment for the loaded model on this machine, plus a capability
|
|
@@ -443,15 +983,37 @@ the prediction.
|
|
|
443
983
|
|
|
444
984
|
## Adapters (LoRA hot-swap)
|
|
445
985
|
|
|
446
|
-
- `GET /v1/adapters` — `{ adapters: [{ id, path, rank, scale, size_bytes,
|
|
986
|
+
- `GET /v1/adapters` — `{ adapters: [{ id, path, rank, scale, size_bytes,
|
|
987
|
+
mounted_layers, ram_bytes }] }` — currently-mounted adapters only.
|
|
988
|
+
`ram_bytes` is the actual resident size of the adapter's mounted
|
|
989
|
+
`lora_a`/`lora_b` arrays (summed `MlxArray.nbytes` — real RAM cost while
|
|
990
|
+
mounted, not a guess from the on-disk file size); the web chat's adapter
|
|
991
|
+
routing table (§5.6 of `docs/design/web-chat-redesign.md`) shows this per
|
|
992
|
+
loaded adapter.
|
|
993
|
+
- `GET /v1/adapters/available` — `{ adapters: [{ id, path, rank, scale,
|
|
994
|
+
base_model, mounted, compatible }] }` — every adapter found on disk
|
|
995
|
+
(`~/.cache/mlx-bun-finetunes`, `~/.cache/mlx-bun/adapters`), unfiltered.
|
|
996
|
+
`compatible` is true when the adapter's recorded base model matches the
|
|
997
|
+
currently-served model (compared by bare repo name) or the adapter
|
|
998
|
+
recorded no base at all; `mounted` is true when it's already loaded.
|
|
999
|
+
The web chat's adapter chip uses `compatible` to gray out entries it
|
|
1000
|
+
won't let you select rather than hiding them.
|
|
447
1001
|
- `POST /v1/adapters` — `{ "id": "...", "path": "/dir" }`; mounts
|
|
448
|
-
through the generation queue (never races a forward pass).
|
|
449
|
-
|
|
1002
|
+
through the generation queue (never races a forward pass). Response:
|
|
1003
|
+
`{ id, mounted_layers, rank, scale, ram_bytes }`. 400 on shape/compat
|
|
1004
|
+
mismatch — validation is all-or-nothing.
|
|
450
1005
|
- `DELETE /v1/adapters/<id>` — unmount; 404 if not mounted.
|
|
451
1006
|
|
|
452
|
-
Select per request with the `adapter` body field
|
|
453
|
-
|
|
454
|
-
|
|
1007
|
+
Select per request with the `adapter` body field, which also accepts a
|
|
1008
|
+
composed spec — `"a+b"` (or `"a,b"`) stacks two mounted adapters, their
|
|
1009
|
+
LoRA residuals summed in order (`AdapterManager.resolveSpec` /
|
|
1010
|
+
`parseAdapterSpec` in `src/lora.ts`); every named id must already be
|
|
1011
|
+
mounted or the request 400s with the unknown id named. The web chat's
|
|
1012
|
+
adapter routing table exposes this as a "stack" action on top of the
|
|
1013
|
+
composer's single-select quick-switcher. Prompt-cache entries are
|
|
1014
|
+
namespaced per adapter spec (including composed ones), so switching
|
|
1015
|
+
adapters — or switching which ones are stacked — never reuses another
|
|
1016
|
+
combination's KV.
|
|
455
1017
|
|
|
456
1018
|
`serve --adapter <dir>` (alias `--adapter-path`, mlx_lm.server's
|
|
457
1019
|
spelling) mounts an adapter at startup through this same machinery and
|
|
@@ -459,6 +1021,56 @@ makes it the default for requests that send no `adapter` field; an
|
|
|
459
1021
|
explicit `adapter` (including `"none"`) always wins, and hot-swap via
|
|
460
1022
|
these endpoints is unchanged.
|
|
461
1023
|
|
|
1024
|
+
## App-aware assistant (`/ws/chat`)
|
|
1025
|
+
|
|
1026
|
+
The web chat agent can see and act on the app it lives in —
|
|
1027
|
+
`docs/design/web-chat-redesign.md` §6.6, beat matrix Axis 12. No
|
|
1028
|
+
screenshots and no vision model are involved: the browser sends a
|
|
1029
|
+
structured DOM snapshot as a WS frame, and the model gets three tools that
|
|
1030
|
+
read/navigate/highlight that snapshot.
|
|
1031
|
+
|
|
1032
|
+
Three tools join the web chat's tool allowlist unconditionally (same
|
|
1033
|
+
class as `web_search`/`read` — read-only-on-the-machine, never gated by
|
|
1034
|
+
the approval card, regardless of memory or `codingTools` state; see
|
|
1035
|
+
`APP_AWARE_TOOL_NAMES` in `src/pi-web.ts`):
|
|
1036
|
+
|
|
1037
|
+
- **`get_current_app_context`** — returns the last context the browser
|
|
1038
|
+
pushed: `{ route, view?, step?, snapshot }`. `snapshot` is a capped
|
|
1039
|
+
(~120) list of visible interactive elements as `{ref, label, kind,
|
|
1040
|
+
role?, selector, spotlightId?}`, agent chrome excluded.
|
|
1041
|
+
- **`navigate_app({ route | page })`** — validates against the route
|
|
1042
|
+
catalog (`chat`, `quantize`, `finetune`, `dataset`, `status`; unknown
|
|
1043
|
+
routes 400 as a tool error, never silently sent to the browser) and
|
|
1044
|
+
emits a `ui_navigate` frame. Reversible; no approval needed.
|
|
1045
|
+
- **`spotlight_ui({ ref | label | selector | target, route?, message?
|
|
1046
|
+
})`** — emits a `ui_spotlight` frame; the browser resolves the target
|
|
1047
|
+
(ref from its last snapshot, then live selector, then fuzzy label
|
|
1048
|
+
match, then a curated catalog id) and shows a brief highlight. Never
|
|
1049
|
+
blocks: the overlay traps no focus and passes clicks straight through
|
|
1050
|
+
to the highlighted control, auto-dismisses in ~3s, and any keystroke,
|
|
1051
|
+
click, or scroll dismisses it instantly.
|
|
1052
|
+
|
|
1053
|
+
Client → server WS frame (pushed by the browser on every route change and
|
|
1054
|
+
every wizard-step change, never on a timer):
|
|
1055
|
+
|
|
1056
|
+
```jsonc
|
|
1057
|
+
{ "type": "context", "context": { "route": "quantize", "step": { "index": 1, "count": 4, "label": "Configure" }, "snapshot": { "route": "quantize", "capturedAt": "…", "elements": [/* … */] } } }
|
|
1058
|
+
```
|
|
1059
|
+
|
|
1060
|
+
Server → client frames the two action tools produce:
|
|
1061
|
+
|
|
1062
|
+
```jsonc
|
|
1063
|
+
{ "type": "ui_navigate", "route": "quantize" }
|
|
1064
|
+
{ "type": "ui_spotlight", "target": "quantize-source", "message": "Paste a Hugging Face repo id or local path here" }
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
The server also auto-prepends a compact one-line ambient context (e.g.
|
|
1068
|
+
`[user is on: Quantize · step 2/4]`) to the system prompt on every turn —
|
|
1069
|
+
never a full snapshot dump — so the model is never answering completely
|
|
1070
|
+
blind even when it doesn't call a tool. `ui_act` (approval-gated form-fill
|
|
1071
|
+
and job-start actions) is explicitly out of scope for this v1; only
|
|
1072
|
+
navigate/spotlight are wired.
|
|
1073
|
+
|
|
462
1074
|
## Client setup: pi
|
|
463
1075
|
|
|
464
1076
|
The supported one-command path:
|