mlx-bun 0.3.0 → 0.5.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.
Files changed (231) hide show
  1. package/README.md +57 -32
  2. package/docs/README.md +58 -42
  3. package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +3617 -0
  4. package/docs/archive/investigations/openjarvis-fit.md +198 -0
  5. package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
  6. package/docs/archive/planning/release-notes-v0.4.0.md +45 -0
  7. package/docs/archive/planning/release-notes-v0.5.0.md +37 -0
  8. package/docs/design/batching.md +1535 -50
  9. package/docs/design/decode-speed-program.md +394 -1171
  10. package/docs/design/generic-model-support.md +195 -3
  11. package/docs/design/kv-cache.md +538 -225
  12. package/docs/design/speculative-decoding.md +552 -50
  13. package/docs/design/turboquant.md +140 -17
  14. package/docs/design/unified-engine-frontier-plan.md +259 -150
  15. package/docs/planning/PRODUCT_ROADMAP.md +21 -6
  16. package/docs/planning/public-readiness-handoff.md +244 -0
  17. package/docs/planning/public-readiness.md +149 -0
  18. package/docs/reference/benchmarks.md +3537 -1112
  19. package/docs/reference/cli.md +102 -2
  20. package/docs/reference/distribution.md +45 -13
  21. package/docs/reference/environment.md +159 -13
  22. package/docs/reference/library-api.md +18 -4
  23. package/docs/reference/memory.md +1 -1
  24. package/docs/reference/models.md +28 -7
  25. package/docs/reference/server-api.md +257 -33
  26. package/docs/reference/server-config.md +372 -151
  27. package/docs/reference/troubleshooting.md +13 -21
  28. package/package.json +2 -2
  29. package/src/audio/audiotoolbox.ts +112 -0
  30. package/src/audio/conformer.ts +8 -0
  31. package/src/audio/mic-capture.ts +98 -0
  32. package/src/audio/silero-vad.ts +379 -0
  33. package/src/audio/whisper-decode.ts +774 -0
  34. package/src/audio/whisper-fast.ts +732 -0
  35. package/src/audio/whisper-format.ts +73 -0
  36. package/src/audio/whisper-mel.ts +185 -0
  37. package/src/audio/whisper-timing.ts +274 -0
  38. package/src/audio/whisper-tokenizer.ts +239 -0
  39. package/src/audio/whisper-transcribe.ts +410 -0
  40. package/src/backends/mlx/assistant-target.ts +46 -0
  41. package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
  42. package/src/backends/mlx/autoregressive.ts +22 -1
  43. package/src/backends/mlx/batch-group.ts +464 -236
  44. package/src/backends/mlx/cache-attachment.ts +67 -0
  45. package/src/backends/mlx/cache-layout.ts +31 -0
  46. package/src/backends/mlx/checkpoint-state.ts +53 -0
  47. package/src/backends/mlx/continuation-persistence.ts +107 -0
  48. package/src/backends/mlx/continuation-request.ts +43 -0
  49. package/src/backends/mlx/continuation.ts +77 -0
  50. package/src/backends/mlx/draft-target.ts +7 -22
  51. package/src/backends/mlx/encoder-cache.ts +25 -0
  52. package/src/backends/mlx/fill-append.ts +21 -8
  53. package/src/backends/mlx/fill-group.ts +497 -0
  54. package/src/backends/mlx/full-kv-row-append.ts +32 -0
  55. package/src/backends/mlx/full-kv-row-donor.ts +54 -0
  56. package/src/backends/mlx/full-transitioning-kv-rows.ts +65 -0
  57. package/src/backends/mlx/gateway-binding.ts +122 -19
  58. package/src/backends/mlx/kv-codec.ts +9 -0
  59. package/src/backends/mlx/kv-maintenance.ts +161 -0
  60. package/src/backends/mlx/kv-tensor-rows.ts +212 -0
  61. package/src/backends/mlx/mixed-iteration.ts +62 -0
  62. package/src/backends/mlx/model-prompt.ts +46 -32
  63. package/src/backends/mlx/model-serving.ts +5 -4
  64. package/src/backends/mlx/paged-cache-codec.ts +27 -0
  65. package/src/backends/mlx/prefill-cohort.ts +100 -0
  66. package/src/backends/mlx/prefill-policy.ts +26 -0
  67. package/src/backends/mlx/prefill-rows.ts +244 -0
  68. package/src/backends/mlx/prefill.ts +1 -1
  69. package/src/backends/mlx/projected-context-rows.ts +72 -0
  70. package/src/backends/mlx/prompt-input.ts +29 -0
  71. package/src/backends/mlx/qwen-prompt-input.ts +40 -0
  72. package/src/backends/mlx/request-state-policy.ts +54 -0
  73. package/src/backends/mlx/rollback.ts +45 -4
  74. package/src/backends/mlx/row-sampling.ts +49 -0
  75. package/src/backends/mlx/serial-executor.ts +51 -73
  76. package/src/backends/mlx/speculative-group.ts +438 -0
  77. package/src/backends/mlx/speculative-round.ts +161 -0
  78. package/src/backends/mlx/speculative.ts +24 -7
  79. package/src/backends/mlx/state-rows.ts +104 -0
  80. package/src/backends/mlx/state-views.ts +21 -1
  81. package/src/backends/mlx/target-layout-capability.ts +35 -0
  82. package/src/backends/mlx/transitioning-kv-rows.ts +173 -0
  83. package/src/cli.ts +333 -36
  84. package/src/contracts/execution.ts +21 -0
  85. package/src/contracts/object-cache.ts +8 -0
  86. package/src/contracts/pi-provider.ts +1 -1
  87. package/src/contracts/pi.ts +3 -1
  88. package/src/contracts/prefix-cache.ts +33 -0
  89. package/src/contracts/resources.ts +7 -0
  90. package/src/contracts/sampling.ts +23 -0
  91. package/src/contracts/scheduling.ts +27 -0
  92. package/src/engine/execution-plan.ts +38 -25
  93. package/src/engine/execution-tasks.ts +44 -0
  94. package/src/engine/namespaced-cache.ts +13 -0
  95. package/src/engine/resources.ts +12 -1
  96. package/src/engine/scheduler.ts +32 -7
  97. package/src/fill/fill-session.ts +14 -3
  98. package/src/fit.ts +29 -15
  99. package/src/generate.ts +105 -195
  100. package/src/grammar.ts +13 -0
  101. package/src/inference/draft-acceptance.ts +41 -0
  102. package/src/inference/draft-output.ts +49 -0
  103. package/src/inference/prefill.ts +6 -0
  104. package/src/inference/rollback.ts +2 -2
  105. package/src/kv-scheme.ts +92 -12
  106. package/src/kv-store.ts +183 -93
  107. package/src/lab/README.md +5 -1
  108. package/src/lab/paged-kv/paged-attention.ts +117 -0
  109. package/src/lab/paged-kv/paged-kv-rows.ts +111 -0
  110. package/src/lab/paged-kv/paged-kv.ts +126 -46
  111. package/src/md-modules.d.ts +5 -0
  112. package/src/mlx/array.ts +39 -1
  113. package/src/mlx/ffi.ts +19 -0
  114. package/src/mlx/normalized-argmax.ts +116 -0
  115. package/src/mlx/ops.ts +97 -10
  116. package/src/model/artifact-identity.ts +73 -0
  117. package/src/model/batched-kv.ts +235 -0
  118. package/src/model/batched-mask.ts +12 -4
  119. package/src/model/batched-quant.ts +8 -2
  120. package/src/model/batched-quantized-kv.ts +144 -0
  121. package/src/model/batched-rotating-quant.ts +76 -15
  122. package/src/model/batched-rotating-state.ts +76 -0
  123. package/src/model/batched-rotating.ts +64 -22
  124. package/src/model/batched-row-storage.ts +52 -0
  125. package/src/model/batched-ssm.ts +52 -0
  126. package/src/model/batched-turboquant-kv.ts +94 -0
  127. package/src/model/compiled-decode.ts +7 -1
  128. package/src/model/decoded-kv-donor.ts +12 -0
  129. package/src/model/delayed-quantized-kv.ts +72 -0
  130. package/src/model/delayed-rotating-quantized-kv.ts +93 -0
  131. package/src/model/delayed-turboquant-kv.ts +57 -0
  132. package/src/model/factory.ts +26 -0
  133. package/src/model/fingerprint.ts +12 -0
  134. package/src/model/full-prefill-padding.ts +56 -0
  135. package/src/model/full-prefill-row.ts +105 -0
  136. package/src/model/gemma4-base.ts +183 -82
  137. package/src/model/gemma4.ts +116 -28
  138. package/src/model/generated/gemma4-12b.ts +4 -1
  139. package/src/model/generated/gemma4-26b.ts +4 -1
  140. package/src/model/generated/gemma4-e4b.ts +4 -1
  141. package/src/model/glm52-cache.ts +80 -4
  142. package/src/model/glm52-mla.ts +12 -6
  143. package/src/model/glm52.ts +19 -40
  144. package/src/model/kv-attention-view.ts +70 -0
  145. package/src/model/minicpm5.ts +11 -7
  146. package/src/model/profile.ts +29 -4
  147. package/src/model/quantized-append-attention.ts +31 -0
  148. package/src/model/qwen3-delta-state.ts +74 -0
  149. package/src/model/qwen3-delta.ts +59 -36
  150. package/src/model/qwen3-mrope.ts +7 -9
  151. package/src/model/qwen3_5.ts +177 -75
  152. package/src/model/rotating-kv-donor.ts +35 -0
  153. package/src/model/rotating-kv-layout.ts +262 -0
  154. package/src/model/rotating-row-transaction.ts +35 -0
  155. package/src/model/speculative-rotating-kv.ts +101 -0
  156. package/src/model/ssm-prefill-padding.ts +39 -0
  157. package/src/model/support.ts +8 -0
  158. package/src/model/token-groups.ts +66 -0
  159. package/src/model/trellis-linear.ts +4 -4
  160. package/src/model/turboquant-codec.ts +89 -0
  161. package/src/model/universal/dense.ts +14 -13
  162. package/src/model/whisper.ts +435 -0
  163. package/src/native/mic_capture.swift +92 -0
  164. package/src/native-pack.ts +6 -5
  165. package/src/pi-web.ts +9 -1
  166. package/src/preflight.ts +31 -15
  167. package/src/prompt-cache.ts +306 -86
  168. package/src/runtime-config.ts +1 -5
  169. package/src/safetensors.ts +9 -2
  170. package/src/sampler.ts +37 -25
  171. package/src/serve/audio-routes.ts +323 -0
  172. package/src/serve/chat-request.ts +2 -0
  173. package/src/serve/chat-stage.ts +19 -14
  174. package/src/serve/completion-executor.ts +9 -1
  175. package/src/serve/discovery-routes.ts +17 -1
  176. package/src/serve/generated-token-history.ts +57 -0
  177. package/src/serve/generation-gateway.ts +88 -38
  178. package/src/serve/model-binding.ts +7 -1
  179. package/src/serve/model-host.ts +3 -4
  180. package/src/serve/preparation.ts +3 -3
  181. package/src/serve/prompt-response-trace.ts +14 -0
  182. package/src/serve/request-plan.ts +6 -3
  183. package/src/serve/request-prep.ts +40 -25
  184. package/src/serve/text-completion-stage.ts +2 -2
  185. package/src/serve/transcription-server.ts +64 -0
  186. package/src/serve/transcription-service.ts +502 -0
  187. package/src/server.ts +185 -261
  188. package/src/spec/assistant-rows.ts +93 -0
  189. package/src/spec/assistant-source.ts +28 -18
  190. package/src/spec/cache-identity.ts +22 -0
  191. package/src/spec/deepspec-rows.ts +29 -0
  192. package/src/spec/deepspec-source.ts +12 -10
  193. package/src/spec/dflash-source.ts +28 -4
  194. package/src/spec/drafter.ts +153 -142
  195. package/src/spec/dspark/deepspec-module.ts +95 -120
  196. package/src/spec/dspark/module-dflash.ts +126 -170
  197. package/src/spec/glm52-mtp-graph.ts +29 -0
  198. package/src/spec/glm52-mtp-rows.ts +132 -0
  199. package/src/spec/glm52-mtp-source.ts +54 -136
  200. package/src/spec/glm52-mtp-state.ts +33 -0
  201. package/src/spec/ngram-source.ts +115 -21
  202. package/src/spec/projected-draft-rows.ts +73 -0
  203. package/src/spec/qwen-mtp-module.ts +171 -0
  204. package/src/spec/qwen-mtp-rows.ts +230 -0
  205. package/src/spec/qwen-mtp-source.ts +84 -226
  206. package/src/spec/qwen-mtp-state.ts +30 -0
  207. package/src/spec/serve-loop.ts +73 -69
  208. package/src/spec/source.ts +93 -15
  209. package/src/spec/two-model-rows.ts +62 -0
  210. package/src/spec/two-model.ts +45 -103
  211. package/src/ssd-cache.ts +107 -36
  212. package/src/ssd-durability.ts +29 -27
  213. package/src/storage/host-buffer.ts +24 -0
  214. package/src/storage/kv-writer.ts +83 -0
  215. package/src/storage/kv-writer.worker.js +264 -0
  216. package/src/storage/retention-policy.ts +37 -0
  217. package/src/tiered-prompt-cache.ts +45 -0
  218. package/src/vision/embedder.ts +8 -0
  219. package/src/vision/prompt.ts +101 -65
  220. package/src/vision/qwen3vl-prompt.ts +101 -64
  221. package/src/vision/qwen3vl-tower.ts +11 -0
  222. package/src/vision/siglip.ts +8 -0
  223. package/src/web/app.html +12 -0
  224. package/src/web/app.js +217 -57
  225. package/src/web/src/chat.ts +4 -1
  226. package/src/web/src/composer.ts +3 -0
  227. package/src/web/src/voice.ts +141 -0
  228. package/src/web/sw.js +1 -1
  229. package/src/backends/mlx/memory-guard.ts +0 -40
  230. package/src/spec/prefix-state.ts +0 -45
  231. /package/docs/{planning → archive/planning}/release-notes-v0.3.0.md +0 -0
@@ -30,6 +30,10 @@ every other path is unique. Unmatched paths return
30
30
  | POST | `/v1/messages` | [Anthropic Messages](#post-v1messages-anthropic-messages-api) |
31
31
  | POST | `/v1/responses` | [OpenAI Responses](#post-v1responses-openai-responses-api) |
32
32
  | POST | `/v1/embeddings` | [Embeddings](#post-v1embeddings-openai-embeddings-api) |
33
+ | POST | `/v1/audio/transcriptions`, `/v1/audio/translations` | [Audio transcription](#post-v1audiotranscriptions--post-v1audiotranslations-speech-to-text) |
34
+ | POST | `/v1/audio/sessions`, `/v1/audio/sessions/<id>/audio`, `/v1/audio/sessions/<id>/finish` | [Streaming sessions](#streaming-transcription-sessions-v1audiosessions) |
35
+ | DELETE | `/v1/audio/sessions/<id>` | [Streaming sessions](#streaming-transcription-sessions-v1audiosessions) |
36
+ | POST | `/admin/transcription/unload` | [Audio transcription](#post-v1audiotranscriptions--post-v1audiotranslations-speech-to-text) |
33
37
  | GET | `/v1` | [API index](#get-v1) |
34
38
  | GET | `/v1/models`, `/v1/models/<id>` | [Models](#get-v1models) |
35
39
  | GET / POST | `/v1/adapters` | [Adapters](#adapters-lora-hot-swap) |
@@ -40,6 +44,7 @@ every other path is unique. Unmatched paths return
40
44
  | GET | `/fit` | [Fit](#get-fit) |
41
45
  | GET | `/library` | [Library](#get-library) |
42
46
  | GET | `/downloads` | [Downloads](#get-downloads) |
47
+ | POST | `/admin/cache/session/close` | [Close cache session](#post-admincachesessionclose) |
43
48
  | POST | `/admin/cache/flush` | [SSD cache flush](#post-admincacheflush) |
44
49
  | GET | `/api/jobs`, `/api/jobs/<id>`, `/api/jobs/<id>/stream` | [Jobs](#jobs-apijobs) |
45
50
  | POST | `/api/finetune/submit`, `/inspect-dataset`, `/merge`, `/export` | [Fine-tune](#fine-tune-apifinetune) |
@@ -117,8 +122,8 @@ Request body (OpenAI chat schema; unknown fields ignored):
117
122
  "stream": false,
118
123
  "max_tokens": 1024, // or max_completion_tokens (wins); unset =
119
124
  // no cap — generate until EOS or the
120
- // admitted context is exhausted (admission
121
- // clamps; --max-tokens sets a server default)
125
+ // an explicit operator limit is reached;
126
+ // --max-tokens sets a server default
122
127
  "temperature": 0.7, // 0 = greedy
123
128
  "top_p": 0, "top_k": 0, // 0 = off
124
129
  "seed": 1234, // omit for time-derived
@@ -369,15 +374,34 @@ Speculative decoding is a **server-level mode** (`serve --draft-model`,
369
374
  or the model-free `serve --draft-kind ngram`, which mounts no draft
370
375
  model); there is no per-request draft field. The `speculation` usage
371
376
  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
379
- through the serial lane (mlx_lm.server parity: `is_batchable = draft is
380
- None`) — speculation and `--batch N` are different modes.
377
+ the final stream chunk. Spec-eligible requests are text-only on base weights.
378
+ In the shared executor, Qwen grouped MTP, prompt lookup and standalone drafting support logprobs and
379
+ bf16, uniform KV4/KV8 or per-layer affine KV, or TurboQuant. Qwen MTP and prompt lookup also support
380
+ positive library conversion thresholds for uniform KV4/KV8. Per-layer mixed KV and
381
+ adapters remain incompatible with speculation. Strict legacy serial execution
382
+ retains its narrower bf16/KV4 policy. The KV4 compatibility control is
383
+ `MLX_BUN_QWEN_SPEC_KV4=0`.
384
+ Ineligible requests decode normally and omit the field. Qwen MTP reuses paired
385
+ target/draft prefill and completed decode state through the shared RAM/SSD
386
+ cache by default in v0.4.0, reported in `cached_tokens`. Configured SSD persistence
387
+ supports reuse after restart; `MLX_BUN_MTP_PROMPT_CACHE=0` disables reuse.
388
+ Generated checkpoints cover processed tokens and use queued SSD persistence.
389
+ Long-conversation performance acceptance remains open. Qwen prompt lookup also
390
+ retains prefill and completed-output state in the common RAM/SSD cache, with
391
+ its committed token history stored as a companion attachment. Standalone drafting
392
+ on Qwen targets stores the draft model’s attention/recurrent state and pending
393
+ last draft token as companion state, including restoration after restart. Qualified Qwen
394
+ MTP, prompt lookup and standalone drafting use shared execution at one or several active rows and
395
+ report `usage.lane: "batched"`. Supported full-attention and rotating targets, including
396
+ Llama, MiniCPM and Gemma, use shared bf16, uniform KV4/KV8 or per-layer affine KV or TurboQuant lookup and standalone drafting, with
397
+ logprobs and generated RAM/SSD checkpoints. Delayed and per-layer affine rotating speculation uses the same row lifecycle; positive conversion thresholds are a library setting, while served schemes default to zero. Gemma assistant drafting also shares execution with bf16, uniform KV4/KV8 or per-layer affine KV or TurboQuant target storage and stores its
398
+ last true target hidden alongside generated RAM/SSD prefixes. Donor attention
399
+ handles row validity independently of the draft graph. DeepSpec and DSpark also use these shared target layouts and the common RAM/SSD cache
400
+ for projected context, with independent confidence-based proposal lengths.
401
+ Other provider/target combinations still
402
+ use the serial executor.
403
+ Prompt-lookup concurrent performance acceptance remains open. Ineligible requests can use continuous ordinary execution
404
+ when their features support it, even while a draft is mounted.
381
405
 
382
406
  `usage.fill` reports **token fast-forwarding** (`MLX_BUN_FILL=strict|echo`).
383
407
  Strict mode appends structural spans from the request's tools and chat
@@ -387,19 +411,45 @@ using its qualified append chunk size, then resumes sampling
387
411
  (design: [speculative-decoding.md](../design/speculative-decoding.md) §"Token
388
412
  fast-forwarding"). Injected tokens are billed in `completion_tokens` like any
389
413
  other generated token; `decodeSteps` counts ordinary sampling steps, so
390
- `injected / (injected + decodeSteps)` is the share appended without sampling.
414
+ `injected / (injected + decodeSteps)` is the share emitted through fill.
415
+ Strict spans bypass sampling; echo spans still undergo sampled verification.
391
416
  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).
417
+ request's shape excludes it, including `logprobs`, structured output or media.
418
+ Strict shared fill supports bf16, affine KV and
419
+ TurboQuant layouts and retains absolute sampling positions for explicit seeds.
420
+ Its `usage.fill` statistics use the same fields as explicit serial execution.
421
+ Known tokens still advance model state. All-known steps skip intermediate
422
+ vocabulary heads and use the model-qualified append geometry, including the
423
+ specialized Qwen B1 append. Wider Qwen rows retain one-position arithmetic. Compiled
424
+ replay, paging and interruption checkpoints remain outside this shared method.
425
+ Explicit serial fill still excludes user-fixed seeds.
394
426
 
395
427
  Under `MLX_BUN_FILL=echo` a second, weaker source joins: spans copied from
396
428
  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
429
+ single forward, are checked by the request's sampler on its logits, and have
398
430
  their rejected tail rewound — so `verifyAccepted + verifyRejected` is what the
399
431
  echo index proposed and `injected` is what survived. `wastedSamples` counts
400
432
  only ASSERT fills (a verify fill consumes the in-flight sample as its first
401
433
  position's check instead of discarding it).
402
434
 
435
+ Qwen MTP can alternate learned draft rounds with verified echo rounds. The
436
+ provider consumes accepted copied tokens and target context before drafting
437
+ again. Its asserted template spans remain ordinary sampled output. Learned
438
+ draft counts, acceptance histograms and `speculation.rounds` exclude echo;
439
+ `targetCalls`, `tokensPerForward` and `forwardsSaved` cover both round types.
440
+ Echo activity remains in `usage.fill`, including matching prefixes established
441
+ by ordinary MTP samples, so it is not additive with learned-draft counts.
442
+ A request fully served by echoes can
443
+ have zero learned drafts. Other mounted providers do not yet support this
444
+ combination.
445
+
446
+ An explicit `seed` controls a request-local random stream and can use
447
+ continuous execution. Repeating a request reproduces sampling at the same
448
+ model arithmetic and batch composition. Different batch shapes can change
449
+ floating-point results even with identical random draws; `--batch 1` retains
450
+ the existing fixed-shape control. This differs from mlx-lm's policy of routing
451
+ explicit seeds to its single-request generator.
452
+
403
453
  ### logprobs / top_logprobs
404
454
 
405
455
  With `"logprobs": true` and/or `"top_logprobs": k` (1–11), the
@@ -432,8 +482,8 @@ penalties), **before** the sampler's temperature/top-p/top-k/min-p/XTC.
432
482
  mlx-lm's top-k order is unspecified (argpartition); ours is sorted
433
483
  descending — the same set, so the entry is deterministically the argmax.
434
484
  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
485
+ either). Requests with logprobs can use ordinary continuous execution under
486
+ `--batch N`; other feature exclusions still apply. Invalid values are rejected
437
487
  with mlx-lm's exact messages (see Errors).
438
488
 
439
489
  ### Streaming
@@ -544,11 +594,10 @@ Design and fidelity notes: [docs/reference/server-api.md](./server-api.md).
544
594
 
545
595
  ### Errors
546
596
 
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.
597
+ Memory estimates do not reject or shorten requests by default. Actual runtime
598
+ failures use the request/stream error path when recoverable; native Metal
599
+ allocation failure can terminate the process. Saved generation checkpoints
600
+ remain available for an identical retry.
552
601
 
553
602
  All errors are `{ "error": { "message": …, ... } }`.
554
603
 
@@ -567,10 +616,12 @@ All errors are `{ "error": { "message": …, ... } }`.
567
616
  'low', 'medium', 'high', 'xhigh'`).
568
617
  - `400` with `"type": "memory_admission"`, `"code":
569
618
  "context_over_budget"` — the PROMPT itself leaves no generation slot
570
- within the max safe context (`src/serve/request-plan.ts`). A prompt that
619
+ within an explicitly configured context/budget limit (`src/serve/request-plan.ts`). A prompt that
571
620
  fits is never rejected for a broad `max_tokens`: the upper bound is
572
621
  capped to the remaining room (`max_tokens` is a ceiling, not a promise)
573
- and generation proceeds. The ceiling is visible at `/stats`.
622
+ and generation proceeds. This is opt-in via an operator budget/context cap
623
+ (or the fixed GLM-5.2 layout), never enforced from default fit estimates.
624
+ `/stats.admission.enforced_context_tokens` is null when no cap applies.
574
625
  - `429` with `"type": "resource_admission"`, `"code": "queue_full"` when
575
626
  a preparation or generation queue has 64 waiting requests. If SSE headers
576
627
  are already sent, the stream emits its terminal error instead.
@@ -590,7 +641,7 @@ and adapter selection as chat.
590
641
  "prompt": "Once upon a time", // REQUIRED, non-empty string only (token
591
642
  // arrays rejected, matching mlx_lm.server)
592
643
  "max_tokens": 512, // unset = no cap: generate until EOS or the
593
- // admitted context (DEVIATION: mlx_lm.server
644
+ // an explicit operator limit (DEVIATION: mlx_lm.server
594
645
  // defaults 512 — pass --max-tokens 512 to
595
646
  // reproduce); max_completion_tokens wins
596
647
  "stream": false,
@@ -737,6 +788,97 @@ curl localhost:8080/v1/embeddings -H 'content-type: application/json' \
737
788
  -d '{"input": ["the cat sat on the mat", "a kitten by the window"]}'
738
789
  ```
739
790
 
791
+ ## POST /v1/audio/transcriptions · POST /v1/audio/translations (speech-to-text)
792
+
793
+ OpenAI-compatible Whisper transcription (`translations` = the same route with
794
+ task `translate`, English output). Served by every `mlx-bun serve` beside the
795
+ chat model — the Whisper checkpoint (`--whisper-model`, default: the first
796
+ downloaded `whisper` model) loads for each take and is released right after
797
+ it unless `--whisper-idle-unload`/`--whisper-resident` say otherwise — and by the transcription-only server that
798
+ `serve <whisper checkpoint>` starts ([server-config.md](server-config.md)).
799
+ Model support and the parity contract: [models.md](models.md#supported-models);
800
+ CLI equivalent: [`mlx-bun transcribe`](cli.md#transcribe--speech-to-text).
801
+
802
+ **Request** — `multipart/form-data` (OpenAI's shape) or JSON:
803
+
804
+ | Field | Meaning |
805
+ | --- | --- |
806
+ | `file` | audio upload (multipart) or base64 / `data:` URL string (JSON; `input_audio.data` and `audio` are accepted aliases). WAV decodes through the exact PCM parser; any other CoreAudio-readable container (mp3/m4a/aac/flac/ogg/aiff/caf, mp4/mov audio tracks…) decodes in-process through AudioToolbox (`src/audio/audiotoolbox.ts`, 3.7 ms for an 11 s mp3). Undecodable bytes are a `400`. |
807
+ | `model` | ignored — the configured Whisper checkpoint is used |
808
+ | `language` | ISO code or name; omitted / `auto` detects from the first 30 s |
809
+ | `prompt` | initial prompt (vocabulary, style); the last 223 tokens are kept |
810
+ | `response_format` | `json` (default) · `verbose_json` · `text` · `srt` · `vtt` |
811
+ | `temperature` | one temperature in [0, 1]; omitted = mlx-whisper's `(0, 0.2, …, 1.0)` fallback ladder |
812
+ | `stream` | `true` → server-sent events (below) |
813
+ | `timestamp_granularities[]` | `segment` (default); `word` adds `words` (`word, start, end, probability`) to `verbose_json` segments via cross-attention DTW on the checkpoint's alignment heads — one extra decoder pass per window |
814
+ | `beam_size` | **non-standard**: beam search width (default greedy); requires temperature 0 |
815
+ | `vocabulary` | **non-standard**: array (JSON) or comma-separated (multipart) recognition hints, fitted whole-term into the 223-token prompt budget after `prompt`; usage is reported |
816
+ | `condition_on_previous_text` | **non-standard**, default `true` |
817
+ | `no_speech_threshold`, `without_timestamps` | **non-standard** mlx-whisper decode options |
818
+ | `vad`, `vad_threshold`, `vad_min_speech_ms`, `vad_trim` | **non-standard**: Silero VAD gate (v6.2, `ggml-org/whisper-vad` weights in the HF cache). No detected speech → `{"text": ""}` with `mlx_bun.vad.speech = false` and Whisper never runs (≈25 ms for 11 s). `vad_trim` crops to the speech span ± 0.5 s first; timestamps stay clip-relative. Segments are reported in `mlx_bun.vad.segments` (seconds). Defaults: threshold 0.5, min speech 250 ms |
819
+ | `faithful` | **non-standard**: `true` runs the oracle-parity graph instead of the fast path (fused attention, compiled decoder step); same tokens up to documented near-ties, slower |
820
+ | `audio_ctx` | **non-standard, Lab**: encode only the first n of 1500 encoder positions (whisper.cpp `-ac`). Cheaper for short clips; measured hallucinated repeats below ~1024 on the fixture clips — off unless set |
821
+
822
+ **Response** — `json`: `{ "text": "…", "mlx_bun": { "model", "language",
823
+ "duration", "timings": { "load_ms", "transcribe_ms", "total_ms" },
824
+ "vocabulary"?: { "included", "omitted", "token_count", "token_budget" } } }`.
825
+ `verbose_json` adds `task`, `language`, `duration` and openai-whisper's
826
+ `segments` (`id, seek, start, end, text, tokens, temperature, avg_logprob,
827
+ compression_ratio, no_speech_prob`). `text`/`srt`/`vtt` return the bare body
828
+ (`text/plain`, `application/x-subrip`, `text/vtt`) with `x-mlx-bun-language`
829
+ and `x-mlx-bun-timings` headers. `load_ms` is non-zero exactly when the
830
+ request paged the weights in.
831
+
832
+ **Streaming** (`stream=true`): `text/event-stream` with `transcript.text.delta`
833
+ `{ "delta": "<segment text>", "segment": { "id", "start", "end" } }` per
834
+ finalized segment, `transcript.progress` `{ "done", "total" }` (mel frames),
835
+ and a final `transcript.text.done` carrying the non-streaming body.
836
+
837
+ **Errors**: `400 invalid_request_error` (fields, undecodable audio, clips
838
+ shorter than 0.1 s), `415` (content type), `499` on client cancel, and
839
+ `503 model_unavailable` when no Whisper checkpoint is on disk (the message
840
+ carries the `mlx-bun get` command).
841
+
842
+ `POST /admin/transcription/unload` pages the weights out now and returns
843
+ `{ "unloaded": bool, "resident", "loads", "unloads", "requests",
844
+ "last_load_ms", "idle_unload_sec" }` — the same block `/health` and
845
+ `/stats` report as `transcription` on the transcription-only server.
846
+ Transcriptions run one at a time and never overlap chat generation on the
847
+ GPU (the generation gateway's exclusive lock).
848
+
849
+ ```sh
850
+ curl localhost:8080/v1/audio/transcriptions -F file=@meeting.m4a -F language=en -F response_format=srt
851
+ curl localhost:8080/v1/audio/transcriptions -H 'content-type: application/json' \
852
+ -d '{"file":"<base64 wav>","beam_size":5,"vocabulary":["Sotto","SwiftUI","Metal"]}'
853
+ ```
854
+
855
+ ## Streaming transcription sessions (`/v1/audio/sessions`)
856
+
857
+ For audio that is being recorded while it is sent (dictation). Every
858
+ completed 30 s window is transcribed as soon as it exists, under the same
859
+ exclusive lock as everything else, so `finish` costs one window regardless
860
+ of take length (measured: a 62 s take that needs 1165 ms in batch returns
861
+ 327 ms after its last chunk, identical text — M1 Max). With `vad: true` the
862
+ Silero gate runs on the stream: nothing is transcribed until speech has
863
+ been seen, and a take with no speech finishes as `{"text": ""}` without
864
+ ever running Whisper. The one-shot parameters (`language`, `prompt`,
865
+ `vocabulary`, `beam_size`, `temperature`, `vad*`, `timestamp_granularities`,
866
+ `faithful`) apply; `response_format` is `json` or `verbose_json`.
867
+
868
+ | Route | Body / response |
869
+ | --- | --- |
870
+ | `POST /v1/audio/sessions` | JSON params → `{ "id", "model", "vocabulary" }` (the weights load here if they were paged out) |
871
+ | `POST /v1/audio/sessions/<id>/audio` | one chunk: `content-type: audio/pcm;rate=16000` raw little-endian float32 mono (sotto's format, no decode), or any CoreAudio container. → `{ "samples", "duration", "speech", "segments": [...] }` with every segment finalized so far |
872
+ | `POST /v1/audio/sessions/<id>/finish` | → the one-shot response body (`json`/`verbose_json` with `mlx_bun`), closes the session |
873
+ | `DELETE /v1/audio/sessions/<id>` | discard, `204` |
874
+
875
+ Chunk order is the caller's responsibility (send sequentially and await
876
+ each receipt). Unknown ids are `404`; a finished session is `409`; at most
877
+ 64 sessions are open at once (`429`). Windows fed early are normalized
878
+ against the audio received so far (the log-mel clamp is relative to the
879
+ clip maximum), which can differ from a batch transcription in near-silent
880
+ mel bins — the fixture takes decode identically.
881
+
740
882
  ## GET /v1
741
883
 
742
884
  API index for discovery tooling:
@@ -744,7 +886,8 @@ API index for discovery tooling:
744
886
  ```jsonc
745
887
  { "name": "mlx-bun", "version": "<package version>", "model": "<served model id>",
746
888
  "endpoints": ["POST /v1/chat/completions", "POST /v1/completions", "POST /v1/messages",
747
- "POST /v1/responses", "POST /v1/embeddings", "GET /v1/models",
889
+ "POST /v1/responses", "POST /v1/embeddings", "POST /v1/audio/transcriptions",
890
+ "POST /v1/audio/translations", "GET /v1/models",
748
891
  "GET/POST/DELETE /v1/adapters", "GET /health", "GET /stats", "GET /fit",
749
892
  "GET /library", "GET /downloads"] }
750
893
  ```
@@ -836,11 +979,11 @@ vision/audio, adapters, and training are not emulated by the serving port.
836
979
  ```jsonc
837
980
  {
838
981
  "server": { "owner": "serve" | "pi-session" | "embedded", "model": "...", "started_at": 0 },
839
- "prompt_cache": { "entries": 0, "bytes": 0, "max_bytes": 0, "hits": 0, "misses": 0 },
982
+ "prompt_cache": { "entries": 0, "bytes": 0, "max_bytes": 0, "hits": 0, "misses": 0, "object_hits": 0, "object_misses": 0, "object_restores": 0 },
840
983
  "response_store": { "entries": 0, "bytes": 0, "max_bytes": 33554432, "ttl_ms": 3600000 },
841
984
  "kv_quant": { "mode": "bf16" | "uniform-kv8" | "turbo k8v3" | "mixed (kv_config.json)",
842
985
  "layers": { "kv4": 8, "bf16": 40 }, // turbo: { "turbo-k8v3": 8, "bf16": 40 }
843
- "attention": { "global": 10, "sliding_window": 38 } },
986
+ "attention": { "global": 10, "sliding_window": 38 }, "recurrent_layers": 0 },
844
987
  // present only when --ssd-cache is on:
845
988
  "ssd_cache": { "dir": "...", "entries": 0, "bytes": 0, "max_bytes": 0,
846
989
  "restores": 0, "spills": 0, "restore_ms_last": 0, "demotions": 0,
@@ -849,8 +992,9 @@ vision/audio, adapters, and training are not emulated by the serving port.
849
992
  "pending_spill_bytes": 0, "dropped_spills": 0,
850
993
  "failed_spills": 0, "longest_durable_prefix_tokens": 0 },
851
994
  "admission": {
852
- "max_safe_context": 0, // tokens; requests above this 400
853
- "memory_budget_bytes": null, // explicit budget, or null (machine default)
995
+ "max_safe_context": 0, // advisory fit estimate, tokens
996
+ "enforced_context_tokens": null, // explicit cap or fixed layout; null = none
997
+ "memory_budget_bytes": null, // explicit budget, or null
854
998
  "usable_bytes": 0,
855
999
  "weights_bytes": 0
856
1000
  },
@@ -898,6 +1042,12 @@ appears only when `--kv-quant config` was passed explicitly and the
898
1042
  checkpoint ships a per-layer `kv_config.json` — per-layer KV quantization
899
1043
  is never selected automatically.
900
1044
 
1045
+ `kv_quant.layers` counts configured attention KV codecs; `attention` separates
1046
+ full and sliding attention. `recurrent_layers` counts linear/recurrent layers
1047
+ whose convolution and recurrent state retain their own precision. They do not
1048
+ count as bf16 or quantized attention KV. These are model/policy counts, not
1049
+ live allocation measurements or a per-request conversion-progress report.
1050
+
901
1051
  `pending_snapshots` counts prompt-cache entries scheduled for persistence but
902
1052
  not yet confirmed by the atomic SSD store. `longest_durable_prefix_tokens` is
903
1053
  the longest prefix already present in the SSD index, not merely queued.
@@ -924,7 +1074,7 @@ generic all-resident SKU estimator is inapplicable
924
1074
  ```jsonc
925
1075
  {
926
1076
  "machine": { "chip": "M4 Pro", "ram_bytes": 0, "bandwidth_gbs": 0.0 },
927
- "context_tokens": 8192, // current admission ceiling
1077
+ "context_tokens": 8192, // estimated context capacity
928
1078
  "typical_context_tokens": 8192, // min(8192, context_tokens)
929
1079
  "typical_decode_tps": 0.0, // predicted at typical_context_tokens
930
1080
  "measured_decode_tps": null, // real number from eval DB, or null
@@ -959,7 +1109,11 @@ generic all-resident SKU estimator is inapplicable
959
1109
  ## Adapters (LoRA hot-swap)
960
1110
 
961
1111
  Routes in `src/serve/model-admin-routes.ts`; the manager is
962
- `AdapterManager` in `src/lora.ts`.
1112
+ `AdapterManager` in `src/lora.ts`. Requests selecting the same ordered adapter
1113
+ set can share a continuous group. Different sets wait for the current group
1114
+ to finish and then use the same executor. Prefix-cache identity is resolved
1115
+ under the execution lease, so replacing an adapter while a request is queued
1116
+ cannot reuse state from its previous weights.
963
1117
 
964
1118
  - `GET /v1/adapters` — `{ adapters: [{ id, path, rank, scale, size_bytes,
965
1119
  mounted_layers, ram_bytes }] }` — currently-mounted adapters only.
@@ -998,6 +1152,50 @@ makes it the default for requests that send no `adapter` field; an
998
1152
  explicit `adapter` (including `"none"`) always wins, and hot-swap via
999
1153
  these endpoints is unchanged.
1000
1154
 
1155
+ ## Cache session affinity
1156
+
1157
+ Completion requests may supply `session_id` or `prompt_cache_key` in the JSON
1158
+ body, or `x-session-affinity` / `session_id` in a header. Precedence is body
1159
+ `session_id`, body `prompt_cache_key`, `x-session-affinity`, then `session_id`.
1160
+ This applies to chat completions, text completions, Responses and Messages.
1161
+ The shared mlx-bun Pi provider sends session-affinity headers automatically.
1162
+
1163
+ A session indexes its latest published checkpoint within each numerical cache
1164
+ namespace. Compatible continuations use that checkpoint directly; edited,
1165
+ branched or missing histories use ordinary prefix matching. The full intended
1166
+ input is still required: a session label does not append omitted messages,
1167
+ isolate access, change model settings or bypass token compatibility.
1168
+
1169
+ The cache prefers session-referenced checkpoints when choosing RAM residents,
1170
+ but can evict them when required. SSD persistence remains independent. Session
1171
+ indexes are process-local; after restart, the first request uses ordinary SSD
1172
+ lookup and re-establishes the association. Entries remain shareable across
1173
+ sessions and requests without session metadata. `MLX_BUN_SESSION_CACHE` provides
1174
+ the A/B control described in server-config.md.
1175
+
1176
+ `GET /stats` adds `prompt_cache.session_hits`, `session_misses` and
1177
+ `prefix_scans`. A session hit counts direct RAM checkpoint selection, including
1178
+ state prepared from SSD before execution. A session miss counts execution
1179
+ lookups with session metadata that need ordinary candidate search. Prefix scans
1180
+ count ordinary execution searches, not tokenization or preparation probes.
1181
+
1182
+
1183
+ Qwen image/video encoder reuse reports `prompt_cache.object_hits`,
1184
+ `object_misses` and `object_restores`. A hit borrows an exact RAM tensor object;
1185
+ a miss may restore from SSD or recompute. Object entries and bytes are included
1186
+ in the existing cache totals and share its RAM budget and persistence queue.
1187
+ These counters do not count language-model KV prefix hits. Media prompt KV
1188
+ reuse remains disabled because token IDs alone do not identify media contents.
1189
+
1190
+
1191
+ ## POST /admin/cache/session/close
1192
+
1193
+ Send `{ "session_id": "agent-42" }` to remove that session's cache association
1194
+ and retention preference. Returns `{ "closed": true }` when an ID is supplied,
1195
+ or `{ "closed": false }` otherwise. Closing does not delete checkpoints from
1196
+ RAM or SSD. Applications can close idle or completed sessions; a later request
1197
+ with the same ID establishes a new association.
1198
+
1001
1199
  ## POST /admin/cache/flush
1002
1200
 
1003
1201
  Forces every pending SSD prompt-cache snapshot through the serial write queue
@@ -1621,7 +1819,11 @@ repetition_context_size?, presence_penalty?, frequency_penalty?, seed?}`
1621
1819
  `delete_session {path}`, `regenerate`, `edit_resend {text}`,
1622
1820
  `switch_sibling {entryId}`, `context {context}` (app-aware assistant, below).
1623
1821
 
1624
- Server → client: `ready {model, vision, audio, thinking, genDefaults}`,
1822
+ Server → client: `ready {model, vision, audio, thinking, genDefaults, transcription}`
1823
+ (`transcription` is true when a Whisper checkpoint serves [`/v1/audio/sessions`](#streaming-transcription-sessions-v1audiosessions);
1824
+ the web composer then shows a hold-to-talk mic that streams 16 kHz float32
1825
+ into a session and inserts the transcript into the message box — voice
1826
+ input for any chat model),
1625
1827
  `turn_start`, `text_delta {delta}`, `thinking_delta {delta}`, `tool_start
1626
1828
  {callId, tool, args}`, `tool_approval_request {callId, tool, args}`,
1627
1829
  `tool_update {callId, chunk}`, `tool_end {callId, ok, result}`, `turn_end
@@ -1719,3 +1921,25 @@ Under `--isolate`, the parent owns Responses continuation history and exposes it
1719
1921
  bounded store counters in `GET /engine` as `response_store`. Model-worker restart
1720
1922
  or eviction preserves that history; restarting the parent clears it. Direct
1721
1923
  serving retains the same process-local TTL and byte limits.
1924
+
1925
+
1926
+ Supported grouped TurboQuant drafting retains `usage.speculation`, seeded
1927
+ sampling, logprobs, structured output and generated RAM/SSD continuations.
1928
+ The cache codec does not select the scheduler or sampler implementation.
1929
+ Composition coverage: [batching design](../design/batching.md).
1930
+
1931
+ With paged KV enabled, supported Gemma4 bf16 text requests report the batched lane when submitted through continuous scheduling. Seeded sampling, logprobs and grammar use the same interfaces. These requests bypass prompt-cache lookup/publication; paging does not provide SSD persistence.
1932
+
1933
+ Ordinary adapter checkpoints are isolated by ordered adapter IDs and mounted
1934
+ content revisions. Base-model requests cannot restore or remove an adapter checkpoint.
1935
+ With generation checkpointing enabled, repeating an identical eligible request
1936
+ replays its saved completion prefix and resumes from the saved pending token.
1937
+ Cancellation retains queued snapshots; successful completion removes them.
1938
+ `POST /admin/cache/flush` drains both prompt spills and generation checkpoints,
1939
+ and its elapsed time and durability statistics include both queues. A hard
1940
+ process exit can lose snapshots that have not drained.
1941
+
1942
+ For resumed generation checkpoints, `usage.prompt_tokens_details.cached_tokens`
1943
+ counts only the reused original prompt, bounded by `usage.prompt_tokens`. Saved
1944
+ completion tokens are replayed as completion output and do not increase that
1945
+ prompt-cache usage counter.