mlx-bun 0.0.13 → 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 -578
- package/bin/mlx-bun.mjs +1 -1
- package/docs/README.md +91 -0
- package/docs/{design → archive}/hlg-sampling.md +3 -3
- package/docs/{design → archive/investigations}/audio-input-plan.md +4 -4
- package/docs/{design → archive/investigations}/colibri-glm52-port.md +1 -1
- package/docs/{design → archive/investigations}/diffusion-gemma-port.md +3 -3
- package/docs/{design → archive/investigations}/faithful-l1-consolidation.md +4 -4
- package/docs/{investigations → archive/investigations}/hlg-sampling-investigation.md +1 -1
- package/docs/{design → archive/investigations}/minicpm5-decode-megakernel.md +11 -11
- package/docs/{design → archive/investigations}/optimization_plan.md +1 -1
- package/docs/archive/investigations/pareto-specialized-runtime-findings.md +92 -0
- package/docs/{design → archive/investigations}/pre-colibri-stabilization.md +5 -5
- package/docs/{design → archive/investigations}/the-dreaming-handoff.md +2 -2
- package/docs/{planning → archive/planning}/chat-ui-vision.md +1 -1
- package/docs/{planning → archive/planning}/chunk-task-roadmap.md +1 -1
- package/docs/{planning → archive/planning}/curve-sampler-research-plan.md +4 -4
- package/docs/{planning → archive/planning}/memory-docs-and-dag-plan.md +1 -1
- package/docs/{planning → archive/planning}/release-notes-v0.0.9.md +1 -1
- package/docs/archive/planning/release-notes-v0.2.0.md +49 -0
- package/docs/{planning → archive/planning}/website-readme-pass-plan.md +1 -1
- 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 +421 -0
- package/docs/reference/benchmarks.md +1162 -0
- package/docs/reference/cli.md +395 -149
- package/docs/reference/distribution.md +321 -181
- package/docs/reference/environment.md +348 -0
- package/docs/reference/glossary.md +39 -0
- package/docs/reference/library-api.md +580 -142
- package/docs/reference/memory.md +227 -64
- package/docs/reference/models.md +137 -6
- package/docs/reference/server-api.md +938 -422
- package/docs/reference/server-config.md +652 -320
- package/docs/reference/training.md +393 -254
- package/docs/reference/troubleshooting.md +166 -0
- package/package.json +10 -5
- package/src/admission-plan.ts +18 -0
- package/src/anthropic.ts +92 -3
- 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 +189 -61
- 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 +263 -23
- package/src/eval/reference-logits.ts +207 -0
- package/src/eval/runner.ts +10 -79
- package/src/eval/tasks/bfcl.ts +2 -1
- package/src/eval/tasks/gsm8k.ts +2 -1
- package/src/eval/tasks/hashhop.ts +2 -1
- package/src/eval/tasks/humaneval.ts +5 -4
- package/src/eval/tasks/ifeval.ts +2 -1
- package/src/eval/tasks/mmlu.ts +2 -1
- package/src/expert-io.ts +3 -1
- package/src/expert-offload.ts +3 -1
- 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 +24 -43
- package/src/generate.ts +740 -337
- package/src/grammar.ts +14 -8
- package/src/harness-pi.ts +15 -34
- package/src/hub-rest.ts +4 -4
- package/src/index.ts +27 -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 +15 -14
- 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-scheme.ts +199 -0
- package/src/kv-store.ts +459 -234
- 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} +7 -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 +20 -4
- package/src/memory/article.ts +1 -1
- package/src/memory/chunk.ts +1 -1
- package/src/memory/model.ts +32 -170
- 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 +3 -4
- package/src/mlx/array.ts +9 -10
- package/src/mlx/compile.ts +12 -2
- package/src/mlx/ffi.ts +66 -22
- package/src/mlx/materialize.ts +23 -0
- package/src/mlx/metal-kernel.ts +10 -11
- package/src/mlx/ops.ts +64 -19
- 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 +9 -3
- package/src/model/batched-quant.ts +2 -2
- package/src/model/batched-rotating-quant.ts +45 -99
- package/src/model/batched-rotating-state.ts +73 -0
- package/src/model/batched-rotating.ts +57 -106
- package/src/model/batched-row-storage.ts +127 -0
- package/src/model/compiled-decode.ts +4 -3
- package/src/model/diffusion-gemma.ts +8 -8
- package/src/model/factory.ts +87 -49
- package/src/model/fingerprint.ts +1 -1
- package/src/model/flash-attention.ts +3 -3
- package/src/model/gemma4-base.ts +187 -17
- package/src/model/gemma4.ts +6 -6
- package/src/model/generated/gemma4-12b.ts +52 -148
- package/src/model/generated/gemma4-26b.ts +34 -94
- package/src/model/generated/gemma4-e4b.ts +28 -76
- package/src/model/glm52-cache.ts +19 -9
- package/src/model/glm52-dsa.ts +13 -9
- package/src/model/glm52-memory.ts +28 -1
- package/src/model/implementation.ts +69 -0
- package/src/model/minicpm5.ts +7 -8
- package/src/model/profile.ts +443 -0
- package/src/model/qwen-append.ts +12 -0
- package/src/model/qwen-conv.ts +52 -0
- package/src/model/qwen3-delta.ts +98 -2
- package/src/model/qwen3-mrope.ts +252 -0
- package/src/model/qwen3_5-checkpoint.ts +164 -0
- package/src/model/qwen3_5.ts +336 -60
- 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/frame_extract.swift +140 -0
- package/src/native-pack.ts +15 -7
- package/src/pi-provider.ts +6 -64
- package/src/pi-web.ts +14 -320
- package/src/prompt-cache.ts +97 -54
- package/src/quantize/allocator.ts +1 -1
- package/src/quantize/atomic-output.ts +38 -0
- package/src/quantize/calibration.ts +2 -1
- package/src/quantize/config-writer.ts +16 -3
- package/src/quantize/index.ts +28 -1
- package/src/quantize/job.ts +11 -0
- package/src/quantize/quantizer.ts +265 -100
- package/src/quantize/rotate.ts +603 -0
- package/src/quantize/safetensors-writer.ts +79 -1
- package/src/quantize/sensitivity.ts +41 -59
- package/src/quantize/trellis.ts +586 -0
- package/src/quantize/weight-transform.ts +292 -0
- package/src/responses.ts +130 -3
- package/src/runtime-config.ts +81 -0
- package/src/sampler.ts +276 -4
- package/src/serve/admin-routes.ts +184 -0
- package/src/serve/aux-routes.ts +132 -0
- package/src/serve/batch-scheduler.ts +8 -1558
- 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 +354 -0
- package/src/serve/completion-sink.ts +157 -0
- package/src/serve/discovery-routes.ts +204 -0
- package/src/serve/generation-gateway.ts +211 -243
- 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/kv-budget.ts +14 -0
- package/src/serve/lab-routes.ts +341 -0
- package/src/serve/lane-registry.ts +2 -1
- package/src/serve/model-admin-routes.ts +155 -0
- 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/prompt-response-trace.ts +147 -0
- package/src/serve/request-plan.ts +162 -0
- 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 +101 -0
- package/src/serve/text-completion-stage.ts +104 -0
- package/src/serve/token-streams.ts +519 -0
- package/src/server.ts +523 -3551
- package/src/spec/assistant-source.ts +10 -44
- package/src/spec/deepspec-source.ts +6 -6
- package/src/spec/dflash-source.ts +9 -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 +5 -4
- 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 +9 -1
- package/src/spec/ngram-source.ts +1 -1
- package/src/spec/prefix-state.ts +45 -0
- package/src/spec/qwen-mtp-source.ts +528 -0
- package/src/spec/serve-loop.ts +179 -213
- package/src/spec/source.ts +42 -10
- package/src/spec/two-model.ts +1 -1
- package/src/ssd-cache.ts +95 -24
- package/src/ssd-durability.ts +207 -0
- package/src/tool-call.ts +51 -14
- package/src/train/flash-cce.ts +14 -13
- package/src/train/forward.ts +38 -1
- package/src/train/loss.ts +3 -2
- package/src/train/prefix-shared.ts +5 -1
- package/src/train/segmented.ts +9 -6
- package/src/train/trainer.ts +9 -8
- package/src/vision/preprocess.ts +1 -1
- package/src/vision/prompt.ts +48 -2
- package/src/vision/qwen3vl-preprocess.ts +409 -0
- package/src/vision/qwen3vl-prompt.ts +196 -0
- package/src/vision/qwen3vl-tower.ts +555 -0
- package/src/vision/video-frames.ts +148 -0
- package/src/web/app.html +1 -1
- package/src/web/app.js +15 -6
- package/src/web/src/adapters-panel.ts +3 -3
- package/src/web/src/assistant.ts +2 -36
- package/src/web/src/chat.ts +16 -9
- 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 +134 -33
- 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 -268
- 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-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 -189
- 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 -288
- 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/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/investigations/curve-runs/bisector-route.json +0 -98
- package/docs/investigations/curve-runs/bisector-trace.json +0 -1393
- package/docs/investigations/curve-runs/curve-terrain.json +0 -1179
- package/docs/investigations/curve-runs/curve-terrain.svg +0 -42
- package/docs/investigations/hlg-ab.html +0 -34
- package/docs/investigations/hlg-explorer.html +0 -238
- package/docs/investigations/hlg-figs/cliff.svg +0 -1
- package/docs/investigations/hlg-figs/cross.svg +0 -1
- package/docs/investigations/hlg-figs/edges.svg +0 -1
- package/docs/investigations/hlg-figs/k20-grids.svg +0 -1
- package/docs/investigations/hlg-figs/k20-peaks.svg +0 -1
- package/docs/investigations/hlg-figs/lw-curve.svg +0 -1
- package/docs/investigations/hlg-figs/terrain.svg +0 -1
- package/docs/investigations/hlg-report.html +0 -262
- package/docs/investigations/hlg-runs/hlg-ab.json +0 -149
- package/docs/investigations/hlg-runs/hlg-blind-prompts.md +0 -87
- package/docs/investigations/hlg-runs/hlg-blind.json +0 -107
- package/docs/investigations/hlg-runs/hlg-cliff.json +0 -197
- package/docs/investigations/hlg-runs/hlg-cross.json +0 -505
- package/docs/investigations/hlg-runs/hlg-edge.json +0 -388
- package/docs/investigations/hlg-runs/hlg-eval1.json +0 -25
- package/docs/investigations/hlg-runs/hlg-frontier.json +0 -86
- package/docs/investigations/hlg-runs/hlg-grid.json +0 -242
- package/docs/investigations/hlg-runs/hlg-grid2.json +0 -343
- package/docs/investigations/hlg-runs/hlg-lw-coarse.json +0 -110
- package/docs/investigations/hlg-runs/hlg-map.json +0 -383
- package/docs/investigations/hlg-runs/run0-original.log +0 -159
- package/docs/investigations/hlg-runs/runA-recommended.log +0 -131
- package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +0 -26
- package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +0 -18
- package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +0 -25
- package/docs/investigations/hlg-runs/runAD-ab-samples.log +0 -8
- package/docs/investigations/hlg-runs/runAE-edge-e4b.log +0 -51
- package/docs/investigations/hlg-runs/runAF-logit-calibration.log +0 -10
- package/docs/investigations/hlg-runs/runAG-eval-config2.log +0 -16
- package/docs/investigations/hlg-runs/runB-gain-variants.log +0 -192
- package/docs/investigations/hlg-runs/runC-pivots.log +0 -162
- package/docs/investigations/hlg-runs/runD-oetf.log +0 -132
- package/docs/investigations/hlg-runs/runE-eotf.log +0 -162
- package/docs/investigations/hlg-runs/runF-shaper-working.log +0 -86
- package/docs/investigations/hlg-runs/runG-shaper-sweep.log +0 -101
- package/docs/investigations/hlg-runs/runH-wsweep.log +0 -176
- package/docs/investigations/hlg-runs/runI-asweep.log +0 -176
- package/docs/investigations/hlg-runs/runJ-knobmap.log +0 -49
- package/docs/investigations/hlg-runs/runK-frontier.log +0 -19
- package/docs/investigations/hlg-runs/runL-widemap.log +0 -65
- package/docs/investigations/hlg-runs/runM-frontier-emb.log +0 -15
- package/docs/investigations/hlg-runs/runN-verify.log +0 -116
- package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +0 -15
- package/docs/investigations/hlg-runs/runP-grid-AxSm.log +0 -15
- package/docs/investigations/hlg-runs/runQ-grid-tgW.log +0 -15
- package/docs/investigations/hlg-runs/runR-blind-transcript.log +0 -79
- package/docs/investigations/hlg-runs/runS-cross-coherence.log +0 -33
- package/docs/investigations/hlg-runs/runT-cross-cpm5.log +0 -33
- package/docs/investigations/hlg-runs/runU-cross-12b.log +0 -33
- package/docs/investigations/hlg-runs/runV-lw-e4b.log +0 -22
- package/docs/investigations/hlg-runs/runW-lw-12b.log +0 -22
- package/docs/investigations/hlg-runs/runX-lw-cpm5.log +0 -22
- package/docs/investigations/hlg-runs/runY-cliff-e4b.log +0 -26
- package/docs/investigations/hlg-runs/runZ-cliff-12b.log +0 -26
- package/docs/planning/IDEAS.md +0 -69
- package/docs/reference/embedding.md +0 -197
- package/docs/reference/features-matrix.md +0 -100
- package/docs/reference/orpo-quickstart.md +0 -135
- package/src/flags.ts +0 -13
- /package/docs/{investigations → archive/investigations}/chunk-finetune-experiment.md +0 -0
- /package/docs/{investigations → archive/investigations}/colibri-oracle-pin.md +0 -0
- /package/docs/{investigations → archive/investigations}/curve-bisector-routing.md +0 -0
- /package/docs/{investigations → archive/investigations}/decode-roofline-lookagain.md +0 -0
- /package/docs/{investigations → archive/investigations}/dspark-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/expert-offload-single-user-moe.md +0 -0
- /package/docs/{investigations → archive/investigations}/indexshare-performance-spike.md +0 -0
- /package/docs/{investigations → archive/investigations}/kernel-perf-review-2026-07.md +0 -0
- /package/docs/{investigations → archive/investigations}/lab-build-journal.md +0 -0
- /package/docs/{investigations → archive/investigations}/openwiki-evaluation.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-base-uf-experiment-and-directions.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-flash-cce-pin-leak.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-uf-testing-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/pi-builtin-investigation.md +0 -0
- /package/docs/{investigations → archive/investigations}/segmented-backward-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/starter-model-port-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/steel-flash-cce-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/trainer-validation-experiment.md +0 -0
- /package/docs/{planning → archive/planning}/chat-ui-vision-memory-first.md +0 -0
- /package/docs/{planning → archive/planning}/journal.md +0 -0
- /package/docs/{planning → archive/planning}/pi-integration-review.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.10.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.11.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.12.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.13.md +0 -0
- /package/docs/{planning → archive/planning}/web-ui-pass-plan.md +0 -0
- /package/{docs/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)
|
|
@@ -67,8 +164,8 @@ Request body (OpenAI chat schema; unknown fields ignored):
|
|
|
67
164
|
// wins. On templates with a reasoning-depth
|
|
68
165
|
// variable (Qwen3.8) the level also maps
|
|
69
166
|
// into the template: minimal/low → low,
|
|
70
|
-
// medium → medium, high/xhigh → xhigh
|
|
71
|
-
//
|
|
167
|
+
// medium → medium, high/xhigh → xhigh.
|
|
168
|
+
// Any OTHER string is a 400.
|
|
72
169
|
"hlg": { // HLG tone-curve sampling (per request).
|
|
73
170
|
"enabled": true, // merged over --hlg-sampling server defaults.
|
|
74
171
|
"width": 4, // logit-width of the tone plateau
|
|
@@ -89,6 +186,13 @@ Sampling defaults follow the model author's `generation_config.json`
|
|
|
89
186
|
when a field is omitted (optiq serve's gen_config behavior); explicit
|
|
90
187
|
request values always win. MiniCPM5 defaults to the no-think direct
|
|
91
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
|
+
|
|
92
196
|
GLM-5.2 also defaults to no-think: its generation primer is
|
|
93
197
|
`<think></think>`. Enabling thinking renders `Reasoning Effort: Max` in the
|
|
94
198
|
system turn and leaves an open `<think>` primer for generation. The same
|
|
@@ -103,18 +207,42 @@ and `finish_reason` is `"stop"`.
|
|
|
103
207
|
Message `content` is a string or an array of parts:
|
|
104
208
|
`{ "type": "text", "text": ... }` and
|
|
105
209
|
`{ "type": "image_url", "image_url": { "url": "data:image/png;base64,..." } }`
|
|
106
|
-
(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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).
|
|
213
|
+
|
|
214
|
+
Remote (http/https) media URLs — image, audio, and video alike — go
|
|
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
|
|
114
221
|
(`prompt build failed: image url rejected: …`). Serving media from a LAN
|
|
115
222
|
host (a NAS, another machine) is the opt-in
|
|
116
|
-
[`--allow-private-media`](server-config.md#start-flags) flag
|
|
117
|
-
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.
|
|
226
|
+
|
|
227
|
+
### Video input (Qwen3.5-family models)
|
|
228
|
+
|
|
229
|
+
Video rides the same content-part array:
|
|
230
|
+
|
|
231
|
+
```jsonc
|
|
232
|
+
{ "type": "video_url", "video_url": { "url": "data:video/mp4;base64,…|http(s)://…" } }
|
|
233
|
+
{ "type": "video", "data": "<base64>" } // alias
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
The container decodes through macOS's own AVFoundation codecs (mp4/mov,
|
|
237
|
+
H.264/HEVC/ProRes/… — whatever this macOS plays) via the
|
|
238
|
+
`mlx-bun-frame-extract` helper that ships beside the binary and in the
|
|
239
|
+
native pack; from a source tree it compiles on first use with the Xcode
|
|
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.
|
|
118
246
|
|
|
119
247
|
### Audio input
|
|
120
248
|
|
|
@@ -149,12 +277,13 @@ curl -s http://localhost:8080/v1/chat/completions \
|
|
|
149
277
|
`400` naming the model — never a silent text-only answer.
|
|
150
278
|
- **Formats**: WAV is decoded natively (PCM 16/24/32-bit + float32, any
|
|
151
279
|
channel count / sample rate). mp3, m4a/AAC, FLAC, ogg(-CAF), AIFF and
|
|
152
|
-
anything else CoreAudio reads transcode via macOS `afconvert
|
|
153
|
-
undecodable bytes are a `400
|
|
280
|
+
anything else CoreAudio reads transcode via macOS `afconvert`
|
|
281
|
+
(`src/audio/transcode.ts`); undecodable bytes are a `400`.
|
|
154
282
|
- **Internal format**: 16 kHz mono — multi-channel input is mean-mixed,
|
|
155
283
|
other rates resample. Clips truncate at **30 s** (the oracle
|
|
156
284
|
processor's 480 000-sample cap); one clip costs
|
|
157
|
-
`ceil(duration_ms / 40)` prompt tokens, at most **750
|
|
285
|
+
`ceil(duration_ms / 40)` prompt tokens, at most **750**
|
|
286
|
+
(`src/audio/decode.ts`).
|
|
158
287
|
- Multiple clips per message and **mixed image+audio** requests work;
|
|
159
288
|
media splices in document order.
|
|
160
289
|
- **Routing**: audio requests always run on the **serial lane** (never
|
|
@@ -175,12 +304,16 @@ Non-streaming response:
|
|
|
175
304
|
"index": 0,
|
|
176
305
|
"message": {
|
|
177
306
|
"role": "assistant",
|
|
178
|
-
"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)
|
|
179
311
|
"tool_calls": [{ // present when the model called tools
|
|
180
312
|
"id": "call_…", "type": "function",
|
|
181
313
|
"function": { "name": "…", "arguments": "{…json…}" }
|
|
182
314
|
}]
|
|
183
315
|
},
|
|
316
|
+
"logprobs": { /* see below; only when requested */ },
|
|
184
317
|
"finish_reason": "stop" | "length" | "tool_calls"
|
|
185
318
|
}],
|
|
186
319
|
"usage": {
|
|
@@ -188,14 +321,31 @@ Non-streaming response:
|
|
|
188
321
|
"prompt_tokens_details": { "cached_tokens": 0 }, // prompt-cache reuse
|
|
189
322
|
"lane": "serial", // "serial" | "serial+spec" | "batched" —
|
|
190
323
|
// which execution lane served THIS
|
|
191
|
-
// request
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
// only (streaming: final usage chunk).
|
|
324
|
+
// request. Chat completions and text
|
|
325
|
+
// completions (streaming: final usage
|
|
326
|
+
// chunk).
|
|
195
327
|
"speculation": { // only when a draft source is mounted
|
|
196
328
|
"drafted": 0, // draft tokens proposed
|
|
197
329
|
"accepted": 0, // drafts accepted by the verify step
|
|
198
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
|
|
199
349
|
}
|
|
200
350
|
}
|
|
201
351
|
}
|
|
@@ -208,18 +358,48 @@ contributed accepted draft tokens to this reply, `"serial"` otherwise. It
|
|
|
208
358
|
rides the SAME usage block on both streaming (final chunk before `[DONE]`) and
|
|
209
359
|
non-streaming responses.
|
|
210
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
|
+
|
|
211
368
|
Speculative decoding is a **server-level mode** (`serve --draft-model`,
|
|
212
369
|
or the model-free `serve --draft-kind ngram`, which mounts no draft
|
|
213
370
|
model); there is no per-request draft field. The `speculation` usage
|
|
214
|
-
extension
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
normally and omit the field. The spec path bypasses
|
|
219
|
-
|
|
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
|
|
220
379
|
through the serial lane (mlx_lm.server parity: `is_batchable = draft is
|
|
221
380
|
None`) — speculation and `--batch N` are different modes.
|
|
222
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
|
+
|
|
223
403
|
### logprobs / top_logprobs
|
|
224
404
|
|
|
225
405
|
With `"logprobs": true` and/or `"top_logprobs": k` (1–11), the
|
|
@@ -248,44 +428,68 @@ legacy `{tokens, token_logprobs}` text-completion shape).
|
|
|
248
428
|
|
|
249
429
|
The distribution matches mlx-lm `generate_step` exactly: full-vocab
|
|
250
430
|
log-softmax of the logits **after** logits processors (logit_bias,
|
|
251
|
-
penalties), **before** the sampler's temperature/top-p/top-k/min-p/XTC
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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.
|
|
268
461
|
|
|
269
462
|
Tool round-trip: send the assistant message with its `tool_calls` back,
|
|
270
463
|
followed by `{ "role": "tool", "tool_call_id": …, "content": … }`
|
|
271
464
|
messages; multi-turn prompt prefixes reuse the KV prompt cache
|
|
272
465
|
automatically.
|
|
273
466
|
|
|
274
|
-
Tool-call parsing is per model family
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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.
|
|
284
487
|
|
|
285
488
|
### Structured output (grammar-constrained decoding)
|
|
286
489
|
|
|
287
490
|
Both `/v1/chat/completions` and `/v1/completions` accept the
|
|
288
|
-
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`):
|
|
289
493
|
|
|
290
494
|
```jsonc
|
|
291
495
|
{
|
|
@@ -307,9 +511,19 @@ OpenAI/oMLX/vLLM structured-output fields (snake_case on the wire):
|
|
|
307
511
|
}
|
|
308
512
|
```
|
|
309
513
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
`
|
|
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.
|
|
313
527
|
|
|
314
528
|
Enforcement is a per-step token bitmask at the sampler (xgrammar):
|
|
315
529
|
invalid next tokens are masked to −inf **after** the logits processors,
|
|
@@ -323,33 +537,45 @@ Degrade path — a grammar that fails to compile is never rejected (no
|
|
|
323
537
|
requested JSON schema, raw grammar, regex, or choice constraint, and the
|
|
324
538
|
response carries a `Warning` header (`grammar not enforced: …`);
|
|
325
539
|
`/v1/completions` has no chat template to inject into, so it emits the
|
|
326
|
-
`Warning` header only.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
Design and fidelity notes:
|
|
331
|
-
[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).
|
|
332
544
|
|
|
333
545
|
### Errors
|
|
334
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
|
+
|
|
335
553
|
All errors are `{ "error": { "message": …, ... } }`.
|
|
336
554
|
|
|
337
|
-
- `400` — malformed JSON, empty `messages
|
|
338
|
-
|
|
339
|
-
without
|
|
340
|
-
|
|
341
|
-
non-
|
|
342
|
-
|
|
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
|
|
343
563
|
(mlx-lm's exact validation: `logprobs must be of type bool`,
|
|
344
564
|
`top_logprobs must be of type int` / `at least 0` / `at most 11`;
|
|
345
|
-
`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'`).
|
|
346
568
|
- `400` with `"type": "memory_admission"`, `"code":
|
|
347
|
-
"context_over_budget"` — the
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
to the
|
|
351
|
-
|
|
352
|
-
|
|
569
|
+
"context_over_budget"` — the PROMPT itself leaves no generation slot
|
|
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.
|
|
353
579
|
|
|
354
580
|
## POST /v1/completions (raw text completion)
|
|
355
581
|
|
|
@@ -361,17 +587,19 @@ and adapter selection as chat.
|
|
|
361
587
|
|
|
362
588
|
```jsonc
|
|
363
589
|
{
|
|
364
|
-
"prompt": "Once upon a time", // REQUIRED, string only (token
|
|
365
|
-
// rejected, matching mlx_lm.server)
|
|
366
|
-
"max_tokens": 512, //
|
|
367
|
-
//
|
|
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
|
|
368
596
|
"stream": false,
|
|
369
597
|
"stop": "\n\n", // string or array, decoded-text matching
|
|
370
598
|
// plus every sampling/penalty field from /v1/chat/completions:
|
|
371
599
|
// temperature, top_p, top_k, seed, min_p, xtc_probability,
|
|
372
600
|
// xtc_threshold, logit_bias, repetition_penalty,
|
|
373
601
|
// repetition_context_size, presence_penalty, presence_context_size,
|
|
374
|
-
// frequency_penalty, frequency_context_size, adapter,
|
|
602
|
+
// frequency_penalty, frequency_context_size, hlg, adapter,
|
|
375
603
|
// logprobs (BOOL, mlx-lm's type — not OpenAI's legacy int),
|
|
376
604
|
// top_logprobs (0-11 or -1) — same response block as chat, see
|
|
377
605
|
// "logprobs / top_logprobs" above (mlx-lm shares the builder),
|
|
@@ -389,9 +617,11 @@ Response is the OpenAI text-completion object:
|
|
|
389
617
|
{
|
|
390
618
|
"id": "cmpl-…", "object": "text_completion", "created": 1760000000,
|
|
391
619
|
"model": "<loaded model id>",
|
|
392
|
-
"choices": [{ "index": 0, "text": "…", "finish_reason": "stop" | "length"
|
|
620
|
+
"choices": [{ "index": 0, "text": "…", "finish_reason": "stop" | "length",
|
|
621
|
+
"logprobs": { /* only when requested */ } }],
|
|
393
622
|
"usage": { "prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0,
|
|
394
|
-
"prompt_tokens_details": { "cached_tokens": 0 }
|
|
623
|
+
"prompt_tokens_details": { "cached_tokens": 0 },
|
|
624
|
+
"lane": "serial" }
|
|
395
625
|
// + "speculation" under --draft-model, as on chat
|
|
396
626
|
}
|
|
397
627
|
```
|
|
@@ -401,83 +631,102 @@ Streaming (`"stream": true`) is SSE: `text_completion` chunks whose
|
|
|
401
631
|
`finish_reason` and `usage` (always attached — mlx-lm gates usage behind
|
|
402
632
|
`stream_options.include_usage`; ours is an additive superset), then
|
|
403
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.
|
|
404
636
|
|
|
405
637
|
## POST /v1/messages (Anthropic Messages API)
|
|
406
638
|
|
|
407
639
|
Anthropic-protocol surface over the same engine — on by default, like
|
|
408
640
|
`optiq serve`. Point any Anthropic-SDK tool at the server
|
|
409
641
|
(`ANTHROPIC_BASE_URL=http://localhost:8080`, any `x-api-key`) — Claude
|
|
410
|
-
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.
|
|
411
644
|
|
|
412
645
|
- `system` (string or text blocks), `messages` with string or
|
|
413
646
|
content-block arrays; `tool_use` / `tool_result` blocks map to the
|
|
414
|
-
native
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
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.
|
|
419
654
|
- `tools` (`{name, description, input_schema}`) map to function tools;
|
|
420
|
-
server-tool types
|
|
655
|
+
entries without `name` + `input_schema` (server-tool types such as
|
|
656
|
+
web_search) are dropped silently.
|
|
421
657
|
- `max_tokens`, `temperature`, `top_p`, `top_k`, `stop_sequences`,
|
|
422
658
|
`stream` as in the Anthropic spec. The mlx-lm sampler/penalty
|
|
423
659
|
extensions (`min_p`, `xtc_probability`, `xtc_threshold`,
|
|
424
660
|
`repetition_penalty`, `presence_penalty`, `frequency_penalty` + their
|
|
425
661
|
`*_context_size` windows) pass through as extras; `logit_bias` does
|
|
426
662
|
not exist in this protocol and is not accepted here.
|
|
427
|
-
- Response: `{id: "msg_…", type: "message",
|
|
428
|
-
{type: "tool_use"}…], stop_reason,
|
|
429
|
-
output_tokens, cache_read_input_tokens}}` —
|
|
430
|
-
`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.
|
|
431
667
|
- Streaming follows the Anthropic event grammar exactly:
|
|
432
668
|
`message_start → content_block_start/delta/stop (text_delta,
|
|
433
669
|
input_json_delta) → message_delta (stop_reason + usage) →
|
|
434
670
|
message_stop`. Errors are `event: error` frames.
|
|
435
671
|
- Errors: `{type: "error", error: {type: "invalid_request_error" |
|
|
436
|
-
"api_error", message}}
|
|
672
|
+
"api_error", message}}` — the chat handler's `4xx`/`5xx` are re-wrapped
|
|
673
|
+
into this shape.
|
|
437
674
|
|
|
438
675
|
## POST /v1/responses (OpenAI Responses API)
|
|
439
676
|
|
|
440
677
|
Responses-protocol surface (Codex, Cursor, Continue, Cline, and the
|
|
441
|
-
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`.
|
|
442
680
|
|
|
443
681
|
- `input` (string or item array: `message`, `function_call`,
|
|
444
682
|
`function_call_output`), `instructions` (merged with any
|
|
445
683
|
system/developer items into one leading system message),
|
|
446
|
-
`max_output_tokens
|
|
447
|
-
`tools`/`tool_choice` (built-in tool types
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
`
|
|
451
|
-
|
|
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.
|
|
452
692
|
- **`previous_response_id` resumption**: pass a prior response id
|
|
453
693
|
instead of resending the conversation; the server splices the stored
|
|
454
|
-
input + output back in (instructions carry forward when omitted
|
|
455
|
-
Store is per-process, 1 h TTL,
|
|
456
|
-
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`.
|
|
457
698
|
- Response: `{id: "resp_…", object: "response", status: "completed" |
|
|
458
|
-
"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.
|
|
459
704
|
- Streaming event chain: `response.created → response.in_progress →
|
|
460
705
|
response.output_item.added → response.content_part.added →
|
|
461
706
|
response.output_text.delta… → response.output_text.done →
|
|
462
707
|
response.content_part.done → response.output_item.done →
|
|
463
708
|
response.completed` (+ `response.function_call_arguments.delta/.done`
|
|
464
|
-
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}}`.
|
|
465
713
|
|
|
466
714
|
## POST /v1/embeddings (OpenAI Embeddings API)
|
|
467
715
|
|
|
468
716
|
Text embeddings, available when the **served model is an embedding model**
|
|
469
717
|
(plain Qwen3 / Qwen3-Embedding) — consistent with the single-model server:
|
|
470
718
|
`mlx-bun serve mlx-community/Qwen3-Embedding-4B-4bit-DWQ`, then POST here. On
|
|
471
|
-
any other served model the route returns `400 invalid_request_error
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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.
|
|
478
727
|
- Pooling is last-token + L2-normalization (the vectors are unit-length);
|
|
479
728
|
similarity is the dot product. Bit-exact vs the mlx-lm `qwen3` reference
|
|
480
|
-
(`tests/qwen3-embed-parity.test.ts`).
|
|
729
|
+
(`tests/parity/qwen3-embed-parity.test.ts`).
|
|
481
730
|
- Response: `{ "object": "list", "data": [{ "object": "embedding", "index":
|
|
482
731
|
0, "embedding": [float…] }, …], "model": "<served id>", "usage": {
|
|
483
732
|
"prompt_tokens": N, "total_tokens": N } }`. Embedding is a single forward
|
|
@@ -488,16 +737,31 @@ curl localhost:8080/v1/embeddings -H 'content-type: application/json' \
|
|
|
488
737
|
-d '{"input": ["the cat sat on the mat", "a kitten by the window"]}'
|
|
489
738
|
```
|
|
490
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
|
+
|
|
491
754
|
## GET /v1/models
|
|
492
755
|
|
|
493
756
|
`{ "object": "list", "data": [{ "id": "<model id>", "object": "model",
|
|
494
|
-
"created": <unix s>, … }] }`
|
|
757
|
+
"created": <unix s>, … }] }` — `created` is the server start time for every
|
|
758
|
+
row.
|
|
495
759
|
|
|
496
760
|
The served model is FIRST, with extra capability fields:
|
|
497
761
|
|
|
498
762
|
```jsonc
|
|
499
763
|
{
|
|
500
|
-
"id": "<served model id>", "object": "model", "created":
|
|
764
|
+
"id": "<served model id>", "object": "model", "created": 1760000000,
|
|
501
765
|
"owned_by": "mlx-bun",
|
|
502
766
|
"context_window": 32768,
|
|
503
767
|
"reasoning": false, // switchable thinking channel (gates the
|
|
@@ -538,7 +802,7 @@ architectures, one row per repo via `listCanonical()` — duplicate snapshots
|
|
|
538
802
|
from upstream re-pushes are not separate entries):
|
|
539
803
|
|
|
540
804
|
```jsonc
|
|
541
|
-
{ "id": "<repo id>", "object": "model", "created":
|
|
805
|
+
{ "id": "<repo id>", "object": "model", "created": 1760000000,
|
|
542
806
|
"vision": false, // visionCapable(): sidecar OR encoder-free
|
|
543
807
|
// unified vision config
|
|
544
808
|
"tier": "targeted" | "generic" // supportTier(): dedicated/generated forward
|
|
@@ -574,14 +838,16 @@ vision/audio, adapters, and training are not emulated by the serving port.
|
|
|
574
838
|
"server": { "owner": "serve" | "pi-session" | "embedded", "model": "...", "started_at": 0 },
|
|
575
839
|
"prompt_cache": { "entries": 0, "bytes": 0, "max_bytes": 0, "hits": 0, "misses": 0 },
|
|
576
840
|
"response_store": { "entries": 0, "bytes": 0, "max_bytes": 33554432, "ttl_ms": 3600000 },
|
|
577
|
-
"kv_quant": { "mode": "
|
|
841
|
+
"kv_quant": { "mode": "bf16" | "uniform-kv8" | "turbo k8v3" | "mixed (kv_config.json)",
|
|
578
842
|
"layers": { "kv4": 8, "bf16": 40 }, // turbo: { "turbo-k8v3": 8, "bf16": 40 }
|
|
579
843
|
"attention": { "global": 10, "sliding_window": 38 } },
|
|
580
844
|
// present only when --ssd-cache is on:
|
|
581
845
|
"ssd_cache": { "dir": "...", "entries": 0, "bytes": 0, "max_bytes": 0,
|
|
582
846
|
"restores": 0, "spills": 0, "restore_ms_last": 0, "demotions": 0,
|
|
583
847
|
// bounded write-behind queue (MLX_BUN_SSD_SPILL_QUEUE_GB):
|
|
584
|
-
"
|
|
848
|
+
"pending_snapshots": 0, "pending_spills": 0,
|
|
849
|
+
"pending_spill_bytes": 0, "dropped_spills": 0,
|
|
850
|
+
"failed_spills": 0, "longest_durable_prefix_tokens": 0 },
|
|
585
851
|
"admission": {
|
|
586
852
|
"max_safe_context": 0, // tokens; requests above this 400
|
|
587
853
|
"memory_budget_bytes": null, // explicit budget, or null (machine default)
|
|
@@ -602,18 +868,13 @@ vision/audio, adapters, and training are not emulated by the serving port.
|
|
|
602
868
|
"resident_weight_bytes": 0,
|
|
603
869
|
"main_expert_slab_bytes": 0,
|
|
604
870
|
"mtp_expert_slab_bytes": 0,
|
|
605
|
-
"expert_runtime": {
|
|
871
|
+
"expert_runtime": { // null when the expert runtime is off
|
|
606
872
|
"main_residency": { "resident": 0, "pinned": 0, "working": 0,
|
|
607
873
|
"hits": 0, "misses": 0, "evictions": 0 },
|
|
608
|
-
"mtp_residency": {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
"readBytes": 0 } },
|
|
613
|
-
"mtp": { "demand": { "hits": 0, "misses": 0,
|
|
614
|
-
"readBytes": 0 } },
|
|
615
|
-
"pilot": null
|
|
616
|
-
},
|
|
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 },
|
|
617
878
|
"last_repin": []
|
|
618
879
|
}
|
|
619
880
|
},
|
|
@@ -631,12 +892,152 @@ vision/audio, adapters, and training are not emulated by the serving port.
|
|
|
631
892
|
}
|
|
632
893
|
```
|
|
633
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
|
+
|
|
901
|
+
`pending_snapshots` counts prompt-cache entries scheduled for persistence but
|
|
902
|
+
not yet confirmed by the atomic SSD store. `longest_durable_prefix_tokens` is
|
|
903
|
+
the longest prefix already present in the SSD index, not merely queued.
|
|
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
|
+
|
|
1001
|
+
## POST /admin/cache/flush
|
|
1002
|
+
|
|
1003
|
+
Forces every pending SSD prompt-cache snapshot through the serial write queue
|
|
1004
|
+
and waits for the atomic temp-file/fsync/rename boundary. The response is `200`
|
|
1005
|
+
only when no snapshot or spill remains pending and no write failed during this
|
|
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.
|
|
1011
|
+
|
|
1012
|
+
```jsonc
|
|
1013
|
+
{
|
|
1014
|
+
"durable": true,
|
|
1015
|
+
"flushedSnapshots": 1,
|
|
1016
|
+
"missingSnapshots": 0,
|
|
1017
|
+
"pendingSnapshots": 0,
|
|
1018
|
+
"pendingSpills": 0,
|
|
1019
|
+
"pendingSpillBytes": 0,
|
|
1020
|
+
"droppedSpills": 0,
|
|
1021
|
+
"failedSpills": 0,
|
|
1022
|
+
"elapsedMs": 842,
|
|
1023
|
+
"entries": 2,
|
|
1024
|
+
"longest_durable_prefix_tokens": 16384
|
|
1025
|
+
}
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
The standard benchmark calls this endpoint before its restart leg and records
|
|
1029
|
+
the result. Normal `mlx-bun serve` shutdown on `SIGINT` or `SIGTERM` performs
|
|
1030
|
+
the same flush after active requests drain, with a 120-second default timeout.
|
|
1031
|
+
Without `--ssd-cache` the route still answers (`entries: 0`).
|
|
1032
|
+
|
|
634
1033
|
## GET /library
|
|
635
1034
|
|
|
1035
|
+
`?refresh=1` bypasses the short response cache, including after a parent-managed job finishes.
|
|
1036
|
+
|
|
636
1037
|
Returns all models found in the local HuggingFace hub cache (via the
|
|
637
1038
|
registry scan), each annotated with a fit assessment for this machine.
|
|
638
1039
|
Response is cached for 30 seconds (registry scan + config reads; no
|
|
639
|
-
tensor bytes are read).
|
|
1040
|
+
tensor bytes are read). Handler: `src/serve/discovery-routes.ts`.
|
|
640
1041
|
|
|
641
1042
|
```jsonc
|
|
642
1043
|
{
|
|
@@ -665,6 +1066,203 @@ tensor bytes are read).
|
|
|
665
1066
|
One row per repo (`listCanonical()`; duplicate snapshots from upstream
|
|
666
1067
|
re-pushes surface only via the CLI's `mlx-bun ls --all-revisions`).
|
|
667
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
|
+
|
|
668
1266
|
## GET /api/gc/plan · POST /api/gc/execute
|
|
669
1267
|
|
|
670
1268
|
Reclaim disk from superseded snapshots + orphaned blobs in the local HF
|
|
@@ -709,13 +1307,12 @@ Errors follow the same `{ "ok": false, "error": "…" }` shape as the other
|
|
|
709
1307
|
## GET /api/memory/\* · POST /api/memory/init
|
|
710
1308
|
|
|
711
1309
|
Thin loopback JSON wrappers over `src/memory/vault.ts` for the web chat's
|
|
712
|
-
Memory panel
|
|
713
|
-
|
|
714
|
-
`src/memory/
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
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.
|
|
719
1316
|
|
|
720
1317
|
When no vault exists yet, every `GET` route (except `/init`) returns:
|
|
721
1318
|
|
|
@@ -737,25 +1334,23 @@ separate (mirrors `memory_list`):
|
|
|
737
1334
|
```
|
|
738
1335
|
|
|
739
1336
|
`GET /api/memory/search?q=<query>&scope=all|articles|reference&limit=50` —
|
|
740
|
-
wraps `searchArticles
|
|
741
|
-
formats from:
|
|
1337
|
+
wraps `searchArticles` (`limit` capped at 200; `400` without `q`); same
|
|
1338
|
+
summaries/hits shape the `memory_search` tool formats from:
|
|
742
1339
|
|
|
743
1340
|
```jsonc
|
|
744
1341
|
{ "ok": true, "summaries": [{ "article": "…", "occurrences": 3, "matched_terms": ["…"] }], "hits": [{ "article": "…", "anchor": "…", "line": 10, "excerpt": "…" }] }
|
|
745
1342
|
```
|
|
746
1343
|
|
|
747
1344
|
`GET /api/memory/article?name=<article>` — rendered source plus the
|
|
748
|
-
deterministic metadata the panel needs
|
|
749
|
-
|
|
750
|
-
`{ "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:
|
|
751
1347
|
|
|
752
1348
|
```jsonc
|
|
753
1349
|
{ "ok": true, "name": "…", "path": "…", "content": "# …", "infobox": { "type": "…", "entityKind": "thing", "fields": [] }, "lead": "…", "series": null, "structure": [] }
|
|
754
1350
|
```
|
|
755
1351
|
|
|
756
1352
|
`GET /api/memory/links?name=<article>` — inbound/outbound wikilinks (same
|
|
757
|
-
data `memory_links` resolves); articles only, not `Reference
|
|
758
|
-
the tool's own scope):
|
|
1353
|
+
data `memory_links` resolves); articles only, not `Reference/*`:
|
|
759
1354
|
|
|
760
1355
|
```jsonc
|
|
761
1356
|
{ "ok": true, "name": "…", "outbound": ["…"], "inbound": ["…"] }
|
|
@@ -763,7 +1358,7 @@ the tool's own scope):
|
|
|
763
1358
|
|
|
764
1359
|
`GET /api/memory/history?name=<article>&limit=50` — the article's git log
|
|
765
1360
|
(read-only `git log` plumbing on the vault's own repo, via `Bun.spawn` —
|
|
766
|
-
never a shell string):
|
|
1361
|
+
never a shell string; `limit` capped at 200):
|
|
767
1362
|
|
|
768
1363
|
```jsonc
|
|
769
1364
|
{ "ok": true, "name": "…", "isGitRepo": true, "entries": [{ "hash": "…", "date": "YYYY-MM-DD", "subject": "…" }] }
|
|
@@ -772,99 +1367,78 @@ never a shell string):
|
|
|
772
1367
|
`GET /api/memory/diff?name=<article>&rev=<hash>` — that commit's diff for
|
|
773
1368
|
the article (`git show <rev> -- <path>`). `rev` must match
|
|
774
1369
|
`/^[0-9a-f]{4,40}$/` (a bare commit hash — never a ref expression like
|
|
775
|
-
`HEAD~1`
|
|
776
|
-
|
|
777
|
-
uses, so a path-traversal or otherwise-invalid name 404s rather than
|
|
778
|
-
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:
|
|
779
1372
|
|
|
780
1373
|
```jsonc
|
|
781
1374
|
{ "ok": true, "name": "…", "rev": "…", "diff": "diff --git a/articles/…" }
|
|
782
1375
|
```
|
|
783
1376
|
|
|
784
1377
|
`POST /api/memory/init` with optional body `{"path"?: string}` — the
|
|
785
|
-
first-run consent-card backend. Delegates to the
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
the default vault root or the OS temp directory — `setupVault()` mkdir's
|
|
791
|
-
into it and `git init`s it if it isn't already a repo, so anything outside
|
|
792
|
-
those trees 400s before touching the filesystem rather than letting a
|
|
793
|
-
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`:
|
|
794
1383
|
|
|
795
1384
|
```jsonc
|
|
796
1385
|
{ "ok": true, "result": { "root": "…", "created": ["…"], "gitInitialized": true, "alreadySetUp": false }, "status": { "root": "…", "exists": true, "articleCount": 0, "referenceCount": 9, "isGitRepo": true, "recentArticles": [] } }
|
|
797
1386
|
```
|
|
798
1387
|
|
|
799
|
-
## GET /
|
|
800
|
-
|
|
801
|
-
The web chat's durable "always allow this tool" list for gated tools
|
|
802
|
-
(`bash`/`edit`/`write`) — docs/design/web-chat-redesign.md §5.4/§6.5/§9
|
|
803
|
-
Phase 2, risk #6. Backed by `~/.mlx-bun/tool-approvals.json`
|
|
804
|
-
(`src/tool-approvals.ts`), a versioned `{version, allows}` file (same
|
|
805
|
-
plain-JSON-at-0600 convention as `~/.mlx-bun/hf.json`) keyed by stable tool
|
|
806
|
-
NAME, not per-call arguments — "always allow bash" means every future
|
|
807
|
-
`bash` call skips the browser's approval card, on every chat and every
|
|
808
|
-
browser tab, until forgotten. These two routes are read/forget only:
|
|
809
|
-
granting an always-allow happens through the approval card itself (the
|
|
810
|
-
`approval` WS frame on `/ws/chat`, `alwaysAllow: true`), never a bare
|
|
811
|
-
settings-form POST.
|
|
1388
|
+
## GET /v1/memory/synthesize
|
|
812
1389
|
|
|
813
|
-
`
|
|
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.
|
|
814
1395
|
|
|
815
|
-
```jsonc
|
|
816
|
-
{ "ok": true, "alwaysAllow": ["bash", "edit"] }
|
|
817
1396
|
```
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
```jsonc
|
|
824
|
-
{ "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]
|
|
825
1402
|
```
|
|
826
1403
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
section uses (see the `upload` CLI command in [cli.md](./cli.md) for the
|
|
830
|
-
push-to-hub flow this token drives).
|
|
1404
|
+
A failure emits `data: {"type":"error","message":"…"}` and closes without
|
|
1405
|
+
`[DONE]`.
|
|
831
1406
|
|
|
832
|
-
##
|
|
1407
|
+
## Settings (`/api/settings/*`)
|
|
833
1408
|
|
|
834
|
-
|
|
835
|
-
|
|
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.
|
|
836
1422
|
|
|
837
1423
|
```jsonc
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
"current_file": "model.safetensors" | null,
|
|
843
|
-
"received_bytes": 0,
|
|
844
|
-
"total_bytes": 0,
|
|
845
|
-
"files_done": 0,
|
|
846
|
-
"files_total": 0,
|
|
847
|
-
"bytes_per_sec": 0, // rolling ~5 s window
|
|
848
|
-
"started_at": 1760000000,
|
|
849
|
-
"finished_at": null,
|
|
850
|
-
"error": "…" // present on state "error"
|
|
851
|
-
}]
|
|
852
|
-
}
|
|
1424
|
+
// GET
|
|
1425
|
+
{ "ok": true, "alwaysAllow": ["bash", "edit"] }
|
|
1426
|
+
// DELETE with body {"tool": "bash"} — idempotent; returns the updated set
|
|
1427
|
+
{ "ok": true, "alwaysAllow": ["edit"] }
|
|
853
1428
|
```
|
|
854
1429
|
|
|
1430
|
+
`DELETE` without `tool` is `400 "tool required"`.
|
|
1431
|
+
|
|
855
1432
|
## Model Hub (`/api/hub/*`)
|
|
856
1433
|
|
|
857
|
-
The web chat's Model Hub panel
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
functions, no loaded-model dependency); route dispatch in `src/server.ts`
|
|
862
|
-
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).
|
|
863
1438
|
|
|
864
1439
|
`GET /api/hub/local` — every model in the local registry (same source
|
|
865
1440
|
`/library` reads), each with a `/fit`-computed hardware verdict at a fixed
|
|
866
|
-
8k context
|
|
867
|
-
Hub lacks:
|
|
1441
|
+
8k context:
|
|
868
1442
|
|
|
869
1443
|
```jsonc
|
|
870
1444
|
{
|
|
@@ -884,11 +1458,10 @@ Hub lacks:
|
|
|
884
1458
|
```
|
|
885
1459
|
|
|
886
1460
|
`GET /api/hub/search?q=<query>` — server-side Hugging Face model search,
|
|
887
|
-
filtered to the `mlx` library tag
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
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:
|
|
892
1465
|
|
|
893
1466
|
```jsonc
|
|
894
1467
|
{ "ok": true, "offline": false, "results": [{ "id": "mlx-community/…", "downloads": 0, "likes": 0, "size_estimate": null }] }
|
|
@@ -898,69 +1471,51 @@ explicit offline state rather than a 500:
|
|
|
898
1471
|
{ "ok": true, "offline": true, "error": "…", "results": [] }
|
|
899
1472
|
```
|
|
900
1473
|
|
|
901
|
-
`size_estimate` is always `null`
|
|
902
|
-
per-repo file sizes
|
|
903
|
-
|
|
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`).
|
|
904
1477
|
|
|
905
1478
|
`POST /api/hub/download` with body `{"repo": "org/name"}` — starts a
|
|
906
|
-
background download via
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
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:
|
|
910
1482
|
|
|
911
1483
|
```jsonc
|
|
912
1484
|
{ "ok": true, "repo": "org/name", "started": true }
|
|
913
1485
|
```
|
|
914
1486
|
|
|
915
1487
|
```jsonc
|
|
916
|
-
{ "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
|
|
917
1489
|
```
|
|
918
|
-
(409)
|
|
919
1490
|
|
|
920
1491
|
`POST /api/hub/serve` with body `{"model": "org/name"}` — **always answers
|
|
921
|
-
`restart_required`, never performs a live in-process swap.**
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
(The `--isolate` proxy's `ModelPool`, P2 of that doc, *is* a real
|
|
928
|
-
spawn-overlap live-swap mechanism — it just lives entirely on the isolated
|
|
929
|
-
`/v1/*` HTTP surface, not on `/ws/chat`.) Faking a swap here would leave the
|
|
930
|
-
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`):
|
|
931
1498
|
|
|
932
1499
|
```jsonc
|
|
933
1500
|
{ "ok": false, "restart_required": true, "command": "mlx-bun serve org/name" }
|
|
934
1501
|
```
|
|
935
1502
|
|
|
936
|
-
Wiring `/ws/chat` through the isolate proxy (or otherwise reaching
|
|
937
|
-
`ModelPool`'s spawn-overlap swap from the web chat) is the Hub's live-swap
|
|
938
|
-
follow-up, not yet scheduled.
|
|
939
|
-
|
|
940
1503
|
## GET /api/sessions/search · GET /api/sessions/export
|
|
941
1504
|
|
|
942
1505
|
Full-text search across web-chat session message **bodies** and chat
|
|
943
|
-
export
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
`
|
|
955
|
-
|
|
956
|
-
the upgrade path if the corpus ever grows enough to matter) over every
|
|
957
|
-
session file's user/assistant message text, capped at 50 matches total (10
|
|
958
|
-
per session). Each match reports a plain-text ±60-char snippet plus the
|
|
959
|
-
match's own `[start,end)` offsets **into that snippet** — offsets, not
|
|
960
|
-
pre-rendered HTML highlighting, so the frontend escapes the snippet then
|
|
961
|
-
inserts its own `<mark>` (the same escape-then-restore discipline the
|
|
962
|
-
markdown renderer uses for code spans, applied here to search
|
|
963
|
-
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>`:
|
|
964
1519
|
|
|
965
1520
|
```jsonc
|
|
966
1521
|
{
|
|
@@ -976,15 +1531,8 @@ highlighting instead):
|
|
|
976
1531
|
400s `{"ok": false, "error": "q is required"}` without a query.
|
|
977
1532
|
|
|
978
1533
|
`GET /api/sessions/export?path=<session file>` — the raw session JSONL,
|
|
979
|
-
parsed into an array of entries (one per line, in file order)
|
|
980
|
-
|
|
981
|
-
the currently-open one (the open session's own history is already loaded
|
|
982
|
-
client-side, but this endpoint is used uniformly for both cases so there's
|
|
983
|
-
one code path, not two). `path` must resolve under the session directory —
|
|
984
|
-
exactly the same guard shape as `PiWebSession`'s private
|
|
985
|
-
`isUnderSessionDir` (the root itself, or a path prefixed by `root + "/"`)
|
|
986
|
-
— reimplemented as a small standalone check in `session-search.ts` rather
|
|
987
|
-
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:
|
|
988
1536
|
|
|
989
1537
|
```jsonc
|
|
990
1538
|
{ "ok": true, "path": "…", "entries": [{ "type": "session", "id": "…", "cwd": "…" }, { "type": "message", "message": { "role": "user", "content": "…" } }] }
|
|
@@ -995,153 +1543,118 @@ path inside the directory that doesn't exist on disk.
|
|
|
995
1543
|
|
|
996
1544
|
## Web app static routes + PWA installability
|
|
997
1545
|
|
|
998
|
-
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):
|
|
999
1548
|
|
|
1000
1549
|
| Route | Content | Notes |
|
|
1001
1550
|
| --- | --- | --- |
|
|
1002
1551
|
| `GET /` | `src/web/app.html` | The unified SPA shell. |
|
|
1003
|
-
| `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. |
|
|
1004
|
-
| `GET /assets/hljs.js`, `GET /assets/hljs.css` | `src/web/vendor/hljs*` | Vendored syntax highlighting, no CDN
|
|
1005
|
-
| `GET /manifest.webmanifest` | `src/web/manifest.webmanifest` | PWA manifest
|
|
1006
|
-
| `GET /assets/icon.svg` | `src/web/icon.svg` | A single inline SVG app icon
|
|
1007
|
-
| `GET /sw.js` | `src/web/sw.js` | Service worker, served `cache-control: no-store` at the root scope
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
the
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
`
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
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`.
|
|
1041
1595
|
|
|
1042
1596
|
```jsonc
|
|
1043
|
-
{
|
|
1044
|
-
"
|
|
1045
|
-
"
|
|
1046
|
-
"
|
|
1047
|
-
"typical_decode_tps": 0.0, // predicted at typical_context_tokens
|
|
1048
|
-
"measured_decode_tps": null, // real number from eval DB, or null
|
|
1049
|
-
"measured_at": null, // unix ms of measurement, or null
|
|
1050
|
-
"report": {
|
|
1051
|
-
"fits": true,
|
|
1052
|
-
"weights_bytes": 0,
|
|
1053
|
-
"kv_bytes": 0,
|
|
1054
|
-
"transient_bytes": 0,
|
|
1055
|
-
"total_bytes": 0,
|
|
1056
|
-
"usable_bytes": 0,
|
|
1057
|
-
"max_safe_context": 8192,
|
|
1058
|
-
"predicted_decode_tps": 0.0
|
|
1059
|
-
},
|
|
1060
|
-
// GLM-5.2 only:
|
|
1061
|
-
"glm52": {
|
|
1062
|
-
"artifact_disk_bytes": 0, // exact registry size; null if unavailable
|
|
1063
|
-
"main_expert_slab_bytes": 0,
|
|
1064
|
-
"mtp_expert_slab_bytes": 0,
|
|
1065
|
-
"max_generation_tokens": 128,
|
|
1066
|
-
"direct_oracle_warm_decode_tps": 0.27,
|
|
1067
|
-
"aspirational_decode_tps": 2.0
|
|
1068
|
-
},
|
|
1069
|
-
"sku_matrix_ctx": 32768,
|
|
1070
|
-
"sku_matrix": [{
|
|
1071
|
-
"sku": "M4 Pro 24 GB", "ram_gb": 24,
|
|
1072
|
-
"fits": true, "max_context": 32768, "decode_tps": 0.0
|
|
1073
|
-
}]
|
|
1074
|
-
}
|
|
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
|
|
1075
1601
|
```
|
|
1076
1602
|
|
|
1077
|
-
##
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
The web chat agent can see and act on the app it lives in —
|
|
1120
|
-
`docs/design/web-chat-redesign.md` §6.6, beat matrix Axis 12. No
|
|
1121
|
-
screenshots and no vision model are involved: the browser sends a
|
|
1122
|
-
structured DOM snapshot as a WS frame, and the model gets three tools that
|
|
1123
|
-
read/navigate/highlight that snapshot.
|
|
1124
|
-
|
|
1125
|
-
Three tools join the web chat's tool allowlist unconditionally (same
|
|
1126
|
-
class as `web_search`/`read` — read-only-on-the-machine, never gated by
|
|
1127
|
-
the approval card, regardless of memory or `codingTools` state; see
|
|
1128
|
-
`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`):
|
|
1129
1645
|
|
|
1130
1646
|
- **`get_current_app_context`** — returns the last context the browser
|
|
1131
1647
|
pushed: `{ route, view?, step?, snapshot }`. `snapshot` is a capped
|
|
1132
1648
|
(~120) list of visible interactive elements as `{ref, label, kind,
|
|
1133
1649
|
role?, selector, spotlightId?}`, agent chrome excluded.
|
|
1134
1650
|
- **`navigate_app({ route | page })`** — validates against the route
|
|
1135
|
-
catalog (`chat`, `quantize`, `finetune`, `dataset`,
|
|
1136
|
-
routes
|
|
1137
|
-
emits a `ui_navigate` frame.
|
|
1138
|
-
- **`spotlight_ui({ ref | label | selector | target, route?, message?
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
blocks: the overlay traps no focus and passes clicks straight through
|
|
1143
|
-
to the highlighted control, auto-dismisses in ~3s, and any keystroke,
|
|
1144
|
-
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.
|
|
1145
1658
|
|
|
1146
1659
|
Client → server WS frame (pushed by the browser on every route change and
|
|
1147
1660
|
every wizard-step change, never on a timer):
|
|
@@ -1159,10 +1672,8 @@ Server → client frames the two action tools produce:
|
|
|
1159
1672
|
|
|
1160
1673
|
The server also auto-prepends a compact one-line ambient context (e.g.
|
|
1161
1674
|
`[user is on: Quantize · step 2/4]`) to the system prompt on every turn —
|
|
1162
|
-
never a full snapshot dump
|
|
1163
|
-
|
|
1164
|
-
and job-start actions) is explicitly out of scope for this v1; only
|
|
1165
|
-
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.
|
|
1166
1677
|
|
|
1167
1678
|
## Client setup: pi
|
|
1168
1679
|
|
|
@@ -1203,3 +1714,8 @@ solve.
|
|
|
1203
1714
|
|
|
1204
1715
|
Any OpenAI SDK works the same way: `baseURL: "http://127.0.0.1:8080/v1"`,
|
|
1205
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.
|