mlx-bun 0.2.0 → 0.3.0
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 +71 -588
- package/docs/README.md +91 -0
- package/docs/{design → archive/investigations}/audio-input-plan.md +4 -4
- package/docs/{design → archive/investigations}/diffusion-gemma-port.md +3 -3
- package/docs/{design → archive/investigations}/faithful-l1-consolidation.md +4 -4
- package/docs/{design → archive/investigations}/minicpm5-decode-megakernel.md +11 -11
- package/docs/{design → archive/investigations}/optimization_plan.md +1 -1
- package/docs/{design → archive/investigations}/pre-colibri-stabilization.md +5 -5
- package/docs/{design → archive/investigations}/the-dreaming-handoff.md +2 -2
- package/docs/design/batching.md +569 -0
- package/docs/design/decode-speed-program.md +3178 -169
- package/docs/design/dreaming-nightly-pipeline.md +675 -282
- package/docs/design/generic-model-support.md +579 -334
- package/docs/design/kv-cache.md +680 -0
- package/docs/design/orpo-training.md +673 -1239
- package/docs/design/speculative-decoding.md +1627 -0
- package/docs/design/turboquant.md +2102 -0
- package/docs/design/unified-engine-frontier-plan.md +1831 -727
- package/docs/design/web-chat-redesign.md +666 -1083
- package/docs/planning/PRODUCT_ROADMAP.md +34 -1
- package/docs/planning/release-notes-v0.3.0.md +35 -0
- package/docs/planning/video-series.md +2 -2
- package/docs/reference/benchmarks.md +1162 -0
- package/docs/reference/cli.md +394 -159
- package/docs/reference/distribution.md +321 -182
- package/docs/reference/environment.md +348 -0
- package/docs/reference/glossary.md +39 -0
- package/docs/reference/library-api.md +566 -168
- package/docs/reference/memory.md +227 -64
- package/docs/reference/models.md +104 -6
- package/docs/reference/server-api.md +886 -425
- package/docs/reference/server-config.md +638 -330
- package/docs/reference/training.md +393 -254
- package/docs/reference/troubleshooting.md +166 -0
- package/package.json +9 -4
- package/src/audio/decode.ts +2 -2
- package/src/audio/features.ts +2 -2
- package/src/backends/mlx/autoregressive.ts +108 -0
- package/src/backends/mlx/batch-group.ts +1646 -0
- package/src/backends/mlx/diffusion.ts +39 -0
- package/src/backends/mlx/draft-target.ts +47 -0
- package/src/backends/mlx/evaluation.ts +105 -0
- package/src/backends/mlx/fill-append.ts +45 -0
- package/src/backends/mlx/gateway-binding.ts +71 -0
- package/src/backends/mlx/graph.ts +38 -0
- package/src/backends/mlx/memory-client.ts +189 -0
- package/src/backends/mlx/memory-guard.ts +40 -0
- package/src/backends/mlx/methods.ts +90 -0
- package/src/backends/mlx/model-prompt.ts +164 -0
- package/src/backends/mlx/model-serving.ts +79 -0
- package/src/backends/mlx/prefill.ts +33 -0
- package/src/backends/mlx/request-policy.ts +20 -0
- package/src/backends/mlx/rollback.ts +51 -0
- package/src/backends/mlx/serial-executor.ts +273 -0
- package/src/backends/mlx/speculative.ts +114 -0
- package/src/backends/mlx/state-views.ts +26 -0
- package/src/backends/mlx/text-engine.ts +73 -0
- package/src/cli.ts +58 -25
- package/src/client.ts +66 -0
- package/src/config.ts +21 -4
- package/src/contracts/completion.ts +16 -0
- package/src/contracts/execution.ts +49 -0
- package/src/contracts/generation.ts +103 -0
- package/src/contracts/host.ts +7 -0
- package/src/contracts/jobs.ts +66 -0
- package/src/contracts/pi-provider.ts +30 -0
- package/src/contracts/pi.ts +259 -0
- package/src/contracts/resources.ts +18 -0
- package/src/contracts/scheduling.ts +27 -0
- package/src/contracts/tsconfig.json +14 -0
- package/src/diffusion/diffusion-generate.ts +218 -153
- package/src/embed.ts +1 -1
- package/src/engine/admission.ts +59 -0
- package/src/engine/cancellation.ts +38 -0
- package/src/engine/engine.ts +56 -0
- package/src/engine/execution-coordinator.ts +70 -0
- package/src/engine/execution-plan.ts +51 -0
- package/src/engine/resources.ts +42 -0
- package/src/engine/scheduler.ts +67 -0
- package/src/engine/session.ts +247 -0
- package/src/eval/README.md +44 -1
- package/src/eval/kl.ts +236 -3
- package/src/eval/reference-logits.ts +207 -0
- package/src/eval/runner.ts +8 -96
- package/src/fill/echo-index.ts +188 -0
- package/src/fill/fill-session.ts +452 -0
- package/src/fill/proposal.ts +68 -0
- package/src/fill/schema-rows.ts +506 -0
- package/src/fill/tool-boundary.ts +103 -0
- package/src/fit.ts +1 -1
- package/src/generate.ts +656 -268
- package/src/grammar.ts +12 -7
- package/src/harness-pi.ts +14 -33
- package/src/hub-rest.ts +4 -4
- package/src/index.ts +7 -0
- package/src/inference/denoising.ts +17 -0
- package/src/inference/graph.ts +33 -0
- package/src/inference/prefill.ts +39 -0
- package/src/inference/rollback.ts +7 -0
- package/src/jobs/db.ts +4 -0
- package/src/jobs/index.ts +2 -0
- package/src/jobs/job-entry.ts +12 -12
- package/src/jobs/runner.ts +98 -13
- package/src/jobs/task-client.ts +19 -0
- package/src/jobs/types.ts +2 -58
- package/src/kv-store.ts +109 -49
- package/src/lab/README.md +18 -0
- package/src/{assets → lab/curve}/curve-designer.html +2 -2
- package/src/{curve-sampler.ts → lab/curve/curve-sampler.ts} +2 -2
- package/src/{expert-trace.ts → lab/expert-trace/expert-trace.ts} +5 -5
- package/src/{model → lab/paged-kv}/paged-kv.ts +9 -6
- package/src/library.ts +40 -0
- package/src/lora.ts +18 -0
- package/src/media-fetch.ts +1 -1
- package/src/memory/article.ts +1 -1
- package/src/memory/chunk.ts +1 -1
- package/src/memory/model.ts +30 -169
- package/src/memory/prompts.ts +1 -1
- package/src/memory/rest.ts +1 -1
- package/src/memory/schedule.ts +1 -1
- package/src/memory/stages.ts +4 -1
- package/src/memory/vault.ts +1 -3
- package/src/mlx/array.ts +9 -10
- package/src/mlx/compile.ts +12 -2
- package/src/mlx/ffi.ts +58 -11
- package/src/mlx/materialize.ts +23 -0
- package/src/mlx/metal-kernel.ts +10 -11
- package/src/mlx/ops.ts +33 -13
- package/src/mlx/quantized-rows.ts +36 -0
- package/src/mlx/token-bitmask.ts +52 -0
- package/src/mlx/turboquant-kv-decode.ts +82 -0
- package/src/mlx/turboquant-kv-inverse.ts +122 -0
- package/src/mlx/turboquant-ops.ts +33 -3
- package/src/mlx/turboquant-tables.ts +2 -2
- package/src/model/batched-mask.ts +8 -2
- package/src/model/batched-quant.ts +1 -1
- package/src/model/batched-rotating-quant.ts +2 -2
- package/src/model/batched-rotating.ts +1 -1
- package/src/model/compiled-decode.ts +1 -1
- package/src/model/diffusion-gemma.ts +8 -8
- package/src/model/factory.ts +76 -33
- package/src/model/fingerprint.ts +1 -1
- package/src/model/flash-attention.ts +3 -3
- package/src/model/gemma4-base.ts +26 -9
- package/src/model/gemma4.ts +6 -6
- package/src/model/generated/gemma4-12b.ts +2 -2
- package/src/model/generated/gemma4-26b.ts +2 -2
- package/src/model/generated/gemma4-e4b.ts +2 -2
- package/src/model/glm52-dsa.ts +13 -9
- package/src/model/glm52-memory.ts +1 -1
- package/src/model/implementation.ts +69 -0
- package/src/model/minicpm5.ts +6 -8
- package/src/model/profile.ts +12 -2
- package/src/model/qwen-append.ts +12 -0
- package/src/model/qwen-conv.ts +52 -0
- package/src/model/qwen3_5-checkpoint.ts +164 -0
- package/src/model/qwen3_5.ts +140 -56
- package/src/model/support.ts +1 -1
- package/src/model/trellis-balanced-scatter.ts +51 -0
- package/src/model/trellis-linear.ts +672 -0
- package/src/model/trellis-shared-m.ts +151 -0
- package/src/model/trellis-shared-scatter.ts +144 -0
- package/src/model/trellis-splitk-prefill.ts +158 -0
- package/src/model/trellis-tiled-prefill.ts +136 -0
- package/src/model/trellis-vector-expand.ts +62 -0
- package/src/model/trellis-wide-prefill.ts +94 -0
- package/src/model/universal/dense.ts +1 -1
- package/src/model/universal/modules.ts +8 -1
- package/src/native-pack.ts +6 -5
- package/src/pi-provider.ts +6 -64
- package/src/pi-web.ts +8 -315
- package/src/prompt-cache.ts +96 -53
- package/src/quantize/allocator.ts +1 -1
- package/src/quantize/config-writer.ts +10 -3
- package/src/quantize/rotate.ts +4 -4
- package/src/quantize/trellis.ts +586 -0
- package/src/responses.ts +28 -1
- package/src/runtime-config.ts +18 -4
- package/src/sampler.ts +4 -4
- package/src/serve/batch-scheduler.ts +8 -1671
- package/src/serve/chat-request.ts +391 -0
- package/src/serve/chat-stage.ts +195 -0
- package/src/serve/checkpoint-identity.ts +64 -0
- package/src/serve/completion-executor.ts +15 -7
- package/src/serve/discovery-routes.ts +12 -13
- package/src/serve/generation-gateway.ts +154 -193
- package/src/serve/http.ts +159 -0
- package/src/serve/inference-request.ts +79 -0
- package/src/serve/isolate.ts +297 -105
- package/src/serve/lab-routes.ts +3 -0
- package/src/serve/lane-registry.ts +2 -1
- package/src/serve/model-admin-routes.ts +7 -5
- package/src/serve/model-binding.ts +43 -0
- package/src/serve/model-host.ts +512 -0
- package/src/serve/openai-wire.ts +103 -0
- package/src/serve/parent-application.ts +56 -0
- package/src/serve/pipeline.ts +33 -0
- package/src/serve/preparation.ts +33 -0
- package/src/serve/request-plan.ts +19 -15
- package/src/serve/request-prep.ts +346 -0
- package/src/serve/response-lease.ts +24 -0
- package/src/serve/responses-client.ts +69 -0
- package/src/serve/session-completion-engine.ts +110 -0
- package/src/serve/session-search.ts +1 -1
- package/src/serve/static-routes.ts +5 -5
- package/src/serve/text-completion-stage.ts +104 -0
- package/src/serve/token-streams.ts +519 -0
- package/src/server.ts +317 -2649
- package/src/spec/assistant-source.ts +10 -44
- package/src/spec/deepspec-source.ts +6 -6
- package/src/spec/dflash-source.ts +8 -8
- package/src/spec/dspark/ab-stats.ts +3 -3
- package/src/spec/dspark/calibration.ts +2 -2
- package/src/spec/dspark/deepspec-module.ts +2 -2
- package/src/spec/dspark/generate-dflash.ts +2 -2
- package/src/spec/dspark/generate.ts +2 -2
- package/src/spec/dspark/loader.ts +1 -1
- package/src/spec/dspark/module-dflash.ts +4 -3
- package/src/spec/dspark/module.ts +3 -3
- package/src/spec/dspark/quantize-drafter.ts +1 -1
- package/src/spec/generate.ts +1 -1
- package/src/spec/glm52-mtp-source.ts +1 -1
- package/src/spec/ngram-source.ts +1 -1
- package/src/spec/prefix-state.ts +45 -0
- package/src/spec/qwen-mtp-source.ts +166 -69
- package/src/spec/serve-loop.ts +154 -156
- package/src/spec/source.ts +42 -10
- package/src/spec/two-model.ts +1 -1
- package/src/ssd-cache.ts +80 -12
- package/src/ssd-durability.ts +25 -13
- package/src/tool-call.ts +51 -14
- package/src/train/flash-cce.ts +2 -2
- package/src/train/forward.ts +6 -0
- package/src/train/loss.ts +1 -1
- package/src/train/prefix-shared.ts +5 -1
- package/src/train/segmented.ts +5 -3
- package/src/train/trainer.ts +6 -6
- package/src/vision/prompt.ts +1 -1
- package/src/vision/qwen3vl-tower.ts +52 -10
- package/src/web/app.html +1 -1
- package/src/web/app.js +5 -3
- package/src/web/src/adapters-panel.ts +3 -3
- package/src/web/src/assistant.ts +2 -36
- package/src/web/src/chat.ts +4 -4
- package/src/web/src/composer.ts +4 -4
- package/src/web/src/hub.ts +1 -1
- package/src/web/src/markdown.ts +0 -0
- package/src/web/src/memory-panel.ts +1 -1
- package/src/web/src/model-picker.ts +1 -1
- package/src/web/src/palette.ts +1 -1
- package/src/web/src/protocol.ts +3 -39
- package/src/web/src/sessions.ts +1 -1
- package/src/web/src/ui-catalog.ts +4 -18
- package/src/web-assets.ts +47 -0
- package/src/weights.ts +55 -9
- package/docs/archive/investigations/curve-runs/bisector-route.json +0 -98
- package/docs/archive/investigations/curve-runs/bisector-trace.json +0 -1393
- package/docs/archive/investigations/curve-runs/curve-terrain.json +0 -1179
- package/docs/archive/investigations/curve-runs/curve-terrain.svg +0 -42
- package/docs/archive/investigations/hlg-ab.html +0 -34
- package/docs/archive/investigations/hlg-explorer.html +0 -238
- package/docs/archive/investigations/hlg-figs/cliff.svg +0 -1
- package/docs/archive/investigations/hlg-figs/cross.svg +0 -1
- package/docs/archive/investigations/hlg-figs/edges.svg +0 -1
- package/docs/archive/investigations/hlg-figs/k20-grids.svg +0 -1
- package/docs/archive/investigations/hlg-figs/k20-peaks.svg +0 -1
- package/docs/archive/investigations/hlg-figs/lw-curve.svg +0 -1
- package/docs/archive/investigations/hlg-figs/terrain.svg +0 -1
- package/docs/archive/investigations/hlg-report.html +0 -262
- package/docs/archive/investigations/hlg-runs/hlg-ab.json +0 -149
- package/docs/archive/investigations/hlg-runs/hlg-blind-prompts.md +0 -87
- package/docs/archive/investigations/hlg-runs/hlg-blind.json +0 -107
- package/docs/archive/investigations/hlg-runs/hlg-cliff.json +0 -197
- package/docs/archive/investigations/hlg-runs/hlg-cross.json +0 -505
- package/docs/archive/investigations/hlg-runs/hlg-edge.json +0 -388
- package/docs/archive/investigations/hlg-runs/hlg-eval1.json +0 -25
- package/docs/archive/investigations/hlg-runs/hlg-frontier.json +0 -86
- package/docs/archive/investigations/hlg-runs/hlg-grid.json +0 -242
- package/docs/archive/investigations/hlg-runs/hlg-grid2.json +0 -343
- package/docs/archive/investigations/hlg-runs/hlg-lw-coarse.json +0 -110
- package/docs/archive/investigations/hlg-runs/hlg-map.json +0 -383
- package/docs/archive/investigations/hlg-runs/run0-original.log +0 -159
- package/docs/archive/investigations/hlg-runs/runA-recommended.log +0 -131
- package/docs/archive/investigations/hlg-runs/runAA-cliff-cpm5.log +0 -26
- package/docs/archive/investigations/hlg-runs/runAB-grid2-e4b.log +0 -18
- package/docs/archive/investigations/hlg-runs/runAC-peak-e4b-partial.log +0 -25
- package/docs/archive/investigations/hlg-runs/runAD-ab-samples.log +0 -8
- package/docs/archive/investigations/hlg-runs/runAE-edge-e4b.log +0 -51
- package/docs/archive/investigations/hlg-runs/runAF-logit-calibration.log +0 -10
- package/docs/archive/investigations/hlg-runs/runAG-eval-config2.log +0 -16
- package/docs/archive/investigations/hlg-runs/runB-gain-variants.log +0 -192
- package/docs/archive/investigations/hlg-runs/runC-pivots.log +0 -162
- package/docs/archive/investigations/hlg-runs/runD-oetf.log +0 -132
- package/docs/archive/investigations/hlg-runs/runE-eotf.log +0 -162
- package/docs/archive/investigations/hlg-runs/runF-shaper-working.log +0 -86
- package/docs/archive/investigations/hlg-runs/runG-shaper-sweep.log +0 -101
- package/docs/archive/investigations/hlg-runs/runH-wsweep.log +0 -176
- package/docs/archive/investigations/hlg-runs/runI-asweep.log +0 -176
- package/docs/archive/investigations/hlg-runs/runJ-knobmap.log +0 -49
- package/docs/archive/investigations/hlg-runs/runK-frontier.log +0 -19
- package/docs/archive/investigations/hlg-runs/runL-widemap.log +0 -65
- package/docs/archive/investigations/hlg-runs/runM-frontier-emb.log +0 -15
- package/docs/archive/investigations/hlg-runs/runN-verify.log +0 -116
- package/docs/archive/investigations/hlg-runs/runO-frontier-seed5000.log +0 -15
- package/docs/archive/investigations/hlg-runs/runP-grid-AxSm.log +0 -15
- package/docs/archive/investigations/hlg-runs/runQ-grid-tgW.log +0 -15
- package/docs/archive/investigations/hlg-runs/runR-blind-transcript.log +0 -79
- package/docs/archive/investigations/hlg-runs/runS-cross-coherence.log +0 -33
- package/docs/archive/investigations/hlg-runs/runT-cross-cpm5.log +0 -33
- package/docs/archive/investigations/hlg-runs/runU-cross-12b.log +0 -33
- package/docs/archive/investigations/hlg-runs/runV-lw-e4b.log +0 -22
- package/docs/archive/investigations/hlg-runs/runW-lw-12b.log +0 -22
- package/docs/archive/investigations/hlg-runs/runX-lw-cpm5.log +0 -22
- package/docs/archive/investigations/hlg-runs/runY-cliff-e4b.log +0 -26
- package/docs/archive/investigations/hlg-runs/runZ-cliff-12b.log +0 -26
- package/docs/archive/mlx-bun-lab-report.html +0 -275
- package/docs/archive/wwdc-mlx-bun.html +0 -848
- package/docs/design/adapters-end-to-end.md +0 -205
- package/docs/design/batching-perf-path.md +0 -255
- package/docs/design/batching-v2-plan.md +0 -529
- package/docs/design/bucketing-stage.md +0 -197
- package/docs/design/compat-cli-surface-design.md +0 -230
- package/docs/design/docs-reorg-plan.md +0 -165
- package/docs/design/dspark-serving-program.md +0 -371
- package/docs/design/dspark-speculative-decoding.md +0 -318
- package/docs/design/grammar-spec-batching-integration.md +0 -301
- package/docs/design/memory-inference-path.md +0 -422
- package/docs/design/memory-synthesis.md +0 -287
- package/docs/design/memory-system.md +0 -237
- package/docs/design/mlx-lm-serving-execution-seam.md +0 -81
- package/docs/design/mlx-lm-tool-parity-plan.md +0 -545
- package/docs/design/omlx-adoption-map.md +0 -98
- package/docs/design/orpo-dynamic-lambda.md +0 -220
- package/docs/design/orpo-future-enhancements.md +0 -340
- package/docs/design/paged-kv-cache.md +0 -214
- package/docs/design/parallel-slots.md +0 -567
- package/docs/design/parity-tier-dag.md +0 -161
- package/docs/design/repo-cleanup-plan.md +0 -231
- package/docs/design/runtime-isolation.md +0 -263
- package/docs/design/segmented-backward-training.md +0 -543
- package/docs/design/spec-decode-larger-targets.md +0 -229
- package/docs/design/ssd-kv-cold-tier.md +0 -415
- package/docs/design/structured-output.md +0 -484
- package/docs/design/tauri-desktop-app.md +0 -127
- package/docs/design/the-dreaming-master-plan.md +0 -445
- package/docs/design/turboquant-kv.md +0 -235
- package/docs/design/turboquant-weights.md +0 -467
- package/docs/design/w4a16-compute-precision-spike.md +0 -232
- package/docs/design/web-chat-beat-matrix.md +0 -315
- package/docs/design/write-pipeline-entity.md +0 -468
- package/docs/planning/IDEAS.md +0 -69
- package/docs/reference/embedding.md +0 -197
- package/docs/reference/features-matrix.md +0 -101
- package/docs/reference/orpo-quickstart.md +0 -135
- package/src/flags.ts +0 -11
- /package/docs/{design → archive/investigations}/colibri-glm52-port.md +0 -0
- /package/docs/{planning → archive/planning}/chunk-task-roadmap.md +0 -0
- /package/docs/{planning → archive/planning}/curve-sampler-research-plan.md +0 -0
- /package/docs/{planning → archive/planning}/memory-docs-and-dag-plan.md +0 -0
- /package/docs/{planning → archive/planning}/pi-integration-review.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.2.0.md +0 -0
- /package/src/{memory-plan.ts → admission-plan.ts} +0 -0
- /package/{docs/archive/investigations → src/serve/assets}/curve-terrain.html +0 -0
- /package/{docs/dag → src/serve/assets}/training-inference-map.html +0 -0
|
@@ -1,19 +1,111 @@
|
|
|
1
1
|
# Server API
|
|
2
2
|
|
|
3
|
-
`mlx-bun serve` exposes an OpenAI-compatible
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
`mlx-bun serve` exposes an OpenAI-compatible HTTP API on one model. The
|
|
4
|
+
request's `model` field is ignored; the loaded model's id is echoed back.
|
|
5
|
+
The default concurrency cap is eight. Eligible requests use continuous batching;
|
|
6
|
+
`--batch 1` pins serial execution. Both use the shared generation session for
|
|
7
|
+
output, cancellation and cleanup. Each model implementation supplies its own
|
|
8
|
+
execution methods; protocol handlers do not select a concrete model class.
|
|
9
|
+
Embedding execution also acquires the shared model/GPU lease, so it waits for
|
|
10
|
+
active generation or model mutation before running.
|
|
11
|
+
|
|
12
|
+
This is the canonical wire reference: every route the server registers is
|
|
13
|
+
listed below, and every field documented here exists in the code that
|
|
14
|
+
serves it (`src/server.ts`, `src/serve/*-routes.ts`, `src/serve/isolate.ts`,
|
|
15
|
+
`src/pi-web.ts`). Start flags (`--port`, `--memory-budget`,
|
|
10
16
|
`--prompt-cache`, `--batch`, `--kv-quant`, `--thinking`, sampling
|
|
11
|
-
defaults
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
defaults) and which combinations compose live in
|
|
18
|
+
[server-config.md](./server-config.md); the CLI in [cli.md](./cli.md).
|
|
19
|
+
|
|
20
|
+
## Route index
|
|
21
|
+
|
|
22
|
+
Dispatch order matters only for `/ws/chat` (upgrade) and the static shell;
|
|
23
|
+
every other path is unique. Unmatched paths return
|
|
24
|
+
`404 {"error":{"message":"not found"}}`.
|
|
25
|
+
|
|
26
|
+
| Method | Path | Section |
|
|
27
|
+
| --- | --- | --- |
|
|
28
|
+
| POST | `/v1/chat/completions` | [Chat completions](#post-v1chatcompletions) |
|
|
29
|
+
| POST | `/v1/completions` | [Text completions](#post-v1completions-raw-text-completion) |
|
|
30
|
+
| POST | `/v1/messages` | [Anthropic Messages](#post-v1messages-anthropic-messages-api) |
|
|
31
|
+
| POST | `/v1/responses` | [OpenAI Responses](#post-v1responses-openai-responses-api) |
|
|
32
|
+
| POST | `/v1/embeddings` | [Embeddings](#post-v1embeddings-openai-embeddings-api) |
|
|
33
|
+
| GET | `/v1` | [API index](#get-v1) |
|
|
34
|
+
| GET | `/v1/models`, `/v1/models/<id>` | [Models](#get-v1models) |
|
|
35
|
+
| GET / POST | `/v1/adapters` | [Adapters](#adapters-lora-hot-swap) |
|
|
36
|
+
| GET | `/v1/adapters/available` | [Adapters](#adapters-lora-hot-swap) |
|
|
37
|
+
| DELETE | `/v1/adapters/<id>` | [Adapters](#adapters-lora-hot-swap) |
|
|
38
|
+
| GET | `/health` | [Health](#get-health) |
|
|
39
|
+
| GET | `/stats` | [Stats](#get-stats) |
|
|
40
|
+
| GET | `/fit` | [Fit](#get-fit) |
|
|
41
|
+
| GET | `/library` | [Library](#get-library) |
|
|
42
|
+
| GET | `/downloads` | [Downloads](#get-downloads) |
|
|
43
|
+
| POST | `/admin/cache/flush` | [SSD cache flush](#post-admincacheflush) |
|
|
44
|
+
| GET | `/api/jobs`, `/api/jobs/<id>`, `/api/jobs/<id>/stream` | [Jobs](#jobs-apijobs) |
|
|
45
|
+
| POST | `/api/finetune/submit`, `/inspect-dataset`, `/merge`, `/export` | [Fine-tune](#fine-tune-apifinetune) |
|
|
46
|
+
| POST | `/api/quantize/inspect`, `/submit`, `/resolve-folder` | [Quantize](#quantize-apiquantize) |
|
|
47
|
+
| GET / POST | `/api/dataset/templates`, `/api/dataset/submit` | [Dataset](#dataset-apidataset) |
|
|
48
|
+
| POST | `/api/model/resolve-folder` | [Quantize](#quantize-apiquantize) |
|
|
49
|
+
| POST | `/api/{quantize,finetune,dataset}/push` | [Push to Hub](#post-apiquantizefinetunedatasetpush) |
|
|
50
|
+
| GET / POST | `/api/gc/plan`, `/api/gc/execute` | [GC](#get-apigcplan--post-apigcexecute) |
|
|
51
|
+
| GET / POST | `/api/memory/*` | [Memory](#get-apimemory--post-apimemoryinit) |
|
|
52
|
+
| GET | `/v1/memory/synthesize` | [Memory synthesis](#get-v1memorysynthesize) |
|
|
53
|
+
| GET / POST / DELETE | `/api/settings/hf-token`, `/api/settings/tool-approvals` | [Settings](#settings-apisettings) |
|
|
54
|
+
| GET / POST | `/api/hub/local`, `/search`, `/download`, `/serve` | [Model Hub](#model-hub-apihub) |
|
|
55
|
+
| GET | `/api/sessions/search`, `/api/sessions/export` | [Sessions](#get-apisessionssearch--get-apisessionsexport) |
|
|
56
|
+
| GET | `/`, `/assets/*`, `/manifest.webmanifest`, `/sw.js`, `/curves`, `/curve-terrain`, `/dag` | [Static](#web-app-static-routes--pwa-installability) |
|
|
57
|
+
| GET | `/status`, `/chat`, `/quantize`, `/finetune`, `/dataset` | 302 to `/#<path>` ([Static](#web-app-static-routes--pwa-installability)) |
|
|
58
|
+
| POST | `/signal`, `/generate` | [Curve Designer](#curve-designer-post-signal--post-generate) |
|
|
59
|
+
| WS | `/ws/chat` | [Web chat WebSocket](#web-chat-websocket-wschat) |
|
|
60
|
+
|
|
61
|
+
Internal routes (not part of the client contract):
|
|
62
|
+
|
|
63
|
+
| Method | Path | Where | Notes |
|
|
64
|
+
| --- | --- | --- | --- |
|
|
65
|
+
| POST | `/admin/drain` | `src/server.ts` | Only registered when the server is bound to a unix socket (`--unix`, i.e. an `--isolate` engine child). Quiesces the gateway and demotes the whole prompt cache to the SSD tier; returns `{ "drained": true, "demotions": N }`. Never exposed on TCP. |
|
|
66
|
+
| POST | `/admin/lease` | `src/server.ts` | Unix-socket workers only. Flushes durability, then holds the gateway's native execution lease while the response connection stays open. The isolation parent uses it for managed GPU jobs; disconnect releases the lease. Never exposed on TCP. |
|
|
67
|
+
| GET | `/engine` | `src/serve/isolate.ts` | Only on the `--isolate` parent proxy: `{ isolated: true, pid, restarts, socket, response_store, pool?: { resident, default } }`. Worker fields describe the current default worker, or are `null` while it is evicted; inspection never loads it. |
|
|
68
|
+
|
|
69
|
+
Under `--isolate`, the parent proxies everything to the engine child;
|
|
70
|
+
`/ws/chat` is answered `501` there (web chat needs a non-isolated server),
|
|
71
|
+
`/v1/chat/completions`, `/v1/completions`, `/v1/messages`, `/v1/responses`,
|
|
72
|
+
and `/v1/embeddings` are routed by the body's `model` field when a
|
|
73
|
+
`--model-pool` is configured, an unreachable child is
|
|
74
|
+
`502 {"error":{"type":"engine_unavailable"}}`, and a client disconnect
|
|
75
|
+
mid-proxy is `499`.
|
|
76
|
+
|
|
77
|
+
## Diagnostics: `x-mlx-bun-trace-id` and `MLX_BUN_P2R_TRACE`
|
|
78
|
+
|
|
79
|
+
`/v1/chat/completions` and `/v1/completions` read an optional
|
|
80
|
+
`x-mlx-bun-trace-id` request header. With `MLX_BUN_P2R_TRACE=1` set on the
|
|
81
|
+
server, each of those requests emits one JSON record to stderr, prefixed
|
|
82
|
+
`[p2r] `, when it finishes (`src/serve/prompt-response-trace.ts`). Without
|
|
83
|
+
the env var nothing is allocated and the header is ignored.
|
|
84
|
+
|
|
85
|
+
```jsonc
|
|
86
|
+
{
|
|
87
|
+
"version": 1,
|
|
88
|
+
"traceId": "<x-mlx-bun-trace-id, or the request id when absent>",
|
|
89
|
+
"requestId": "chatcmpl-…" | "cmpl-…",
|
|
90
|
+
"route": "/v1/chat/completions" | "/v1/completions",
|
|
91
|
+
"clock": "monotonic-ms",
|
|
92
|
+
"outcome": "success" | "error" | "abort",
|
|
93
|
+
"totalMs": 0.0,
|
|
94
|
+
"events": [ // sorted by startMs
|
|
95
|
+
{ "phase": "request.body_parse", "startMs": 0.0, "durationMs": 0.0,
|
|
96
|
+
"attributes": { /* optional string|number|boolean|null map */ } }
|
|
97
|
+
],
|
|
98
|
+
"attributes": { "stage": "body_parse" | "prepare_completion" } // on some errors
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Phases: `request.body_parse`, `request.prompt_prepare`, `completion.total`,
|
|
103
|
+
`completion.placement`, `engine.admission_wait`, `cache.lookup_restore`,
|
|
104
|
+
`prefill.total`, `prefill.batch_setup`, `prefill.chunk`,
|
|
105
|
+
`prefill.kv_maintenance`, `token_zero.total`, `token_zero.forward`,
|
|
106
|
+
`token_zero.head`, `token_zero.sample`, `response.first_write`,
|
|
107
|
+
`response.final_write`. Marks (`response.first_write`,
|
|
108
|
+
`response.final_write`) have `durationMs: 0`.
|
|
17
109
|
|
|
18
110
|
## POST /v1/chat/completions
|
|
19
111
|
|
|
@@ -23,7 +115,10 @@ Request body (OpenAI chat schema; unknown fields ignored):
|
|
|
23
115
|
{
|
|
24
116
|
"messages": [ /* role: system | user | assistant | tool */ ],
|
|
25
117
|
"stream": false,
|
|
26
|
-
"max_tokens": 1024, // or max_completion_tokens (wins)
|
|
118
|
+
"max_tokens": 1024, // or max_completion_tokens (wins); unset =
|
|
119
|
+
// no cap — generate until EOS or the
|
|
120
|
+
// admitted context is exhausted (admission
|
|
121
|
+
// clamps; --max-tokens sets a server default)
|
|
27
122
|
"temperature": 0.7, // 0 = greedy
|
|
28
123
|
"top_p": 0, "top_k": 0, // 0 = off
|
|
29
124
|
"seed": 1234, // omit for time-derived
|
|
@@ -48,7 +143,9 @@ Request body (OpenAI chat schema; unknown fields ignored):
|
|
|
48
143
|
// (token, logprob) pairs per position
|
|
49
144
|
"stop": "\n\n", // or ["###", "\n\n"] (spec: up to 4)
|
|
50
145
|
"tools": [ /* OpenAI function tools */ ],
|
|
51
|
-
"tool_choice": "auto", // "none" disables tools
|
|
146
|
+
"tool_choice": "auto", // "none" disables tools; "auto" and the
|
|
147
|
+
// {type:"function", function:{name}}
|
|
148
|
+
// object are accepted and treated as auto
|
|
52
149
|
"chat_template_kwargs": { // forwarded to the chat template
|
|
53
150
|
"enable_thinking": false, // MiniCPM5 / Qwen3.5/3.8 / GLM-5.2: <think>
|
|
54
151
|
// channel on/off (Qwen3.8 default: ON)
|
|
@@ -68,9 +165,7 @@ Request body (OpenAI chat schema; unknown fields ignored):
|
|
|
68
165
|
// variable (Qwen3.8) the level also maps
|
|
69
166
|
// into the template: minimal/low → low,
|
|
70
167
|
// medium → medium, high/xhigh → xhigh.
|
|
71
|
-
// Any OTHER string is a 400
|
|
72
|
-
// to silently force thinking ON
|
|
73
|
-
// (Qwen3.8's default depth is xhigh).
|
|
168
|
+
// Any OTHER string is a 400.
|
|
74
169
|
"hlg": { // HLG tone-curve sampling (per request).
|
|
75
170
|
"enabled": true, // merged over --hlg-sampling server defaults.
|
|
76
171
|
"width": 4, // logit-width of the tone plateau
|
|
@@ -91,6 +186,13 @@ Sampling defaults follow the model author's `generation_config.json`
|
|
|
91
186
|
when a field is omitted (optiq serve's gen_config behavior); explicit
|
|
92
187
|
request values always win. MiniCPM5 defaults to the no-think direct
|
|
93
188
|
answer mode unless `chat_template_kwargs.enable_thinking` is `true`.
|
|
189
|
+
|
|
190
|
+
Assistant history accepts `reasoning` as an alias for `reasoning_content`,
|
|
191
|
+
including Pi's reasoning on tool-call messages. An explicit `reasoning_content`
|
|
192
|
+
string takes precedence, even when empty. Normalization preserves the original
|
|
193
|
+
message and exposes the canonical field to templates, so Qwen's
|
|
194
|
+
`preserve_thinking: true` retains that history across tool turns.
|
|
195
|
+
|
|
94
196
|
GLM-5.2 also defaults to no-think: its generation primer is
|
|
95
197
|
`<think></think>`. Enabling thinking renders `Reasoning Effort: Max` in the
|
|
96
198
|
system turn and leaves an open `<think>` primer for generation. The same
|
|
@@ -105,20 +207,22 @@ and `finish_reason` is `"stop"`.
|
|
|
105
207
|
Message `content` is a string or an array of parts:
|
|
106
208
|
`{ "type": "text", "text": ... }` and
|
|
107
209
|
`{ "type": "image_url", "image_url": { "url": "data:image/png;base64,..." } }`
|
|
108
|
-
(
|
|
109
|
-
|
|
210
|
+
(`{ "type": "image", ... }` is accepted as an alias; http/https URLs also
|
|
211
|
+
accepted; PNG, JPEG, HEIC, AVIF, WebP, TIFF, GIF, BMP via native OS codecs;
|
|
212
|
+
requires a model with the vision sidecar).
|
|
110
213
|
|
|
111
214
|
Remote (http/https) media URLs — image, audio, and video alike — go
|
|
112
|
-
through a destination policy: **private/loopback/
|
|
113
|
-
refused by default** (the request URL is
|
|
114
|
-
SSRF), every redirect hop is re-validated, and
|
|
115
|
-
timeout** and a **64 MB response cap** (video:
|
|
116
|
-
legitimately larger; frame sampling truncates long
|
|
117
|
-
Violations are clean `400`s
|
|
215
|
+
through a destination policy (`src/media-fetch.ts`): **private/loopback/
|
|
216
|
+
link-local hosts are refused by default** (the request URL is
|
|
217
|
+
attacker-controlled input — SSRF), every redirect hop is re-validated, and
|
|
218
|
+
the fetch has a **10 s timeout** and a **64 MB response cap** (video:
|
|
219
|
+
**256 MB** — clips are legitimately larger; frame sampling truncates long
|
|
220
|
+
clips server-side). Violations are clean `400`s
|
|
118
221
|
(`prompt build failed: image url rejected: …`). Serving media from a LAN
|
|
119
222
|
host (a NAS, another machine) is the opt-in
|
|
120
|
-
[`--allow-private-media`](server-config.md#start-flags) flag
|
|
121
|
-
URLs are decoded locally and
|
|
223
|
+
[`--allow-private-media`](server-config.md#start-flags) flag (or
|
|
224
|
+
`MLX_BUN_ALLOW_PRIVATE_MEDIA=1`). `data:` URLs are decoded locally and
|
|
225
|
+
never policy-checked.
|
|
122
226
|
|
|
123
227
|
### Video input (Qwen3.5-family models)
|
|
124
228
|
|
|
@@ -133,11 +237,12 @@ The container decodes through macOS's own AVFoundation codecs (mp4/mov,
|
|
|
133
237
|
H.264/HEVC/ProRes/… — whatever this macOS plays) via the
|
|
134
238
|
`mlx-bun-frame-extract` helper that ships beside the binary and in the
|
|
135
239
|
native pack; from a source tree it compiles on first use with the Xcode
|
|
136
|
-
CLT. Frames are sampled at **2 fps, capped at 768 frames
|
|
137
|
-
gated Qwen3-VL video pipeline
|
|
138
|
-
attention, 3D mRoPE positions).
|
|
139
|
-
|
|
140
|
-
|
|
240
|
+
CLT. Frames are sampled at **2 fps, capped at 768 frames**
|
|
241
|
+
(`src/vision/video-frames.ts`), then run the gated Qwen3-VL video pipeline
|
|
242
|
+
(temporal frame pairs, per-frame-group attention, 3D mRoPE positions).
|
|
243
|
+
Qwen3.5-family models only; video never combines with audio parts
|
|
244
|
+
(`400 "video and audio content parts cannot be combined"`), and a non-Qwen
|
|
245
|
+
model answers video parts with a `400` naming the model.
|
|
141
246
|
|
|
142
247
|
### Audio input
|
|
143
248
|
|
|
@@ -172,12 +277,13 @@ curl -s http://localhost:8080/v1/chat/completions \
|
|
|
172
277
|
`400` naming the model — never a silent text-only answer.
|
|
173
278
|
- **Formats**: WAV is decoded natively (PCM 16/24/32-bit + float32, any
|
|
174
279
|
channel count / sample rate). mp3, m4a/AAC, FLAC, ogg(-CAF), AIFF and
|
|
175
|
-
anything else CoreAudio reads transcode via macOS `afconvert
|
|
176
|
-
undecodable bytes are a `400
|
|
280
|
+
anything else CoreAudio reads transcode via macOS `afconvert`
|
|
281
|
+
(`src/audio/transcode.ts`); undecodable bytes are a `400`.
|
|
177
282
|
- **Internal format**: 16 kHz mono — multi-channel input is mean-mixed,
|
|
178
283
|
other rates resample. Clips truncate at **30 s** (the oracle
|
|
179
284
|
processor's 480 000-sample cap); one clip costs
|
|
180
|
-
`ceil(duration_ms / 40)` prompt tokens, at most **750
|
|
285
|
+
`ceil(duration_ms / 40)` prompt tokens, at most **750**
|
|
286
|
+
(`src/audio/decode.ts`).
|
|
181
287
|
- Multiple clips per message and **mixed image+audio** requests work;
|
|
182
288
|
media splices in document order.
|
|
183
289
|
- **Routing**: audio requests always run on the **serial lane** (never
|
|
@@ -198,12 +304,16 @@ Non-streaming response:
|
|
|
198
304
|
"index": 0,
|
|
199
305
|
"message": {
|
|
200
306
|
"role": "assistant",
|
|
201
|
-
"content": "…", //
|
|
307
|
+
"content": "…", // null when the reply is ONLY tool calls
|
|
308
|
+
"reasoning": "…", // present only when the model emitted a
|
|
309
|
+
// <think> channel (thinking-capable
|
|
310
|
+
// templates with thinking on)
|
|
202
311
|
"tool_calls": [{ // present when the model called tools
|
|
203
312
|
"id": "call_…", "type": "function",
|
|
204
313
|
"function": { "name": "…", "arguments": "{…json…}" }
|
|
205
314
|
}]
|
|
206
315
|
},
|
|
316
|
+
"logprobs": { /* see below; only when requested */ },
|
|
207
317
|
"finish_reason": "stop" | "length" | "tool_calls"
|
|
208
318
|
}],
|
|
209
319
|
"usage": {
|
|
@@ -211,14 +321,31 @@ Non-streaming response:
|
|
|
211
321
|
"prompt_tokens_details": { "cached_tokens": 0 }, // prompt-cache reuse
|
|
212
322
|
"lane": "serial", // "serial" | "serial+spec" | "batched" —
|
|
213
323
|
// which execution lane served THIS
|
|
214
|
-
// request
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
// only (streaming: final usage chunk).
|
|
324
|
+
// request. Chat completions and text
|
|
325
|
+
// completions (streaming: final usage
|
|
326
|
+
// chunk).
|
|
218
327
|
"speculation": { // only when a draft source is mounted
|
|
219
328
|
"drafted": 0, // draft tokens proposed
|
|
220
329
|
"accepted": 0, // drafts accepted by the verify step
|
|
221
330
|
"targetCalls": 0 // target-model forward calls
|
|
331
|
+
},
|
|
332
|
+
"fill": { // only under MLX_BUN_FILL=strict, when a
|
|
333
|
+
// fill table was armed for the request
|
|
334
|
+
"events": 0, // injected spans
|
|
335
|
+
"injected": 0, // tokens appended by the engine
|
|
336
|
+
"strict": 0, // …of those, from schema/template rows
|
|
337
|
+
"echo": 0, // …from the echo index (MLX_BUN_FILL=echo)
|
|
338
|
+
"spanLens": [], // length of each span, in order
|
|
339
|
+
"wastedSamples": 0, // in-flight samples dropped at an ASSERT fill
|
|
340
|
+
"parseFallback": 0, // tool-call parses rejected after a fill
|
|
341
|
+
"indexTruncated": 0, // spans cut by EOS / delimiter / budget / span
|
|
342
|
+
"decodeSteps": 0, // tokens the model actually sampled
|
|
343
|
+
"verifyEvents": 0, // verify-policy spans that reached the forward
|
|
344
|
+
"verifyAccepted": 0, // …tokens the model's own argmax agreed with
|
|
345
|
+
"verifyRejected": 0, // …tokens dropped and rewound
|
|
346
|
+
"verifyUnsupported": 0, // verify spans dropped: caches cannot rewind
|
|
347
|
+
"checkpointMs": 0, // time in cache checkpoint / rollback
|
|
348
|
+
"branchStops": 0 // echo spans stopped by a history fork
|
|
222
349
|
}
|
|
223
350
|
}
|
|
224
351
|
}
|
|
@@ -231,18 +358,48 @@ contributed accepted draft tokens to this reply, `"serial"` otherwise. It
|
|
|
231
358
|
rides the SAME usage block on both streaming (final chunk before `[DONE]`) and
|
|
232
359
|
non-streaming responses.
|
|
233
360
|
|
|
361
|
+
`usage.completion_tokens` counts the stopping EOS token in both native and
|
|
362
|
+
speculative serial generation, matching mlx-lm. EOS is excluded from response
|
|
363
|
+
content. An early callback stop or token budget does not count a later EOS
|
|
364
|
+
that happened to be evaluated in the same speculative burst.
|
|
365
|
+
An observed EOS reports `finish_reason: "stop"`, including when its counted
|
|
366
|
+
position equals `max_tokens`.
|
|
367
|
+
|
|
234
368
|
Speculative decoding is a **server-level mode** (`serve --draft-model`,
|
|
235
369
|
or the model-free `serve --draft-kind ngram`, which mounts no draft
|
|
236
370
|
model); there is no per-request draft field. The `speculation` usage
|
|
237
|
-
extension
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
normally and omit the field. The spec path bypasses
|
|
242
|
-
|
|
371
|
+
extension appears on chat and text completions alike, non-streaming and on
|
|
372
|
+
the final stream chunk. Spec-eligible requests are text-only on base weights
|
|
373
|
+
(no adapter or logprobs capture, bf16 KV by default); experimental Qwen
|
|
374
|
+
uniform KV4 start=0 can qualify through `MLX_BUN_QWEN_SPEC_KV4=1`.
|
|
375
|
+
Ineligible requests decode normally and omit the field. The spec path bypasses
|
|
376
|
+
the ordinary prompt cache. Qwen MTP with `MLX_BUN_MTP_PROMPT_CACHE=1` can reuse
|
|
377
|
+
a paired target/draft RAM prefix, reported in `cached_tokens`; the default is
|
|
378
|
+
zero. These prefixes do not persist across restarts. While a draft is mounted every request routes
|
|
243
379
|
through the serial lane (mlx_lm.server parity: `is_batchable = draft is
|
|
244
380
|
None`) — speculation and `--batch N` are different modes.
|
|
245
381
|
|
|
382
|
+
`usage.fill` reports **token fast-forwarding** (`MLX_BUN_FILL=strict|echo`).
|
|
383
|
+
Strict mode appends structural spans from the request's tools and chat
|
|
384
|
+
template at qualified parser boundaries, with no draft, verification or
|
|
385
|
+
rollback. The model advances its hidden states and caches through the span,
|
|
386
|
+
using its qualified append chunk size, then resumes sampling
|
|
387
|
+
(design: [speculative-decoding.md](../design/speculative-decoding.md) §"Token
|
|
388
|
+
fast-forwarding"). Injected tokens are billed in `completion_tokens` like any
|
|
389
|
+
other generated token; `decodeSteps` counts ordinary sampling steps, so
|
|
390
|
+
`injected / (injected + decodeSteps)` is the share appended without sampling.
|
|
391
|
+
Injected positions still run through the model layers. The field is absent when the feature is off or the
|
|
392
|
+
request's shape refuses it (fixed `seed`, `logprobs`, structured output,
|
|
393
|
+
media, a mounted draft model, quantized KV).
|
|
394
|
+
|
|
395
|
+
Under `MLX_BUN_FILL=echo` a second, weaker source joins: spans copied from
|
|
396
|
+
earlier in the same session, carried under policy `verify`. Those ride the same
|
|
397
|
+
single forward, are checked against the argmax already in its logits, and have
|
|
398
|
+
their rejected tail rewound — so `verifyAccepted + verifyRejected` is what the
|
|
399
|
+
echo index proposed and `injected` is what survived. `wastedSamples` counts
|
|
400
|
+
only ASSERT fills (a verify fill consumes the in-flight sample as its first
|
|
401
|
+
position's check instead of discarding it).
|
|
402
|
+
|
|
246
403
|
### logprobs / top_logprobs
|
|
247
404
|
|
|
248
405
|
With `"logprobs": true` and/or `"top_logprobs": k` (1–11), the
|
|
@@ -271,44 +428,68 @@ legacy `{tokens, token_logprobs}` text-completion shape).
|
|
|
271
428
|
|
|
272
429
|
The distribution matches mlx-lm `generate_step` exactly: full-vocab
|
|
273
430
|
log-softmax of the logits **after** logits processors (logit_bias,
|
|
274
|
-
penalties), **before** the sampler's temperature/top-p/top-k/min-p/XTC
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
431
|
+
penalties), **before** the sampler's temperature/top-p/top-k/min-p/XTC.
|
|
432
|
+
mlx-lm's top-k order is unspecified (argpartition); ours is sorted
|
|
433
|
+
descending — the same set, so the entry is deterministically the argmax.
|
|
434
|
+
Stream chunks never carry logprobs (mlx-lm's streaming responses don't
|
|
435
|
+
either). Requests with logprobs run on the serial lane under `--batch N`
|
|
436
|
+
(like the other mlx-lm sampler extensions). Invalid values are rejected
|
|
437
|
+
with mlx-lm's exact messages (see Errors).
|
|
438
|
+
|
|
439
|
+
### Streaming
|
|
440
|
+
|
|
441
|
+
`"stream": true` is SSE: `data: <chunk>\n\n` per event, terminated by
|
|
442
|
+
`data: [DONE]`. Chunks are `chat.completion.chunk` objects whose
|
|
443
|
+
`choices[0].delta` carries `{role: "assistant", content: ""}` first, then
|
|
444
|
+
`{reasoning}` increments while the model is inside its `<think>` channel,
|
|
445
|
+
then `{content}` increments (multi-byte sequences are held back until
|
|
446
|
+
decodable; text that could begin a `stop` sequence is held back until
|
|
447
|
+
disambiguated, so no part of a stop sequence is ever streamed), then for
|
|
448
|
+
tool calls a `{tool_calls: [{index, id, type, function}]}` delta; the last
|
|
449
|
+
chunk carries `finish_reason` and `usage`. A generation error mid-stream is
|
|
450
|
+
sent as `data: {"error":{"message":…}}` and the stream closes. While a
|
|
451
|
+
large tool-call body is buffered for parsing, the server sends SSE comment
|
|
452
|
+
heartbeats (`: keep-alive`) so client idle timers do not terminate the call.
|
|
453
|
+
|
|
454
|
+
### Tool calls
|
|
455
|
+
|
|
456
|
+
Qwen's `<parameter=...>` values are raw text. The parser preserves HTML
|
|
457
|
+
entities, indentation and JSON string escapes, removing only one framing
|
|
458
|
+
newline at either end. Array/object parameters are JSON-decoded according to
|
|
459
|
+
the tool schema. The outer Qwen markup is parsed before generic JSON repair,
|
|
460
|
+
so a nested edit array is not mistaken for the tool-call envelope.
|
|
291
461
|
|
|
292
462
|
Tool round-trip: send the assistant message with its `tool_calls` back,
|
|
293
463
|
followed by `{ "role": "tool", "tool_call_id": …, "content": … }`
|
|
294
464
|
messages; multi-turn prompt prefixes reuse the KV prompt cache
|
|
295
465
|
automatically.
|
|
296
466
|
|
|
297
|
-
Tool-call parsing is per model family
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
467
|
+
Tool-call parsing is per model family (`selectToolStreamMode` in
|
|
468
|
+
`src/server.ts`, parsers in `src/tool-call.ts`). Gemma 4 uses its native
|
|
469
|
+
`<|tool_call>`…`<tool_call|>` sentinel tokens. Every other family parses
|
|
470
|
+
**decoded text** once tools are active: OpenAI-JSON
|
|
471
|
+
`<tool_call>{…}</tool_call>`, Qwen-style
|
|
472
|
+
`<tool_call><function=name><parameter=key>value</parameter></function></tool_call>`,
|
|
473
|
+
GLM-5.2's `name<arg_key>…<arg_value>…`, and MiniCPM5's
|
|
474
|
+
`<function name="…"><param name="…">…</param></function>` (CDATA-wrapped
|
|
475
|
+
values supported). Content before the tool markup still streams live; only
|
|
476
|
+
the markup is withheld and converted to `tool_calls`. Argument values are
|
|
477
|
+
decoded against the tool's JSON schema (string-typed params stay strings).
|
|
478
|
+
|
|
479
|
+
When strict parsing fails, a repair pass runs before giving up: a
|
|
480
|
+
```` ```json ```` fence or prose prefix is stripped, single quotes and
|
|
481
|
+
trailing commas are fixed, truncated braces are closed, a function name
|
|
482
|
+
that landed inside `arguments` is lifted into the envelope, and Python
|
|
483
|
+
literals `True`/`False`/`None` **outside string values** are coerced to
|
|
484
|
+
`true`/`false`/`null` (`pythonLiteralsToJson`, `src/tool-call.ts` — Qwen3.5
|
|
485
|
+
in particular emits Python booleans). The repair is not reported on the
|
|
486
|
+
wire; markup that still fails to parse falls back to plain content.
|
|
307
487
|
|
|
308
488
|
### Structured output (grammar-constrained decoding)
|
|
309
489
|
|
|
310
490
|
Both `/v1/chat/completions` and `/v1/completions` accept the
|
|
311
|
-
OpenAI/oMLX/vLLM structured-output fields (snake_case on the wire
|
|
491
|
+
OpenAI/oMLX/vLLM structured-output fields (snake_case on the wire; the
|
|
492
|
+
field names are `GrammarRequest` in `src/grammar.ts`):
|
|
312
493
|
|
|
313
494
|
```jsonc
|
|
314
495
|
{
|
|
@@ -330,9 +511,19 @@ OpenAI/oMLX/vLLM structured-output fields (snake_case on the wire):
|
|
|
330
511
|
}
|
|
331
512
|
```
|
|
332
513
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
`
|
|
514
|
+
| Field | Compiles to (xgrammar) | Output |
|
|
515
|
+
| --- | --- | --- |
|
|
516
|
+
| `response_format: {type:"json_object"}` | builtin JSON grammar | any valid JSON |
|
|
517
|
+
| `response_format: {type:"json_schema", json_schema:{name, schema, strict?, any_whitespace?}}` | `compileJSONSchema(schema)` | schema-conformant JSON |
|
|
518
|
+
| `guided_grammar` | `Grammar.fromEBNF` | text matching the grammar |
|
|
519
|
+
| `guided_regex` | EBNF-wrapped regex | text matching the regex subset |
|
|
520
|
+
| `guided_choice` | `root ::= ("a" \| "b" \| …)` | exactly one of the strings |
|
|
521
|
+
| `structured_outputs` | bare schema (alias for `json_schema.schema`) | schema-conformant JSON |
|
|
522
|
+
|
|
523
|
+
Precedence when several are set (`resolveGrammarRequest`): `guided_grammar`
|
|
524
|
+
> `response_format` json_schema > json_object > `structured_outputs` >
|
|
525
|
+
`guided_regex` > `guided_choice`. `response_format: {type:"text"}` or no
|
|
526
|
+
field at all means no constraint.
|
|
336
527
|
|
|
337
528
|
Enforcement is a per-step token bitmask at the sampler (xgrammar):
|
|
338
529
|
invalid next tokens are masked to −inf **after** the logits processors,
|
|
@@ -346,32 +537,45 @@ Degrade path — a grammar that fails to compile is never rejected (no
|
|
|
346
537
|
requested JSON schema, raw grammar, regex, or choice constraint, and the
|
|
347
538
|
response carries a `Warning` header (`grammar not enforced: …`);
|
|
348
539
|
`/v1/completions` has no chat template to inject into, so it emits the
|
|
349
|
-
`Warning` header only.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
Design and fidelity notes:
|
|
354
|
-
[structured-output.md](../design/structured-output.md).
|
|
540
|
+
`Warning` header only. `MLX_BUN_GRAMMAR=0` disables mask compilation but
|
|
541
|
+
does not silently ignore a requested constraint: it uses this same degrade
|
|
542
|
+
path with the hint `grammar compilation disabled by MLX_BUN_GRAMMAR=0`.
|
|
543
|
+
Design and fidelity notes: [docs/reference/server-api.md](./server-api.md).
|
|
355
544
|
|
|
356
545
|
### Errors
|
|
357
546
|
|
|
547
|
+
Runtime memory pressure can fail a request even after prompt-length admission.
|
|
548
|
+
The serial memory guard first reclaims older prompt-cache entries, persisting
|
|
549
|
+
them to the SSD tier when configured. If insufficient GPU headroom remains,
|
|
550
|
+
the normal request/stream error path reports the failure. Saved in-flight
|
|
551
|
+
generation checkpoints remain available for an identical retry.
|
|
552
|
+
|
|
358
553
|
All errors are `{ "error": { "message": …, ... } }`.
|
|
359
554
|
|
|
360
|
-
- `400` — malformed JSON, empty `messages
|
|
361
|
-
|
|
362
|
-
without
|
|
363
|
-
|
|
364
|
-
non-
|
|
365
|
-
|
|
555
|
+
- `400` — malformed JSON (`invalid JSON body`), empty `messages`
|
|
556
|
+
(`messages required`), unknown adapter id, vision request on a model
|
|
557
|
+
without a sidecar (`model has no vision sidecar`), audio request on a
|
|
558
|
+
model without an audio tower (message names the model), video on a
|
|
559
|
+
non-Qwen3.5 model or combined with audio, prompt build failures
|
|
560
|
+
(`prompt build failed: …`, which includes media fetch/transcode
|
|
561
|
+
errors), non-numeric `logit_bias` keys/values (`logit_bias must be a
|
|
562
|
+
dict of int to float`, mlx-lm's coercion error), invalid logprobs params
|
|
366
563
|
(mlx-lm's exact validation: `logprobs must be of type bool`,
|
|
367
564
|
`top_logprobs must be of type int` / `at least 0` / `at most 11`;
|
|
368
|
-
`top_logprobs: -1` is the accepted "unset" sentinel)
|
|
565
|
+
`top_logprobs: -1` is the accepted "unset" sentinel), invalid
|
|
566
|
+
`reasoning_effort` (`reasoning_effort must be one of 'none', 'minimal',
|
|
567
|
+
'low', 'medium', 'high', 'xhigh'`).
|
|
369
568
|
- `400` with `"type": "memory_admission"`, `"code":
|
|
370
569
|
"context_over_budget"` — the PROMPT itself leaves no generation slot
|
|
371
|
-
within the max safe context. A prompt that
|
|
372
|
-
broad `max_tokens`: the upper bound is
|
|
373
|
-
(`max_tokens` is a ceiling, not a promise)
|
|
374
|
-
ceiling is visible at `/stats`.
|
|
570
|
+
within the max safe context (`src/serve/request-plan.ts`). A prompt that
|
|
571
|
+
fits is never rejected for a broad `max_tokens`: the upper bound is
|
|
572
|
+
capped to the remaining room (`max_tokens` is a ceiling, not a promise)
|
|
573
|
+
and generation proceeds. The ceiling is visible at `/stats`.
|
|
574
|
+
- `429` with `"type": "resource_admission"`, `"code": "queue_full"` when
|
|
575
|
+
a preparation or generation queue has 64 waiting requests. If SSE headers
|
|
576
|
+
are already sent, the stream emits its terminal error instead.
|
|
577
|
+
- `500` — a generation failure after admission; the stack is logged
|
|
578
|
+
server-side, the body keeps only the message.
|
|
375
579
|
|
|
376
580
|
## POST /v1/completions (raw text completion)
|
|
377
581
|
|
|
@@ -383,17 +587,19 @@ and adapter selection as chat.
|
|
|
383
587
|
|
|
384
588
|
```jsonc
|
|
385
589
|
{
|
|
386
|
-
"prompt": "Once upon a time", // REQUIRED, string only (token
|
|
387
|
-
// rejected, matching mlx_lm.server)
|
|
388
|
-
"max_tokens": 512, //
|
|
389
|
-
//
|
|
590
|
+
"prompt": "Once upon a time", // REQUIRED, non-empty string only (token
|
|
591
|
+
// arrays rejected, matching mlx_lm.server)
|
|
592
|
+
"max_tokens": 512, // unset = no cap: generate until EOS or the
|
|
593
|
+
// admitted context (DEVIATION: mlx_lm.server
|
|
594
|
+
// defaults 512 — pass --max-tokens 512 to
|
|
595
|
+
// reproduce); max_completion_tokens wins
|
|
390
596
|
"stream": false,
|
|
391
597
|
"stop": "\n\n", // string or array, decoded-text matching
|
|
392
598
|
// plus every sampling/penalty field from /v1/chat/completions:
|
|
393
599
|
// temperature, top_p, top_k, seed, min_p, xtc_probability,
|
|
394
600
|
// xtc_threshold, logit_bias, repetition_penalty,
|
|
395
601
|
// repetition_context_size, presence_penalty, presence_context_size,
|
|
396
|
-
// frequency_penalty, frequency_context_size, adapter,
|
|
602
|
+
// frequency_penalty, frequency_context_size, hlg, adapter,
|
|
397
603
|
// logprobs (BOOL, mlx-lm's type — not OpenAI's legacy int),
|
|
398
604
|
// top_logprobs (0-11 or -1) — same response block as chat, see
|
|
399
605
|
// "logprobs / top_logprobs" above (mlx-lm shares the builder),
|
|
@@ -411,9 +617,11 @@ Response is the OpenAI text-completion object:
|
|
|
411
617
|
{
|
|
412
618
|
"id": "cmpl-…", "object": "text_completion", "created": 1760000000,
|
|
413
619
|
"model": "<loaded model id>",
|
|
414
|
-
"choices": [{ "index": 0, "text": "…", "finish_reason": "stop" | "length"
|
|
620
|
+
"choices": [{ "index": 0, "text": "…", "finish_reason": "stop" | "length",
|
|
621
|
+
"logprobs": { /* only when requested */ } }],
|
|
415
622
|
"usage": { "prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0,
|
|
416
|
-
"prompt_tokens_details": { "cached_tokens": 0 }
|
|
623
|
+
"prompt_tokens_details": { "cached_tokens": 0 },
|
|
624
|
+
"lane": "serial" }
|
|
417
625
|
// + "speculation" under --draft-model, as on chat
|
|
418
626
|
}
|
|
419
627
|
```
|
|
@@ -423,83 +631,102 @@ Streaming (`"stream": true`) is SSE: `text_completion` chunks whose
|
|
|
423
631
|
`finish_reason` and `usage` (always attached — mlx-lm gates usage behind
|
|
424
632
|
`stream_options.include_usage`; ours is an additive superset), then
|
|
425
633
|
`data: [DONE]`. There is no `echo` parameter (mlx_lm.server has none).
|
|
634
|
+
`400` on a missing/empty/non-string `prompt`, plus the same logit_bias,
|
|
635
|
+
logprobs, and admission errors as chat.
|
|
426
636
|
|
|
427
637
|
## POST /v1/messages (Anthropic Messages API)
|
|
428
638
|
|
|
429
639
|
Anthropic-protocol surface over the same engine — on by default, like
|
|
430
640
|
`optiq serve`. Point any Anthropic-SDK tool at the server
|
|
431
641
|
(`ANTHROPIC_BASE_URL=http://localhost:8080`, any `x-api-key`) — Claude
|
|
432
|
-
Code works as a client this way.
|
|
642
|
+
Code works as a client this way. Translation lives in `src/anthropic.ts`;
|
|
643
|
+
the request funnels into the chat handler above.
|
|
433
644
|
|
|
434
645
|
- `system` (string or text blocks), `messages` with string or
|
|
435
646
|
content-block arrays; `tool_use` / `tool_result` blocks map to the
|
|
436
|
-
native
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
rejected with a `400` pointing
|
|
647
|
+
native tool-calling path; `image` blocks (base64 or url source) hit the
|
|
648
|
+
vision path on sidecar models, including image blocks nested inside a
|
|
649
|
+
`tool_result` (the computer-use shape). Other image source types are a
|
|
650
|
+
`400`. The protocol has no audio input block; audio-shaped blocks
|
|
651
|
+
(`input_audio` / `audio` / `audio_url`) are rejected with a `400` pointing
|
|
652
|
+
to `/v1/chat/completions`. Prior-turn `thinking` blocks are dropped on
|
|
653
|
+
re-ingest.
|
|
441
654
|
- `tools` (`{name, description, input_schema}`) map to function tools;
|
|
442
|
-
server-tool types
|
|
655
|
+
entries without `name` + `input_schema` (server-tool types such as
|
|
656
|
+
web_search) are dropped silently.
|
|
443
657
|
- `max_tokens`, `temperature`, `top_p`, `top_k`, `stop_sequences`,
|
|
444
658
|
`stream` as in the Anthropic spec. The mlx-lm sampler/penalty
|
|
445
659
|
extensions (`min_p`, `xtc_probability`, `xtc_threshold`,
|
|
446
660
|
`repetition_penalty`, `presence_penalty`, `frequency_penalty` + their
|
|
447
661
|
`*_context_size` windows) pass through as extras; `logit_bias` does
|
|
448
662
|
not exist in this protocol and is not accepted here.
|
|
449
|
-
- Response: `{id: "msg_…", type: "message",
|
|
450
|
-
{type: "tool_use"}…], stop_reason,
|
|
451
|
-
output_tokens, cache_read_input_tokens}}` —
|
|
452
|
-
`cache_read_input_tokens`
|
|
663
|
+
- Response: `{id: "msg_…", type: "message", role: "assistant", content:
|
|
664
|
+
[{type: "text"} | {type: "tool_use"}…], stop_reason, stop_sequence,
|
|
665
|
+
usage: {input_tokens, output_tokens, cache_read_input_tokens?}}` —
|
|
666
|
+
`cache_read_input_tokens` is present when the prompt cache contributed.
|
|
453
667
|
- Streaming follows the Anthropic event grammar exactly:
|
|
454
668
|
`message_start → content_block_start/delta/stop (text_delta,
|
|
455
669
|
input_json_delta) → message_delta (stop_reason + usage) →
|
|
456
670
|
message_stop`. Errors are `event: error` frames.
|
|
457
671
|
- Errors: `{type: "error", error: {type: "invalid_request_error" |
|
|
458
|
-
"api_error", message}}
|
|
672
|
+
"api_error", message}}` — the chat handler's `4xx`/`5xx` are re-wrapped
|
|
673
|
+
into this shape.
|
|
459
674
|
|
|
460
675
|
## POST /v1/responses (OpenAI Responses API)
|
|
461
676
|
|
|
462
677
|
Responses-protocol surface (Codex, Cursor, Continue, Cline, and the
|
|
463
|
-
OpenAI SDK speak this now). Oracle: optiq responses shim
|
|
678
|
+
OpenAI SDK speak this now). Oracle: optiq responses shim; port in
|
|
679
|
+
`src/responses.ts`.
|
|
464
680
|
|
|
465
681
|
- `input` (string or item array: `message`, `function_call`,
|
|
466
682
|
`function_call_output`), `instructions` (merged with any
|
|
467
683
|
system/developer items into one leading system message),
|
|
468
|
-
`max_output_tokens
|
|
469
|
-
`tools`/`tool_choice` (built-in tool types
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
`
|
|
473
|
-
|
|
684
|
+
`max_output_tokens` (or `max_tokens`), `temperature`, `top_p`, `top_k`,
|
|
685
|
+
flat `tools`/`tool_choice` (built-in tool types — web_search, file_search,
|
|
686
|
+
mcp, computer_use — dropped silently; `{type:"function", name}` maps to
|
|
687
|
+
the chat object form, `"auto"`/`"none"`/`"required"` pass through),
|
|
688
|
+
`stream`. The mlx-lm sampler/penalty extensions (`min_p`,
|
|
689
|
+
`xtc_probability`, `xtc_threshold`, `logit_bias`, `repetition_penalty`,
|
|
690
|
+
`presence_penalty`, `frequency_penalty` + `*_context_size`) pass through
|
|
691
|
+
as extras.
|
|
474
692
|
- **`previous_response_id` resumption**: pass a prior response id
|
|
475
693
|
instead of resending the conversation; the server splices the stored
|
|
476
|
-
input + output back in (instructions carry forward when omitted
|
|
477
|
-
Store is per-process, 1 h TTL,
|
|
478
|
-
at `GET /stats` (`response_store`).
|
|
694
|
+
input + output back in (instructions carry forward when omitted;
|
|
695
|
+
reasoning items are not replayed). Store is per-process, 1 h TTL,
|
|
696
|
+
32 MiB byte-capped LRU — observable at `GET /stats` (`response_store`).
|
|
697
|
+
Unknown/expired id → `404`.
|
|
479
698
|
- Response: `{id: "resp_…", object: "response", status: "completed" |
|
|
480
|
-
"incomplete", output: [{type: "
|
|
699
|
+
"incomplete", output: [{type: "reasoning", summary: [{type:
|
|
700
|
+
"summary_text", text}]}?, {type: "message"}, {type: "function_call"}…],
|
|
701
|
+
usage: {input_tokens, output_tokens, total_tokens,
|
|
702
|
+
output_tokens_details: {reasoning_tokens}}, previous_response_id}`. A
|
|
703
|
+
`reasoning` item is present only when the model emitted a think channel.
|
|
481
704
|
- Streaming event chain: `response.created → response.in_progress →
|
|
482
705
|
response.output_item.added → response.content_part.added →
|
|
483
706
|
response.output_text.delta… → response.output_text.done →
|
|
484
707
|
response.content_part.done → response.output_item.done →
|
|
485
708
|
response.completed` (+ `response.function_call_arguments.delta/.done`
|
|
486
|
-
for tool calls
|
|
709
|
+
for tool calls; a reasoning output item precedes the message when the
|
|
710
|
+
model thinks).
|
|
711
|
+
- Errors: `{error: {message, type: "invalid_request_error" |
|
|
712
|
+
"server_error", param: null, code: null}}`.
|
|
487
713
|
|
|
488
714
|
## POST /v1/embeddings (OpenAI Embeddings API)
|
|
489
715
|
|
|
490
716
|
Text embeddings, available when the **served model is an embedding model**
|
|
491
717
|
(plain Qwen3 / Qwen3-Embedding) — consistent with the single-model server:
|
|
492
718
|
`mlx-bun serve mlx-community/Qwen3-Embedding-4B-4bit-DWQ`, then POST here. On
|
|
493
|
-
any other served model the route returns `400 invalid_request_error
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
719
|
+
any other served model the route returns `400 invalid_request_error`
|
|
720
|
+
(`src/serve/model-admin-routes.ts`).
|
|
721
|
+
|
|
722
|
+
- `input` (string or array of strings; anything else is a `400`), `model`
|
|
723
|
+
(ignored — the served model is used), and a **non-standard**
|
|
724
|
+
`instruction` (optional): applies Qwen3-Embedding's query format
|
|
725
|
+
`Instruct: {instruction}\nQuery:{text}`, which steers *which* similarity
|
|
726
|
+
axis the geometry reflects. Omit it for plain document embeddings.
|
|
500
727
|
- Pooling is last-token + L2-normalization (the vectors are unit-length);
|
|
501
728
|
similarity is the dot product. Bit-exact vs the mlx-lm `qwen3` reference
|
|
502
|
-
(`tests/qwen3-embed-parity.test.ts`).
|
|
729
|
+
(`tests/parity/qwen3-embed-parity.test.ts`).
|
|
503
730
|
- Response: `{ "object": "list", "data": [{ "object": "embedding", "index":
|
|
504
731
|
0, "embedding": [float…] }, …], "model": "<served id>", "usage": {
|
|
505
732
|
"prompt_tokens": N, "total_tokens": N } }`. Embedding is a single forward
|
|
@@ -510,16 +737,31 @@ curl localhost:8080/v1/embeddings -H 'content-type: application/json' \
|
|
|
510
737
|
-d '{"input": ["the cat sat on the mat", "a kitten by the window"]}'
|
|
511
738
|
```
|
|
512
739
|
|
|
740
|
+
## GET /v1
|
|
741
|
+
|
|
742
|
+
API index for discovery tooling:
|
|
743
|
+
|
|
744
|
+
```jsonc
|
|
745
|
+
{ "name": "mlx-bun", "version": "<package version>", "model": "<served model id>",
|
|
746
|
+
"endpoints": ["POST /v1/chat/completions", "POST /v1/completions", "POST /v1/messages",
|
|
747
|
+
"POST /v1/responses", "POST /v1/embeddings", "GET /v1/models",
|
|
748
|
+
"GET/POST/DELETE /v1/adapters", "GET /health", "GET /stats", "GET /fit",
|
|
749
|
+
"GET /library", "GET /downloads"] }
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
(The list is a curated subset; this document is the full inventory.)
|
|
753
|
+
|
|
513
754
|
## GET /v1/models
|
|
514
755
|
|
|
515
756
|
`{ "object": "list", "data": [{ "id": "<model id>", "object": "model",
|
|
516
|
-
"created": <unix s>, … }] }`
|
|
757
|
+
"created": <unix s>, … }] }` — `created` is the server start time for every
|
|
758
|
+
row.
|
|
517
759
|
|
|
518
760
|
The served model is FIRST, with extra capability fields:
|
|
519
761
|
|
|
520
762
|
```jsonc
|
|
521
763
|
{
|
|
522
|
-
"id": "<served model id>", "object": "model", "created":
|
|
764
|
+
"id": "<served model id>", "object": "model", "created": 1760000000,
|
|
523
765
|
"owned_by": "mlx-bun",
|
|
524
766
|
"context_window": 32768,
|
|
525
767
|
"reasoning": false, // switchable thinking channel (gates the
|
|
@@ -560,7 +802,7 @@ architectures, one row per repo via `listCanonical()` — duplicate snapshots
|
|
|
560
802
|
from upstream re-pushes are not separate entries):
|
|
561
803
|
|
|
562
804
|
```jsonc
|
|
563
|
-
{ "id": "<repo id>", "object": "model", "created":
|
|
805
|
+
{ "id": "<repo id>", "object": "model", "created": 1760000000,
|
|
564
806
|
"vision": false, // visionCapable(): sidecar OR encoder-free
|
|
565
807
|
// unified vision config
|
|
566
808
|
"tier": "targeted" | "generic" // supportTier(): dedicated/generated forward
|
|
@@ -596,7 +838,7 @@ vision/audio, adapters, and training are not emulated by the serving port.
|
|
|
596
838
|
"server": { "owner": "serve" | "pi-session" | "embedded", "model": "...", "started_at": 0 },
|
|
597
839
|
"prompt_cache": { "entries": 0, "bytes": 0, "max_bytes": 0, "hits": 0, "misses": 0 },
|
|
598
840
|
"response_store": { "entries": 0, "bytes": 0, "max_bytes": 33554432, "ttl_ms": 3600000 },
|
|
599
|
-
"kv_quant": { "mode": "
|
|
841
|
+
"kv_quant": { "mode": "bf16" | "uniform-kv8" | "turbo k8v3" | "mixed (kv_config.json)",
|
|
600
842
|
"layers": { "kv4": 8, "bf16": 40 }, // turbo: { "turbo-k8v3": 8, "bf16": 40 }
|
|
601
843
|
"attention": { "global": 10, "sliding_window": 38 } },
|
|
602
844
|
// present only when --ssd-cache is on:
|
|
@@ -626,18 +868,13 @@ vision/audio, adapters, and training are not emulated by the serving port.
|
|
|
626
868
|
"resident_weight_bytes": 0,
|
|
627
869
|
"main_expert_slab_bytes": 0,
|
|
628
870
|
"mtp_expert_slab_bytes": 0,
|
|
629
|
-
"expert_runtime": {
|
|
871
|
+
"expert_runtime": { // null when the expert runtime is off
|
|
630
872
|
"main_residency": { "resident": 0, "pinned": 0, "working": 0,
|
|
631
873
|
"hits": 0, "misses": 0, "evictions": 0 },
|
|
632
|
-
"mtp_residency": {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
"readBytes": 0 } },
|
|
637
|
-
"mtp": { "demand": { "hits": 0, "misses": 0,
|
|
638
|
-
"readBytes": 0 } },
|
|
639
|
-
"pilot": null
|
|
640
|
-
},
|
|
874
|
+
"mtp_residency": { /* same shape */ } | null,
|
|
875
|
+
"last_turn": { "main": { "demand": { "hits": 0, "misses": 0, "readBytes": 0 } },
|
|
876
|
+
"mtp": { "demand": { "hits": 0, "misses": 0, "readBytes": 0 } },
|
|
877
|
+
"pilot": null },
|
|
641
878
|
"last_repin": []
|
|
642
879
|
}
|
|
643
880
|
},
|
|
@@ -655,16 +892,122 @@ vision/audio, adapters, and training are not emulated by the serving port.
|
|
|
655
892
|
}
|
|
656
893
|
```
|
|
657
894
|
|
|
895
|
+
`kv_quant.mode` reflects the `--kv-quant` flag the server was started
|
|
896
|
+
with; the default is `bf16` (no KV quantization). `mixed (kv_config.json)`
|
|
897
|
+
appears only when `--kv-quant config` was passed explicitly and the
|
|
898
|
+
checkpoint ships a per-layer `kv_config.json` — per-layer KV quantization
|
|
899
|
+
is never selected automatically.
|
|
900
|
+
|
|
658
901
|
`pending_snapshots` counts prompt-cache entries scheduled for persistence but
|
|
659
902
|
not yet confirmed by the atomic SSD store. `longest_durable_prefix_tokens` is
|
|
660
903
|
the longest prefix already present in the SSD index, not merely queued.
|
|
661
904
|
|
|
905
|
+
## GET /fit
|
|
906
|
+
|
|
907
|
+
Fit assessment for the loaded model on this machine, plus a capability
|
|
908
|
+
matrix across Apple SKUs. Used by the status page. Experts bytes come
|
|
909
|
+
from the registry so MoE active-parameter predictions match the serve
|
|
910
|
+
banner and `mlx-bun fit`. When the eval DB has a real measured decode
|
|
911
|
+
rate for this model snapshot, it is included and takes precedence over
|
|
912
|
+
the prediction.
|
|
913
|
+
|
|
914
|
+
For the direct Colibri GLM-5.2 runtime this endpoint switches to the exact
|
|
915
|
+
serve-time memory plan. The response separates the full streamed artifact on
|
|
916
|
+
disk from resident weights, main/MTP expert slabs, KV, and other reserves.
|
|
917
|
+
`measured_decode_tps` is the G5 warm quality-preserving measurement;
|
|
918
|
+
`glm52.direct_oracle_warm_decode_tps` is the same-machine direct Colibri
|
|
919
|
+
control, while `glm52.aspirational_decode_tps` is a target, not a release
|
|
920
|
+
gate. The single-row SKU result describes the current machine because the
|
|
921
|
+
generic all-resident SKU estimator is inapplicable
|
|
922
|
+
(`report.predicted_decode_tps` is `null` there).
|
|
923
|
+
|
|
924
|
+
```jsonc
|
|
925
|
+
{
|
|
926
|
+
"machine": { "chip": "M4 Pro", "ram_bytes": 0, "bandwidth_gbs": 0.0 },
|
|
927
|
+
"context_tokens": 8192, // current admission ceiling
|
|
928
|
+
"typical_context_tokens": 8192, // min(8192, context_tokens)
|
|
929
|
+
"typical_decode_tps": 0.0, // predicted at typical_context_tokens
|
|
930
|
+
"measured_decode_tps": null, // real number from eval DB, or null
|
|
931
|
+
"measured_at": null, // unix ms of measurement, or null
|
|
932
|
+
"report": {
|
|
933
|
+
"fits": true,
|
|
934
|
+
"weights_bytes": 0,
|
|
935
|
+
"kv_bytes": 0,
|
|
936
|
+
"transient_bytes": 0,
|
|
937
|
+
"total_bytes": 0,
|
|
938
|
+
"usable_bytes": 0,
|
|
939
|
+
"max_safe_context": 8192,
|
|
940
|
+
"predicted_decode_tps": 0.0
|
|
941
|
+
},
|
|
942
|
+
// GLM-5.2 only:
|
|
943
|
+
"glm52": {
|
|
944
|
+
"artifact_disk_bytes": 0, // exact registry size; null if unavailable
|
|
945
|
+
"main_expert_slab_bytes": 0,
|
|
946
|
+
"mtp_expert_slab_bytes": 0,
|
|
947
|
+
"max_generation_tokens": 128,
|
|
948
|
+
"direct_oracle_warm_decode_tps": 0.27,
|
|
949
|
+
"aspirational_decode_tps": 2.0
|
|
950
|
+
},
|
|
951
|
+
"sku_matrix_ctx": 32768,
|
|
952
|
+
"sku_matrix": [{
|
|
953
|
+
"sku": "M4 Pro 24 GB", "ram_gb": 24,
|
|
954
|
+
"fits": true, "max_context": 32768, "decode_tps": 0.0
|
|
955
|
+
}]
|
|
956
|
+
}
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
## Adapters (LoRA hot-swap)
|
|
960
|
+
|
|
961
|
+
Routes in `src/serve/model-admin-routes.ts`; the manager is
|
|
962
|
+
`AdapterManager` in `src/lora.ts`.
|
|
963
|
+
|
|
964
|
+
- `GET /v1/adapters` — `{ adapters: [{ id, path, rank, scale, size_bytes,
|
|
965
|
+
mounted_layers, ram_bytes }] }` — currently-mounted adapters only.
|
|
966
|
+
`ram_bytes` is the actual resident size of the adapter's mounted
|
|
967
|
+
`lora_a`/`lora_b` arrays (summed `MlxArray.nbytes` — real RAM cost while
|
|
968
|
+
mounted, not a guess from the on-disk file size).
|
|
969
|
+
- `GET /v1/adapters/available` — `{ adapters: [{ id, path, rank, scale,
|
|
970
|
+
base_model, mounted, compatible }] }` — every adapter found on disk
|
|
971
|
+
(`~/.cache/mlx-bun-finetunes`, `~/.cache/mlx-bun/adapters`), unfiltered.
|
|
972
|
+
`compatible` is true when the adapter's recorded base model matches the
|
|
973
|
+
currently-served model (compared by bare repo name) or the adapter
|
|
974
|
+
recorded no base at all; `mounted` is true when it's already loaded.
|
|
975
|
+
The web chat's adapter chip uses `compatible` to gray out entries it
|
|
976
|
+
won't let you select rather than hiding them.
|
|
977
|
+
- `POST /v1/adapters` — `{ "id": "...", "path": "/dir" }` (both required,
|
|
978
|
+
else `400 "id and path required"`); mounts through the generation queue
|
|
979
|
+
(never races a forward pass). Response:
|
|
980
|
+
`{ id, mounted_layers, rank, scale, ram_bytes }`. `400` on shape/compat
|
|
981
|
+
mismatch — validation is all-or-nothing.
|
|
982
|
+
- `DELETE /v1/adapters/<id>` — unmount; `{ id, removed_layers }` on
|
|
983
|
+
success, `404 {"error":{"message":"adapter <id> not mounted"}}` otherwise.
|
|
984
|
+
|
|
985
|
+
Select per request with the `adapter` body field (chat, text completions;
|
|
986
|
+
the Anthropic and Responses surfaces pass it through as an extra), which
|
|
987
|
+
also accepts a composed spec — `"a+b"` (or `"a,b"`) stacks two mounted
|
|
988
|
+
adapters, their LoRA residuals summed in order
|
|
989
|
+
(`AdapterManager.resolveSpec` / `parseAdapterSpec` in `src/lora.ts`);
|
|
990
|
+
every named id must already be mounted or the request 400s with the
|
|
991
|
+
unknown id named. Prompt-cache entries are namespaced per adapter spec
|
|
992
|
+
(including composed ones), so switching adapters — or switching which
|
|
993
|
+
ones are stacked — never reuses another combination's KV.
|
|
994
|
+
|
|
995
|
+
`serve --adapter <dir>` (alias `--adapter-path`, mlx_lm.server's
|
|
996
|
+
spelling) mounts an adapter at startup through this same machinery and
|
|
997
|
+
makes it the default for requests that send no `adapter` field; an
|
|
998
|
+
explicit `adapter` (including `"none"`) always wins, and hot-swap via
|
|
999
|
+
these endpoints is unchanged.
|
|
1000
|
+
|
|
662
1001
|
## POST /admin/cache/flush
|
|
663
1002
|
|
|
664
1003
|
Forces every pending SSD prompt-cache snapshot through the serial write queue
|
|
665
1004
|
and waits for the atomic temp-file/fsync/rename boundary. The response is `200`
|
|
666
1005
|
only when no snapshot or spill remains pending and no write failed during this
|
|
667
1006
|
flush; otherwise it is `503` with the counters that explain why.
|
|
1007
|
+
A snapshot missing from both RAM and usable SSD coverage remains pending
|
|
1008
|
+
across repeated flushes. Retrying does not turn a missing snapshot into a
|
|
1009
|
+
successful durability result. Already-durable prefixes avoid another clone
|
|
1010
|
+
and write.
|
|
668
1011
|
|
|
669
1012
|
```jsonc
|
|
670
1013
|
{
|
|
@@ -685,13 +1028,16 @@ flush; otherwise it is `503` with the counters that explain why.
|
|
|
685
1028
|
The standard benchmark calls this endpoint before its restart leg and records
|
|
686
1029
|
the result. Normal `mlx-bun serve` shutdown on `SIGINT` or `SIGTERM` performs
|
|
687
1030
|
the same flush after active requests drain, with a 120-second default timeout.
|
|
1031
|
+
Without `--ssd-cache` the route still answers (`entries: 0`).
|
|
688
1032
|
|
|
689
1033
|
## GET /library
|
|
690
1034
|
|
|
1035
|
+
`?refresh=1` bypasses the short response cache, including after a parent-managed job finishes.
|
|
1036
|
+
|
|
691
1037
|
Returns all models found in the local HuggingFace hub cache (via the
|
|
692
1038
|
registry scan), each annotated with a fit assessment for this machine.
|
|
693
1039
|
Response is cached for 30 seconds (registry scan + config reads; no
|
|
694
|
-
tensor bytes are read).
|
|
1040
|
+
tensor bytes are read). Handler: `src/serve/discovery-routes.ts`.
|
|
695
1041
|
|
|
696
1042
|
```jsonc
|
|
697
1043
|
{
|
|
@@ -720,6 +1066,203 @@ tensor bytes are read).
|
|
|
720
1066
|
One row per repo (`listCanonical()`; duplicate snapshots from upstream
|
|
721
1067
|
re-pushes surface only via the CLI's `mlx-bun ls --all-revisions`).
|
|
722
1068
|
|
|
1069
|
+
## GET /downloads
|
|
1070
|
+
|
|
1071
|
+
Snapshot of the last 5 model downloads (active, completed, or errored)
|
|
1072
|
+
initiated via `mlx-bun download`, the web library panel, or
|
|
1073
|
+
`POST /api/hub/download`.
|
|
1074
|
+
|
|
1075
|
+
```jsonc
|
|
1076
|
+
{
|
|
1077
|
+
"downloads": [{
|
|
1078
|
+
"repo_id": "…",
|
|
1079
|
+
"state": "active" | "done" | "error",
|
|
1080
|
+
"current_file": "model.safetensors" | null,
|
|
1081
|
+
"received_bytes": 0,
|
|
1082
|
+
"total_bytes": 0,
|
|
1083
|
+
"files_done": 0,
|
|
1084
|
+
"files_total": 0,
|
|
1085
|
+
"bytes_per_sec": 0, // rolling ~5 s window
|
|
1086
|
+
"started_at": 1760000000,
|
|
1087
|
+
"finished_at": null,
|
|
1088
|
+
"error": "…" // present on state "error"
|
|
1089
|
+
}]
|
|
1090
|
+
}
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
## Jobs (`/api/jobs`)
|
|
1094
|
+
|
|
1095
|
+
The web wizards (quantize, fine-tune, dataset) run as jobs: a SQLite row
|
|
1096
|
+
plus an NDJSON event log (`src/jobs/`). Submit routes return a `job_id`;
|
|
1097
|
+
these routes read progress. Loopback-served admin routes, no separate auth
|
|
1098
|
+
(same posture as every other `/api/*` route). Handler:
|
|
1099
|
+
`src/serve/admin-routes.ts`.
|
|
1100
|
+
|
|
1101
|
+
`GET /api/jobs?limit=50&kind=quantize|finetune|dataset` — most recent
|
|
1102
|
+
jobs, newest first: `{ "ok": true, "jobs": [JobRow, …] }`.
|
|
1103
|
+
|
|
1104
|
+
`GET /api/jobs/<id>` — one row, or `404 { "ok": false, "error": "job not
|
|
1105
|
+
found" }`:
|
|
1106
|
+
|
|
1107
|
+
```jsonc
|
|
1108
|
+
{ "ok": true, "job": {
|
|
1109
|
+
"id": "…", "kind": "quantize" | "finetune" | "dataset",
|
|
1110
|
+
"status": "queued" | "running" | "done" | "failed" | "zombie",
|
|
1111
|
+
"config_json": "{…}", // the submit body as stored
|
|
1112
|
+
"progress": 0.0, // 0..1, from the latest stage/metric event
|
|
1113
|
+
"message": "…" | null,
|
|
1114
|
+
"log_path": "…", "output_path": "…" | null, "error": "…" | null,
|
|
1115
|
+
"started_at": "<ISO>", "ended_at": "<ISO>" | null
|
|
1116
|
+
} }
|
|
1117
|
+
```
|
|
1118
|
+
|
|
1119
|
+
`GET /api/jobs/<id>/stream` — EventSource-compatible SSE tail of the job's
|
|
1120
|
+
log: a `retry: 1500` line first, then one `data: <JobEvent>` frame per
|
|
1121
|
+
event, polling while the job is non-terminal, then `event: end` +
|
|
1122
|
+
`data: {}` and close. An unknown id streams a single `failed` event then
|
|
1123
|
+
`end`. Events:
|
|
1124
|
+
|
|
1125
|
+
```jsonc
|
|
1126
|
+
{ "type": "started", "ts": 0 }
|
|
1127
|
+
{ "type": "log", "line": "…" }
|
|
1128
|
+
{ "type": "stage", "stage": "…", "progress": 0.5, "message": "…" }
|
|
1129
|
+
{ "type": "metric", "kind": "train" | "val", "step": 0, "loss": 0.0 } // + grad_norm, learning_rate, tokens_per_sec, …
|
|
1130
|
+
{ "type": "done", "ts": 0, "output_dir": "…", "summary": { } }
|
|
1131
|
+
{ "type": "failed", "error": "…", "ts": 0 }
|
|
1132
|
+
```
|
|
1133
|
+
|
|
1134
|
+
## Fine-tune (`/api/finetune/*`)
|
|
1135
|
+
|
|
1136
|
+
The HTTP entry point to LoRA training (`src/serve/lab-routes.ts` →
|
|
1137
|
+
`src/train/job.ts`). It runs the same runner the CLI's `mlx-bun train`
|
|
1138
|
+
drives, as a subprocess job (crash isolation + one GPU lease across
|
|
1139
|
+
concurrent server requests). Data formats, every config knob, and the
|
|
1140
|
+
methodology are in [training.md](./training.md); this section is the wire
|
|
1141
|
+
shape.
|
|
1142
|
+
|
|
1143
|
+
`POST /api/finetune/submit` — body is `FinetuneSubmit` (`src/train/job.ts`).
|
|
1144
|
+
`model_dir` and `data_dir` are required at the route (`400 "model_dir and
|
|
1145
|
+
data_dir required"`); `adapter_path` defaults to
|
|
1146
|
+
`~/.cache/mlx-bun/adapters/adapter-<timestamp>` when omitted. Everything
|
|
1147
|
+
else is optional and falls back to `DEFAULT_TRAIN_CONFIG`:
|
|
1148
|
+
|
|
1149
|
+
```jsonc
|
|
1150
|
+
{
|
|
1151
|
+
"model_dir": "/path/to/snapshot", // required
|
|
1152
|
+
"data_dir": "/path/to/dataset", // required: train.jsonl (+ optional valid.jsonl)
|
|
1153
|
+
"adapter_path": "/path/to/output", // optional (default above)
|
|
1154
|
+
"method": "sft" | "dpo" | "orpo",
|
|
1155
|
+
"rank": 16, "scale": 20, "rank_scaling": "…", "target_modules": ["…"], "num_layers": 0,
|
|
1156
|
+
"iters": 300, "learning_rate": 2e-4, "max_seq_length": 2048, "batch_size": 1,
|
|
1157
|
+
"grad_accumulation_steps": 1, "seed": 0, "steps_per_report": 1, "steps_per_eval": 0,
|
|
1158
|
+
"weight_decay": 0.0, "lora_dropout": 0.0, "rs_lora": false, "lora_plus_ratio": 1,
|
|
1159
|
+
"grad_checkpoint": false, "mlp_split": false, "segment_size": 0,
|
|
1160
|
+
"save_checkpoints": true, "grad_clip_norm": 0, "val_max_examples": 0,
|
|
1161
|
+
"dpo_beta": 0.1, "dpo_warmup_iters": 0, "dpo_lr_schedule": "constant" | "cosine",
|
|
1162
|
+
"orpo_lambda": 0.1, "orpo_warmup_iters": 0, "orpo_lr_schedule": "constant" | "cosine",
|
|
1163
|
+
"orpo_chunk_size": 0, "orpo_fused_ce": true, "orpo_flash_ce": true, "orpo_prefix_shared": true,
|
|
1164
|
+
"sft_scope": "full" | "response",
|
|
1165
|
+
"warm_start_adapter": "/path/to/adapter"
|
|
1166
|
+
}
|
|
1167
|
+
```
|
|
1168
|
+
|
|
1169
|
+
Response: `{ "ok": true, "job_id": "…", "adapter_path": "…" }`. Then
|
|
1170
|
+
follow `GET /api/jobs/<job_id>/stream` for `metric` events (`kind:
|
|
1171
|
+
"train"|"val"`, `step`, `loss`, …) and the terminal `done`/`failed`.
|
|
1172
|
+
Config validation errors (an invalid `sft_scope`, a missing `adapter_path`
|
|
1173
|
+
at the runner) surface as the job's `failed` event, not as a `4xx`.
|
|
1174
|
+
|
|
1175
|
+
```bash
|
|
1176
|
+
curl -s localhost:8080/api/finetune/submit -X POST -H 'content-type: application/json' -d '{
|
|
1177
|
+
"model_dir": "/path/to/snapshot", "data_dir": "/path/to/dataset",
|
|
1178
|
+
"adapter_path": "/path/to/output-adapter",
|
|
1179
|
+
"method": "sft", "rank": 16, "iters": 300, "learning_rate": 2e-4, "max_seq_length": 2048
|
|
1180
|
+
}'
|
|
1181
|
+
# → {"ok":true,"job_id":"…","adapter_path":"/path/to/output-adapter"}
|
|
1182
|
+
curl -N localhost:8080/api/jobs/<job_id>/stream
|
|
1183
|
+
```
|
|
1184
|
+
|
|
1185
|
+
`POST /api/finetune/inspect-dataset` with `{"path": "<data_dir>"}` — probe
|
|
1186
|
+
a dataset directory before submitting: `{ "ok": true, "n_train": N,
|
|
1187
|
+
"n_valid": N, "format": "messages" | "prompt-completion" | "text" | "dpo" |
|
|
1188
|
+
"unknown" }`, or `{ "ok": false, …, "error": "<train.jsonl> not found" }`
|
|
1189
|
+
(HTTP 200 either way).
|
|
1190
|
+
|
|
1191
|
+
`POST /api/finetune/merge` with `{"adapter_a": "/dir", "adapter_b":
|
|
1192
|
+
"/dir", "scales"?: [a, b]}` — fold two adapters into one (`mergeAdapters`
|
|
1193
|
+
in `src/train/merge.ts`; per-source scales default to each adapter's own
|
|
1194
|
+
config). Output goes to `~/.cache/mlx-bun/adapters/merged-<timestamp>`:
|
|
1195
|
+
`{ "ok": true, "merged_path": "…", "stats": { "layersMerged", "layersOnlyInOne",
|
|
1196
|
+
"totalKeysOut", "sources", "scales" } }`; `400 { "ok": false, "error" }` on
|
|
1197
|
+
a missing field or merge failure.
|
|
1198
|
+
|
|
1199
|
+
`POST /api/finetune/export` with `{"base_model": "…", "adapter_path":
|
|
1200
|
+
"/dir", "method"?: "…"}` — writes a `manifest.json` describing the adapter
|
|
1201
|
+
into `~/.cache/mlx-bun/exports/export-<timestamp>`:
|
|
1202
|
+
`{ "ok": true, "export_path": "…", "manifest": { "version": 1, "base_model",
|
|
1203
|
+
"adapter_path", "method"?, "created_at" } }`; `400` on a missing field.
|
|
1204
|
+
|
|
1205
|
+
## Quantize (`/api/quantize/*`)
|
|
1206
|
+
|
|
1207
|
+
`POST /api/quantize/inspect` with `{"model_id": "<repo id | path>"}` —
|
|
1208
|
+
resolve a model through the registry and report whether the quantizer can
|
|
1209
|
+
walk it (`inspectModel`, `src/quantize/job.ts`): `{ "ok": true, "model_id",
|
|
1210
|
+
"arch": "<model_type>" | null, "support": true, "size_gb": 0.0 }`, or
|
|
1211
|
+
`{ "ok": false, …, "error" }` (HTTP 200 either way).
|
|
1212
|
+
|
|
1213
|
+
`POST /api/quantize/submit` — starts a quantize job whose output lands in
|
|
1214
|
+
the HF hub cache as a synthetic repo `<org>/<name>-OptiQ-<suffix>`
|
|
1215
|
+
(`refs/main` written so the registry picks it up; `<suffix>` is
|
|
1216
|
+
`<bits>bit` or `mixed-<target_bpw>bpw`, plus `-rot<seed>` when rotating):
|
|
1217
|
+
|
|
1218
|
+
```jsonc
|
|
1219
|
+
{
|
|
1220
|
+
"model_id": "org/name | /path", // required (400 "model_id required")
|
|
1221
|
+
"bits": 4, "group_size": 64,
|
|
1222
|
+
"target_bpw": 3.5, // mixed-precision target; wins over bits in the name
|
|
1223
|
+
"candidate_bits": [2, 3, 4, 8],
|
|
1224
|
+
"reference": "…", "calibration_mix": "…", "n_calibration": 0,
|
|
1225
|
+
"rotate_weights": false, "rotation_seed": 42
|
|
1226
|
+
}
|
|
1227
|
+
```
|
|
1228
|
+
|
|
1229
|
+
Response: `{ "ok": true, "job_id": "…", "output_dir": "…" }`. The library
|
|
1230
|
+
cache is invalidated when the job completes.
|
|
1231
|
+
|
|
1232
|
+
`POST /api/quantize/resolve-folder` and `POST /api/model/resolve-folder`
|
|
1233
|
+
(same handler) with `{"folder_name"?: "…", "rel_path"?: "…"}` — turn a
|
|
1234
|
+
folder the browser picked (a `models--org--name` hub dir, a snapshot hash
|
|
1235
|
+
dir, or a bare model dir under the hub cache / `~/.cache/mlx-bun`) into a
|
|
1236
|
+
servable path: `{ "ok": true, "path": "…", "repo_id"?: "…", "model_type"?:
|
|
1237
|
+
"…" }`, or `{ "ok": false, "error": "Couldn't locate this folder on disk —
|
|
1238
|
+
paste the path instead." }` (HTTP 200).
|
|
1239
|
+
|
|
1240
|
+
## Dataset (`/api/dataset/*`)
|
|
1241
|
+
|
|
1242
|
+
`GET /api/dataset/templates` — the dataset-builder template registry
|
|
1243
|
+
(`src/dataset/registry.ts`): `{ "templates": [{ "id", "label",
|
|
1244
|
+
"description", "output_format": "messages" | "prompt_completion" | "dpo" |
|
|
1245
|
+
"text", "needs_llm": bool, "fields": [{ "name", "type": "text" | "textarea" |
|
|
1246
|
+
"number", "label", "hint"?, "required"?, "default"? }] }] }`.
|
|
1247
|
+
|
|
1248
|
+
`POST /api/dataset/submit` with `{"template_id": "…", "inputs": {…},
|
|
1249
|
+
"model_name"?: "…"}` — runs the template in-process as a job; LLM-backed
|
|
1250
|
+
templates call back into this server's own `/v1` (`api_url` =
|
|
1251
|
+
`http://127.0.0.1:<port>`). Output dir is
|
|
1252
|
+
`~/.cache/mlx-bun/datasets/dataset-<template>-<timestamp>`. Response:
|
|
1253
|
+
`{ "ok": true, "job_id": "…", "output_dir": "…" }`; `400 { "ok": false,
|
|
1254
|
+
"error": "unknown template …" }` for an unknown id.
|
|
1255
|
+
|
|
1256
|
+
## POST /api/{quantize,finetune,dataset}/push
|
|
1257
|
+
|
|
1258
|
+
Upload a job's output (or any folder) to the Hugging Face Hub with the
|
|
1259
|
+
saved write token (`src/hf-push.ts`). Body: `{"repo_id": "org/name",
|
|
1260
|
+
"job_id"?: "…", "source_path"?: "/dir", "private"?: false}` — `repo_id` is
|
|
1261
|
+
required; the source is `source_path`, else the job's `output_path`. The
|
|
1262
|
+
`dataset` variant pushes as a dataset repo, the others as model repos.
|
|
1263
|
+
Response `{ "ok": true, "url": "…" }`; `400 { "ok": false, "error" }` when
|
|
1264
|
+
no token is saved, no source directory resolves, or the upload fails.
|
|
1265
|
+
|
|
723
1266
|
## GET /api/gc/plan · POST /api/gc/execute
|
|
724
1267
|
|
|
725
1268
|
Reclaim disk from superseded snapshots + orphaned blobs in the local HF
|
|
@@ -764,13 +1307,12 @@ Errors follow the same `{ "ok": false, "error": "…" }` shape as the other
|
|
|
764
1307
|
## GET /api/memory/\* · POST /api/memory/init
|
|
765
1308
|
|
|
766
1309
|
Thin loopback JSON wrappers over `src/memory/vault.ts` for the web chat's
|
|
767
|
-
Memory panel
|
|
768
|
-
|
|
769
|
-
`src/memory/
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
a 500 or an unhandled throw.
|
|
1310
|
+
Memory panel — same loopback-only admin-route posture as `/api/gc/*`, no
|
|
1311
|
+
separate auth. Handlers live in `src/memory/rest.ts`; they never touch the
|
|
1312
|
+
agent-tool surface (`src/memory/tools.ts`), which stays read-only. Every
|
|
1313
|
+
route below degrades gracefully — a missing vault, an unknown article, or
|
|
1314
|
+
a bad param returns `{ "ok": false, ... }` (with an appropriate HTTP
|
|
1315
|
+
status), never a 500.
|
|
774
1316
|
|
|
775
1317
|
When no vault exists yet, every `GET` route (except `/init`) returns:
|
|
776
1318
|
|
|
@@ -792,25 +1334,23 @@ separate (mirrors `memory_list`):
|
|
|
792
1334
|
```
|
|
793
1335
|
|
|
794
1336
|
`GET /api/memory/search?q=<query>&scope=all|articles|reference&limit=50` —
|
|
795
|
-
wraps `searchArticles
|
|
796
|
-
formats from:
|
|
1337
|
+
wraps `searchArticles` (`limit` capped at 200; `400` without `q`); same
|
|
1338
|
+
summaries/hits shape the `memory_search` tool formats from:
|
|
797
1339
|
|
|
798
1340
|
```jsonc
|
|
799
1341
|
{ "ok": true, "summaries": [{ "article": "…", "occurrences": 3, "matched_terms": ["…"] }], "hits": [{ "article": "…", "anchor": "…", "line": 10, "excerpt": "…" }] }
|
|
800
1342
|
```
|
|
801
1343
|
|
|
802
1344
|
`GET /api/memory/article?name=<article>` — rendered source plus the
|
|
803
|
-
deterministic metadata the panel needs
|
|
804
|
-
|
|
805
|
-
`{ "ok": false }` for an unknown name:
|
|
1345
|
+
deterministic metadata the panel needs (infobox, lead, series banner,
|
|
1346
|
+
section skeleton); `404 { "ok": false }` for an unknown name:
|
|
806
1347
|
|
|
807
1348
|
```jsonc
|
|
808
1349
|
{ "ok": true, "name": "…", "path": "…", "content": "# …", "infobox": { "type": "…", "entityKind": "thing", "fields": [] }, "lead": "…", "series": null, "structure": [] }
|
|
809
1350
|
```
|
|
810
1351
|
|
|
811
1352
|
`GET /api/memory/links?name=<article>` — inbound/outbound wikilinks (same
|
|
812
|
-
data `memory_links` resolves); articles only, not `Reference
|
|
813
|
-
the tool's own scope):
|
|
1353
|
+
data `memory_links` resolves); articles only, not `Reference/*`:
|
|
814
1354
|
|
|
815
1355
|
```jsonc
|
|
816
1356
|
{ "ok": true, "name": "…", "outbound": ["…"], "inbound": ["…"] }
|
|
@@ -818,7 +1358,7 @@ the tool's own scope):
|
|
|
818
1358
|
|
|
819
1359
|
`GET /api/memory/history?name=<article>&limit=50` — the article's git log
|
|
820
1360
|
(read-only `git log` plumbing on the vault's own repo, via `Bun.spawn` —
|
|
821
|
-
never a shell string):
|
|
1361
|
+
never a shell string; `limit` capped at 200):
|
|
822
1362
|
|
|
823
1363
|
```jsonc
|
|
824
1364
|
{ "ok": true, "name": "…", "isGitRepo": true, "entries": [{ "hash": "…", "date": "YYYY-MM-DD", "subject": "…" }] }
|
|
@@ -827,99 +1367,78 @@ never a shell string):
|
|
|
827
1367
|
`GET /api/memory/diff?name=<article>&rev=<hash>` — that commit's diff for
|
|
828
1368
|
the article (`git show <rev> -- <path>`). `rev` must match
|
|
829
1369
|
`/^[0-9a-f]{4,40}$/` (a bare commit hash — never a ref expression like
|
|
830
|
-
`HEAD~1`
|
|
831
|
-
|
|
832
|
-
uses, so a path-traversal or otherwise-invalid name 404s rather than
|
|
833
|
-
reaching the `git` argv:
|
|
1370
|
+
`HEAD~1`) or the request 400s before touching git; an unknown `name` or a
|
|
1371
|
+
revision with no diff for it 404s:
|
|
834
1372
|
|
|
835
1373
|
```jsonc
|
|
836
1374
|
{ "ok": true, "name": "…", "rev": "…", "diff": "diff --git a/articles/…" }
|
|
837
1375
|
```
|
|
838
1376
|
|
|
839
1377
|
`POST /api/memory/init` with optional body `{"path"?: string}` — the
|
|
840
|
-
first-run consent-card backend. Delegates to the
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
the default vault root or the OS temp directory — `setupVault()` mkdir's
|
|
846
|
-
into it and `git init`s it if it isn't already a repo, so anything outside
|
|
847
|
-
those trees 400s before touching the filesystem rather than letting a
|
|
848
|
-
caller redirect vault setup at an arbitrary directory:
|
|
1378
|
+
first-run consent-card backend. Delegates to the same `setupVault()` the
|
|
1379
|
+
CLI's `mlx-bun memory init` calls (idempotent: create dirs, write README +
|
|
1380
|
+
Meta pages only if missing, git init + initial commit); omits the CLI's
|
|
1381
|
+
interactive extras. `path`, if given, must resolve under the default vault
|
|
1382
|
+
root or the OS temp directory, else `400`:
|
|
849
1383
|
|
|
850
1384
|
```jsonc
|
|
851
1385
|
{ "ok": true, "result": { "root": "…", "created": ["…"], "gitInitialized": true, "alreadySetUp": false }, "status": { "root": "…", "exists": true, "articleCount": 0, "referenceCount": 9, "isGitRepo": true, "recentArticles": [] } }
|
|
852
1386
|
```
|
|
853
1387
|
|
|
854
|
-
## GET /
|
|
855
|
-
|
|
856
|
-
The web chat's durable "always allow this tool" list for gated tools
|
|
857
|
-
(`bash`/`edit`/`write`) — docs/design/web-chat-redesign.md §5.4/§6.5/§9
|
|
858
|
-
Phase 2, risk #6. Backed by `~/.mlx-bun/tool-approvals.json`
|
|
859
|
-
(`src/tool-approvals.ts`), a versioned `{version, allows}` file (same
|
|
860
|
-
plain-JSON-at-0600 convention as `~/.mlx-bun/hf.json`) keyed by stable tool
|
|
861
|
-
NAME, not per-call arguments — "always allow bash" means every future
|
|
862
|
-
`bash` call skips the browser's approval card, on every chat and every
|
|
863
|
-
browser tab, until forgotten. These two routes are read/forget only:
|
|
864
|
-
granting an always-allow happens through the approval card itself (the
|
|
865
|
-
`approval` WS frame on `/ws/chat`, `alwaysAllow: true`), never a bare
|
|
866
|
-
settings-form POST.
|
|
1388
|
+
## GET /v1/memory/synthesize
|
|
867
1389
|
|
|
868
|
-
`
|
|
1390
|
+
Runs the memory synthesis pipeline (`runSynthesis`, `src/memory/pipeline.ts`
|
|
1391
|
+
— the same job `mlx-bun memory synthesize` runs) and streams its progress
|
|
1392
|
+
as SSE. `?dry=1` plans the stages without model calls or writes. This is a
|
|
1393
|
+
long GPU job over the whole store on a real run; it is registered under
|
|
1394
|
+
`/v1/` for historical reasons and is not part of the OpenAI surface.
|
|
869
1395
|
|
|
870
|
-
```jsonc
|
|
871
|
-
{ "ok": true, "alwaysAllow": ["bash", "edit"] }
|
|
872
1396
|
```
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
```jsonc
|
|
879
|
-
{ "ok": true, "alwaysAllow": ["edit"] }
|
|
1397
|
+
data: {"type":"stage","stage":"ingest","message":"…"}
|
|
1398
|
+
data: {"type":"log","message":"…"}
|
|
1399
|
+
data: {"type":"done","message":"synthesis done — …"}
|
|
1400
|
+
data: {"type":"summary","implemented":true,"stages":["ingest","segment","extract","route","create","section-route","patch","link","wikify"],"note":"…"}
|
|
1401
|
+
data: [DONE]
|
|
880
1402
|
```
|
|
881
1403
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
section uses (see the `upload` CLI command in [cli.md](./cli.md) for the
|
|
885
|
-
push-to-hub flow this token drives).
|
|
1404
|
+
A failure emits `data: {"type":"error","message":"…"}` and closes without
|
|
1405
|
+
`[DONE]`.
|
|
886
1406
|
|
|
887
|
-
##
|
|
1407
|
+
## Settings (`/api/settings/*`)
|
|
888
1408
|
|
|
889
|
-
|
|
890
|
-
|
|
1409
|
+
`GET /api/settings/hf-token` → `{ "ok": true, "hasToken": bool }`;
|
|
1410
|
+
`POST /api/settings/hf-token` with `{"token": "hf_…"}` saves the Hugging
|
|
1411
|
+
Face write token to `~/.mlx-bun/hf.json` (mode 0600) →
|
|
1412
|
+
`{ "ok": true }`; `400 "token required"` when empty. The token drives the
|
|
1413
|
+
push routes above and the `upload` CLI command ([cli.md](./cli.md)).
|
|
1414
|
+
|
|
1415
|
+
`GET /api/settings/tool-approvals` / `DELETE /api/settings/tool-approvals`
|
|
1416
|
+
— the web chat's durable "always allow this tool" list for gated tools
|
|
1417
|
+
(`bash`/`edit`/`write`), backed by `~/.mlx-bun/tool-approvals.json`
|
|
1418
|
+
(`src/tool-approvals.ts`), keyed by stable tool NAME, not per-call
|
|
1419
|
+
arguments. These two routes are read/forget only: granting an always-allow
|
|
1420
|
+
happens through the approval card itself (the `approval` WS frame on
|
|
1421
|
+
`/ws/chat` with `alwaysAllow: true`), never a bare settings POST.
|
|
891
1422
|
|
|
892
1423
|
```jsonc
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
"current_file": "model.safetensors" | null,
|
|
898
|
-
"received_bytes": 0,
|
|
899
|
-
"total_bytes": 0,
|
|
900
|
-
"files_done": 0,
|
|
901
|
-
"files_total": 0,
|
|
902
|
-
"bytes_per_sec": 0, // rolling ~5 s window
|
|
903
|
-
"started_at": 1760000000,
|
|
904
|
-
"finished_at": null,
|
|
905
|
-
"error": "…" // present on state "error"
|
|
906
|
-
}]
|
|
907
|
-
}
|
|
1424
|
+
// GET
|
|
1425
|
+
{ "ok": true, "alwaysAllow": ["bash", "edit"] }
|
|
1426
|
+
// DELETE with body {"tool": "bash"} — idempotent; returns the updated set
|
|
1427
|
+
{ "ok": true, "alwaysAllow": ["edit"] }
|
|
908
1428
|
```
|
|
909
1429
|
|
|
1430
|
+
`DELETE` without `tool` is `400 "tool required"`.
|
|
1431
|
+
|
|
910
1432
|
## Model Hub (`/api/hub/*`)
|
|
911
1433
|
|
|
912
|
-
The web chat's Model Hub panel
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
functions, no loaded-model dependency); route dispatch in `src/server.ts`
|
|
917
|
-
just matches path + method.
|
|
1434
|
+
The web chat's Model Hub panel: browse downloaded models, search Hugging
|
|
1435
|
+
Face, and kick off downloads — all loopback-served, no separate auth.
|
|
1436
|
+
Handlers live in `src/hub-rest.ts` (pure functions, no loaded-model
|
|
1437
|
+
dependency).
|
|
918
1438
|
|
|
919
1439
|
`GET /api/hub/local` — every model in the local registry (same source
|
|
920
1440
|
`/library` reads), each with a `/fit`-computed hardware verdict at a fixed
|
|
921
|
-
8k context
|
|
922
|
-
Hub lacks:
|
|
1441
|
+
8k context:
|
|
923
1442
|
|
|
924
1443
|
```jsonc
|
|
925
1444
|
{
|
|
@@ -939,11 +1458,10 @@ Hub lacks:
|
|
|
939
1458
|
```
|
|
940
1459
|
|
|
941
1460
|
`GET /api/hub/search?q=<query>` — server-side Hugging Face model search,
|
|
942
|
-
filtered to the `mlx` library tag
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
explicit offline state rather than a 500:
|
|
1461
|
+
filtered to the `mlx` library tag so results are MLX-compatible by
|
|
1462
|
+
construction. Never downloads anything. `400` without `q`. Degrades on any
|
|
1463
|
+
network failure (DNS, timeout, non-2xx) to an explicit offline state
|
|
1464
|
+
rather than a 500:
|
|
947
1465
|
|
|
948
1466
|
```jsonc
|
|
949
1467
|
{ "ok": true, "offline": false, "results": [{ "id": "mlx-community/…", "downloads": 0, "likes": 0, "size_estimate": null }] }
|
|
@@ -953,69 +1471,51 @@ explicit offline state rather than a 500:
|
|
|
953
1471
|
{ "ok": true, "offline": true, "error": "…", "results": [] }
|
|
954
1472
|
```
|
|
955
1473
|
|
|
956
|
-
`size_estimate` is always `null`
|
|
957
|
-
per-repo file sizes
|
|
958
|
-
|
|
1474
|
+
`size_estimate` is always `null` — the search endpoint doesn't return
|
|
1475
|
+
per-repo file sizes; the fit badge lands once a model is actually
|
|
1476
|
+
downloaded (`/api/hub/local`).
|
|
959
1477
|
|
|
960
1478
|
`POST /api/hub/download` with body `{"repo": "org/name"}` — starts a
|
|
961
|
-
background download via
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
kick-off for a repo that's already downloading:
|
|
1479
|
+
background download via `downloadModel()` and returns immediately;
|
|
1480
|
+
progress is visible via [`GET /downloads`](#get-downloads). Refuses a
|
|
1481
|
+
duplicate kick-off for a repo that's already downloading:
|
|
965
1482
|
|
|
966
1483
|
```jsonc
|
|
967
1484
|
{ "ok": true, "repo": "org/name", "started": true }
|
|
968
1485
|
```
|
|
969
1486
|
|
|
970
1487
|
```jsonc
|
|
971
|
-
{ "ok": false, "error": "a download for org/name is already in progress" }
|
|
1488
|
+
{ "ok": false, "error": "a download for org/name is already in progress" } // 409
|
|
972
1489
|
```
|
|
973
|
-
(409)
|
|
974
1490
|
|
|
975
1491
|
`POST /api/hub/serve` with body `{"model": "org/name"}` — **always answers
|
|
976
|
-
`restart_required`, never performs a live in-process swap.**
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
(The `--isolate` proxy's `ModelPool`, P2 of that doc, *is* a real
|
|
983
|
-
spawn-overlap live-swap mechanism — it just lives entirely on the isolated
|
|
984
|
-
`/v1/*` HTTP surface, not on `/ws/chat`.) Faking a swap here would leave the
|
|
985
|
-
server in a half-state; the honest answer is the restart command:
|
|
1492
|
+
`restart_required`, never performs a live in-process swap.** The process a
|
|
1493
|
+
web chat session is attached to has exactly one model loaded and no
|
|
1494
|
+
drop-weights-and-reload seam reachable from `/ws/chat` (the `--isolate`
|
|
1495
|
+
proxy's `ModelPool` is a real spawn-overlap swap, but it lives on the
|
|
1496
|
+
isolated `/v1/*` surface only — `src/serve/isolate.ts`). The honest answer
|
|
1497
|
+
is the restart command (HTTP 200, `ok: false`):
|
|
986
1498
|
|
|
987
1499
|
```jsonc
|
|
988
1500
|
{ "ok": false, "restart_required": true, "command": "mlx-bun serve org/name" }
|
|
989
1501
|
```
|
|
990
1502
|
|
|
991
|
-
Wiring `/ws/chat` through the isolate proxy (or otherwise reaching
|
|
992
|
-
`ModelPool`'s spawn-overlap swap from the web chat) is the Hub's live-swap
|
|
993
|
-
follow-up, not yet scheduled.
|
|
994
|
-
|
|
995
1503
|
## GET /api/sessions/search · GET /api/sessions/export
|
|
996
1504
|
|
|
997
1505
|
Full-text search across web-chat session message **bodies** and chat
|
|
998
|
-
export
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
`
|
|
1010
|
-
|
|
1011
|
-
the upgrade path if the corpus ever grows enough to matter) over every
|
|
1012
|
-
session file's user/assistant message text, capped at 50 matches total (10
|
|
1013
|
-
per session). Each match reports a plain-text ±60-char snippet plus the
|
|
1014
|
-
match's own `[start,end)` offsets **into that snippet** — offsets, not
|
|
1015
|
-
pre-rendered HTML highlighting, so the frontend escapes the snippet then
|
|
1016
|
-
inserts its own `<mark>` (the same escape-then-restore discipline the
|
|
1017
|
-
markdown renderer uses for code spans, applied here to search
|
|
1018
|
-
highlighting instead):
|
|
1506
|
+
export. Same loopback-only, no-separate-auth posture as `/api/memory/*`.
|
|
1507
|
+
Handlers live in `src/serve/session-search.ts` (pure, read-only, no
|
|
1508
|
+
loaded-model dependency). Both routes read the same session directory the
|
|
1509
|
+
web chat's `PiWebSession` writes to (`~/.mlx-bun/sessions`, pi's own
|
|
1510
|
+
JSONL-per-session format) and are JSONL-tolerant — a corrupt or mid-write
|
|
1511
|
+
line is skipped, never thrown.
|
|
1512
|
+
|
|
1513
|
+
`GET /api/sessions/search?q=<query>` — case-insensitive substring scan (no
|
|
1514
|
+
index — personal-chat-corpus scale) over every session file's
|
|
1515
|
+
user/assistant message text, capped at 50 matches total (10 per session).
|
|
1516
|
+
Each match reports a plain-text ±60-char snippet plus the match's own
|
|
1517
|
+
`[start,end)` offsets **into that snippet** — offsets, not pre-rendered
|
|
1518
|
+
HTML, so the frontend escapes the snippet then inserts its own `<mark>`:
|
|
1019
1519
|
|
|
1020
1520
|
```jsonc
|
|
1021
1521
|
{
|
|
@@ -1031,15 +1531,8 @@ highlighting instead):
|
|
|
1031
1531
|
400s `{"ok": false, "error": "q is required"}` without a query.
|
|
1032
1532
|
|
|
1033
1533
|
`GET /api/sessions/export?path=<session file>` — the raw session JSONL,
|
|
1034
|
-
parsed into an array of entries (one per line, in file order)
|
|
1035
|
-
|
|
1036
|
-
the currently-open one (the open session's own history is already loaded
|
|
1037
|
-
client-side, but this endpoint is used uniformly for both cases so there's
|
|
1038
|
-
one code path, not two). `path` must resolve under the session directory —
|
|
1039
|
-
exactly the same guard shape as `PiWebSession`'s private
|
|
1040
|
-
`isUnderSessionDir` (the root itself, or a path prefixed by `root + "/"`)
|
|
1041
|
-
— reimplemented as a small standalone check in `session-search.ts` rather
|
|
1042
|
-
than exported from that class:
|
|
1534
|
+
parsed into an array of entries (one per line, in file order). `path` must
|
|
1535
|
+
resolve under the session directory:
|
|
1043
1536
|
|
|
1044
1537
|
```jsonc
|
|
1045
1538
|
{ "ok": true, "path": "…", "entries": [{ "type": "session", "id": "…", "cwd": "…" }, { "type": "message", "message": { "role": "user", "content": "…" } }] }
|
|
@@ -1050,153 +1543,118 @@ path inside the directory that doesn't exist on disk.
|
|
|
1050
1543
|
|
|
1051
1544
|
## Web app static routes + PWA installability
|
|
1052
1545
|
|
|
1053
|
-
The web chat (`GET /`) and everything it loads same-origin, no CDN, ever
|
|
1546
|
+
The web chat (`GET /`) and everything it loads same-origin, no CDN, ever
|
|
1547
|
+
(`src/serve/static-routes.ts`; GET only):
|
|
1054
1548
|
|
|
1055
1549
|
| Route | Content | Notes |
|
|
1056
1550
|
| --- | --- | --- |
|
|
1057
1551
|
| `GET /` | `src/web/app.html` | The unified SPA shell. |
|
|
1058
|
-
| `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. |
|
|
1059
|
-
| `GET /assets/hljs.js`, `GET /assets/hljs.css` | `src/web/vendor/hljs*` | Vendored syntax highlighting, no CDN
|
|
1060
|
-
| `GET /manifest.webmanifest` | `src/web/manifest.webmanifest` | PWA manifest
|
|
1061
|
-
| `GET /assets/icon.svg` | `src/web/icon.svg` | A single inline SVG app icon
|
|
1062
|
-
| `GET /sw.js` | `src/web/sw.js` | Service worker, served `cache-control: no-store` at the root scope
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
the
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
`
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1552
|
+
| `GET /assets/app.js` | `src/web/app.js` | GENERATED from `src/web/src/*.ts` by `bun scripts/build-web.ts` — see `tests/using/web-build.test.ts`'s freshness gate. |
|
|
1553
|
+
| `GET /assets/hljs.js`, `GET /assets/hljs.css` | `src/web/vendor/hljs*` | Vendored syntax highlighting, no CDN. |
|
|
1554
|
+
| `GET /manifest.webmanifest` | `src/web/manifest.webmanifest` | PWA manifest. |
|
|
1555
|
+
| `GET /assets/icon.svg` | `src/web/icon.svg` | A single inline SVG app icon; no binary PNGs are shipped. |
|
|
1556
|
+
| `GET /sw.js` | `src/web/sw.js` | Service worker, served `cache-control: no-store` at the root scope. |
|
|
1557
|
+
| `GET /curves` | `src/lab/curve/curve-designer.html` | The HLG Curve Designer page (drives `/signal` + `/generate` below); read from disk when available, embedded copy in compiled binaries. `no-store`. |
|
|
1558
|
+
| `GET /curve-terrain` | `src/serve/assets/curve-terrain.html` | Curve terrain visualization; `404` when the artifact isn't on disk. |
|
|
1559
|
+
| `GET /dag` | `src/serve/assets/training-inference-map.html` | Training/inference DAG map; `404` when the artifact isn't on disk. |
|
|
1560
|
+
| `GET /status`, `/chat`, `/quantize`, `/finetune`, `/dataset` | — | Legacy page paths: `302` to `/#<path>` (the SPA's hash routes). |
|
|
1561
|
+
|
|
1562
|
+
`/assets/*` and the manifest are served with `cache-control: public,
|
|
1563
|
+
max-age=3600`; `/sw.js`, `/curves`, `/curve-terrain`, `/dag` are `no-store`.
|
|
1564
|
+
|
|
1565
|
+
**PWA scope:** the manifest + service worker exist for *installability and
|
|
1566
|
+
an instant static-shell paint* — "Add to Home Screen" / a browser "Install
|
|
1567
|
+
app" prompt, and the next visit's HTML/JS/CSS loading from cache while the
|
|
1568
|
+
WebSocket connects. This is explicitly **not** offline chat:
|
|
1569
|
+
`src/web/sw.js`'s fetch handler only intercepts the exact shell files
|
|
1570
|
+
(`/`, `/assets/app.js`, `/assets/hljs.js`, `/assets/hljs.css`) and lets
|
|
1571
|
+
every dynamic route (`/api/*`, `/v1/*`, `/ws/chat`, `/downloads`, …) fall
|
|
1572
|
+
straight through to the network unconditionally. Registration is guarded
|
|
1573
|
+
to secure contexts (`https:` or `localhost`/`127.0.0.1`).
|
|
1574
|
+
|
|
1575
|
+
## Curve Designer: POST /signal · POST /generate
|
|
1576
|
+
|
|
1577
|
+
Backends for the `/curves` page (HLG tone-curve sampling research tool,
|
|
1578
|
+
`src/lab/curve/curve-sampler.ts`). Both answer `OPTIONS` and send permissive CORS
|
|
1579
|
+
headers (`access-control-allow-origin: *`) so the page can be opened from
|
|
1580
|
+
a file or another origin. Both take the GPU lock (`gateway.runExclusive`)
|
|
1581
|
+
— they never interleave with a serving request.
|
|
1582
|
+
|
|
1583
|
+
`POST /signal` with `{"prompt": "…"}` — one prefill forward of the prompt
|
|
1584
|
+
rendered as a single user turn; bins the next-token log-prob distribution
|
|
1585
|
+
into 80 buckets over the curve's x-axis so the editor can draw the signal
|
|
1586
|
+
under the curve: `{ "bins": [80 ints], "vocab": V }`. `400 {"error":
|
|
1587
|
+
"invalid JSON"}`; `500 {"error": "signal failed: …"}`.
|
|
1588
|
+
|
|
1589
|
+
`POST /generate` with `{"prompt", "curve"?: {points: [...]}, "n"?: 3,
|
|
1590
|
+
"max_tokens"?: 80, "seed"?: 0, "default"?: false}` — samples `n` (1–8)
|
|
1591
|
+
completions of up to `max_tokens` (1–256) tokens. When `curve` has ≥2
|
|
1592
|
+
points and `default` is not `true`, the drawn log-prob transfer curve
|
|
1593
|
+
replaces temperature+softmax entirely; otherwise the model's default chat
|
|
1594
|
+
recipe (temperature/top-p/top-k) is used. A non-monotone curve is a `400`.
|
|
1096
1595
|
|
|
1097
1596
|
```jsonc
|
|
1098
|
-
{
|
|
1099
|
-
"
|
|
1100
|
-
"
|
|
1101
|
-
"
|
|
1102
|
-
"typical_decode_tps": 0.0, // predicted at typical_context_tokens
|
|
1103
|
-
"measured_decode_tps": null, // real number from eval DB, or null
|
|
1104
|
-
"measured_at": null, // unix ms of measurement, or null
|
|
1105
|
-
"report": {
|
|
1106
|
-
"fits": true,
|
|
1107
|
-
"weights_bytes": 0,
|
|
1108
|
-
"kv_bytes": 0,
|
|
1109
|
-
"transient_bytes": 0,
|
|
1110
|
-
"total_bytes": 0,
|
|
1111
|
-
"usable_bytes": 0,
|
|
1112
|
-
"max_safe_context": 8192,
|
|
1113
|
-
"predicted_decode_tps": 0.0
|
|
1114
|
-
},
|
|
1115
|
-
// GLM-5.2 only:
|
|
1116
|
-
"glm52": {
|
|
1117
|
-
"artifact_disk_bytes": 0, // exact registry size; null if unavailable
|
|
1118
|
-
"main_expert_slab_bytes": 0,
|
|
1119
|
-
"mtp_expert_slab_bytes": 0,
|
|
1120
|
-
"max_generation_tokens": 128,
|
|
1121
|
-
"direct_oracle_warm_decode_tps": 0.27,
|
|
1122
|
-
"aspirational_decode_tps": 2.0
|
|
1123
|
-
},
|
|
1124
|
-
"sku_matrix_ctx": 32768,
|
|
1125
|
-
"sku_matrix": [{
|
|
1126
|
-
"sku": "M4 Pro 24 GB", "ram_gb": 24,
|
|
1127
|
-
"fits": true, "max_context": 32768, "decode_tps": 0.0
|
|
1128
|
-
}]
|
|
1129
|
-
}
|
|
1597
|
+
{ "mode": "curve" | "default",
|
|
1598
|
+
"recipe": { "temperature": 0.7, "topP": 0, "topK": 0 }, // only in default mode
|
|
1599
|
+
"n": 3, "seed": 0,
|
|
1600
|
+
"samples": [{ "text": "…", "junk": false }] } // junk = coherence heuristic tripped
|
|
1130
1601
|
```
|
|
1131
1602
|
|
|
1132
|
-
##
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
The web chat agent can see and act on the app it lives in —
|
|
1175
|
-
`docs/design/web-chat-redesign.md` §6.6, beat matrix Axis 12. No
|
|
1176
|
-
screenshots and no vision model are involved: the browser sends a
|
|
1177
|
-
structured DOM snapshot as a WS frame, and the model gets three tools that
|
|
1178
|
-
read/navigate/highlight that snapshot.
|
|
1179
|
-
|
|
1180
|
-
Three tools join the web chat's tool allowlist unconditionally (same
|
|
1181
|
-
class as `web_search`/`read` — read-only-on-the-machine, never gated by
|
|
1182
|
-
the approval card, regardless of memory or `codingTools` state; see
|
|
1183
|
-
`APP_AWARE_TOOL_NAMES` in `src/pi-web.ts`):
|
|
1603
|
+
## Web chat WebSocket (`/ws/chat`)
|
|
1604
|
+
|
|
1605
|
+
`GET /ws/chat` upgrades to a WebSocket (`426` when the upgrade header is
|
|
1606
|
+
missing; `501` under `--isolate`). It carries the web chat's pi
|
|
1607
|
+
`AgentSession` — the model is driven through pi, which calls back into
|
|
1608
|
+
this server's own loopback `/v1`. Frames are JSON objects with a `type`
|
|
1609
|
+
field; the exact TypeScript unions are `ClientMessage` and `ServerMessage`
|
|
1610
|
+
in `src/pi-web.ts` (the web frontend imports them type-only, so both ends
|
|
1611
|
+
are compile-checked).
|
|
1612
|
+
|
|
1613
|
+
Client → server: `prompt {text, images?: [{data, mimeType}]}`, `abort`,
|
|
1614
|
+
`approval {callId, decision, editedArgs?, alwaysAllow?}`, `set_thinking
|
|
1615
|
+
{enabled}`, `set_coding_tools {enabled}`, `set_adapter {id | null}`,
|
|
1616
|
+
`set_sampling {scope?: "session" | "next_turn", temperature?, top_p?,
|
|
1617
|
+
top_k?, min_p?, xtc_probability?, xtc_threshold?, repetition_penalty?,
|
|
1618
|
+
repetition_context_size?, presence_penalty?, frequency_penalty?, seed?}`
|
|
1619
|
+
(null = server default), `set_system_prompt {text | null}`, `new_session`,
|
|
1620
|
+
`list_sessions`, `open_session {path}`, `fork_session {path}`,
|
|
1621
|
+
`delete_session {path}`, `regenerate`, `edit_resend {text}`,
|
|
1622
|
+
`switch_sibling {entryId}`, `context {context}` (app-aware assistant, below).
|
|
1623
|
+
|
|
1624
|
+
Server → client: `ready {model, vision, audio, thinking, genDefaults}`,
|
|
1625
|
+
`turn_start`, `text_delta {delta}`, `thinking_delta {delta}`, `tool_start
|
|
1626
|
+
{callId, tool, args}`, `tool_approval_request {callId, tool, args}`,
|
|
1627
|
+
`tool_update {callId, chunk}`, `tool_end {callId, ok, result}`, `turn_end
|
|
1628
|
+
{lane?}` (the serving lane the turn ran on, from `usage.lane` via the lane
|
|
1629
|
+
registry — never guessed client-side), `queue_update {steering, followUp}`,
|
|
1630
|
+
`history {items}`, `sessions {items, activePath?}`, `siblings {entryId?,
|
|
1631
|
+
index, count, siblingIds}`, `context {tokens, contextWindow, percent}`
|
|
1632
|
+
(context-window usage — a different frame from the client→server
|
|
1633
|
+
`context`), `coding_tools {active, pending}`, `tool_approvals
|
|
1634
|
+
{alwaysAllow}`, `ui_navigate {route}`, `ui_spotlight {ref?, label?,
|
|
1635
|
+
selector?, target?, message?, route?}`, `error {message}`.
|
|
1636
|
+
|
|
1637
|
+
### App-aware assistant
|
|
1638
|
+
|
|
1639
|
+
The web chat agent can see and act on the app it lives in. No screenshots
|
|
1640
|
+
and no vision model are involved: the browser sends a structured DOM
|
|
1641
|
+
snapshot as a WS frame, and the model gets three tools that
|
|
1642
|
+
read/navigate/highlight that snapshot. The three tools are always in the
|
|
1643
|
+
web chat's tool allowlist (read-only-on-the-machine, never gated by the
|
|
1644
|
+
approval card; `APP_AWARE_TOOL_NAMES` in `src/pi-web.ts`):
|
|
1184
1645
|
|
|
1185
1646
|
- **`get_current_app_context`** — returns the last context the browser
|
|
1186
1647
|
pushed: `{ route, view?, step?, snapshot }`. `snapshot` is a capped
|
|
1187
1648
|
(~120) list of visible interactive elements as `{ref, label, kind,
|
|
1188
1649
|
role?, selector, spotlightId?}`, agent chrome excluded.
|
|
1189
1650
|
- **`navigate_app({ route | page })`** — validates against the route
|
|
1190
|
-
catalog (`chat`, `quantize`, `finetune`, `dataset`,
|
|
1191
|
-
routes
|
|
1192
|
-
emits a `ui_navigate` frame.
|
|
1193
|
-
- **`spotlight_ui({ ref | label | selector | target, route?, message?
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
blocks: the overlay traps no focus and passes clicks straight through
|
|
1198
|
-
to the highlighted control, auto-dismisses in ~3s, and any keystroke,
|
|
1199
|
-
click, or scroll dismisses it instantly.
|
|
1651
|
+
catalog (`APP_ROUTE_IDS`: `chat`, `quantize`, `finetune`, `dataset`,
|
|
1652
|
+
`status`; unknown routes are a tool error, never sent to the browser)
|
|
1653
|
+
and emits a `ui_navigate` frame.
|
|
1654
|
+
- **`spotlight_ui({ ref | label | selector | target, route?, message? })`**
|
|
1655
|
+
— emits a `ui_spotlight` frame; the browser resolves the target (ref
|
|
1656
|
+
from its last snapshot, then live selector, then fuzzy label match, then
|
|
1657
|
+
a curated catalog id) and shows a brief non-blocking highlight.
|
|
1200
1658
|
|
|
1201
1659
|
Client → server WS frame (pushed by the browser on every route change and
|
|
1202
1660
|
every wizard-step change, never on a timer):
|
|
@@ -1214,10 +1672,8 @@ Server → client frames the two action tools produce:
|
|
|
1214
1672
|
|
|
1215
1673
|
The server also auto-prepends a compact one-line ambient context (e.g.
|
|
1216
1674
|
`[user is on: Quantize · step 2/4]`) to the system prompt on every turn —
|
|
1217
|
-
never a full snapshot dump
|
|
1218
|
-
|
|
1219
|
-
and job-start actions) is explicitly out of scope for this v1; only
|
|
1220
|
-
navigate/spotlight are wired.
|
|
1675
|
+
never a full snapshot dump. Approval-gated form-fill / job-start actions
|
|
1676
|
+
are not wired; only navigate/spotlight are.
|
|
1221
1677
|
|
|
1222
1678
|
## Client setup: pi
|
|
1223
1679
|
|
|
@@ -1258,3 +1714,8 @@ solve.
|
|
|
1258
1714
|
|
|
1259
1715
|
Any OpenAI SDK works the same way: `baseURL: "http://127.0.0.1:8080/v1"`,
|
|
1260
1716
|
any non-empty `apiKey`.
|
|
1717
|
+
|
|
1718
|
+
Under `--isolate`, the parent owns Responses continuation history and exposes its
|
|
1719
|
+
bounded store counters in `GET /engine` as `response_store`. Model-worker restart
|
|
1720
|
+
or eviction preserves that history; restarting the parent clears it. Direct
|
|
1721
|
+
serving retains the same process-local TTL and byte limits.
|