mlx-bun 0.4.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 (158) hide show
  1. package/README.md +57 -32
  2. package/docs/README.md +55 -42
  3. package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +3617 -0
  4. package/docs/archive/planning/release-notes-v0.5.0.md +37 -0
  5. package/docs/design/batching.md +220 -7
  6. package/docs/design/decode-speed-program.md +121 -66
  7. package/docs/design/generic-model-support.md +116 -0
  8. package/docs/design/kv-cache.md +380 -211
  9. package/docs/design/speculative-decoding.md +165 -26
  10. package/docs/design/turboquant.md +20 -7
  11. package/docs/design/unified-engine-frontier-plan.md +120 -13
  12. package/docs/planning/PRODUCT_ROADMAP.md +5 -6
  13. package/docs/planning/public-readiness-handoff.md +244 -0
  14. package/docs/planning/public-readiness.md +149 -0
  15. package/docs/reference/benchmarks.md +3463 -3489
  16. package/docs/reference/cli.md +62 -1
  17. package/docs/reference/distribution.md +29 -4
  18. package/docs/reference/environment.md +104 -8
  19. package/docs/reference/library-api.md +18 -4
  20. package/docs/reference/memory.md +1 -1
  21. package/docs/reference/models.md +17 -5
  22. package/docs/reference/server-api.md +181 -11
  23. package/docs/reference/server-config.md +232 -105
  24. package/package.json +2 -2
  25. package/src/audio/audiotoolbox.ts +112 -0
  26. package/src/audio/conformer.ts +8 -0
  27. package/src/audio/mic-capture.ts +98 -0
  28. package/src/audio/silero-vad.ts +379 -0
  29. package/src/audio/whisper-decode.ts +774 -0
  30. package/src/audio/whisper-fast.ts +732 -0
  31. package/src/audio/whisper-format.ts +73 -0
  32. package/src/audio/whisper-mel.ts +185 -0
  33. package/src/audio/whisper-timing.ts +274 -0
  34. package/src/audio/whisper-tokenizer.ts +239 -0
  35. package/src/audio/whisper-transcribe.ts +410 -0
  36. package/src/backends/mlx/autoregressive.ts +22 -1
  37. package/src/backends/mlx/batch-group.ts +111 -27
  38. package/src/backends/mlx/cache-layout.ts +3 -3
  39. package/src/backends/mlx/continuation-persistence.ts +5 -4
  40. package/src/backends/mlx/encoder-cache.ts +25 -0
  41. package/src/backends/mlx/fill-append.ts +21 -8
  42. package/src/backends/mlx/fill-group.ts +497 -0
  43. package/src/backends/mlx/gateway-binding.ts +56 -12
  44. package/src/backends/mlx/kv-maintenance.ts +30 -2
  45. package/src/backends/mlx/mixed-iteration.ts +62 -0
  46. package/src/backends/mlx/model-prompt.ts +46 -32
  47. package/src/backends/mlx/model-serving.ts +4 -3
  48. package/src/backends/mlx/paged-cache-codec.ts +27 -0
  49. package/src/backends/mlx/prefill-cohort.ts +15 -4
  50. package/src/backends/mlx/prefill-policy.ts +26 -0
  51. package/src/backends/mlx/prefill-rows.ts +57 -13
  52. package/src/backends/mlx/prompt-input.ts +29 -0
  53. package/src/backends/mlx/qwen-prompt-input.ts +40 -0
  54. package/src/backends/mlx/request-state-policy.ts +18 -5
  55. package/src/backends/mlx/rollback.ts +6 -3
  56. package/src/backends/mlx/serial-executor.ts +16 -7
  57. package/src/backends/mlx/speculative-group.ts +105 -26
  58. package/src/backends/mlx/speculative.ts +24 -7
  59. package/src/backends/mlx/target-layout-capability.ts +1 -1
  60. package/src/backends/mlx/transitioning-kv-rows.ts +8 -0
  61. package/src/cli.ts +322 -0
  62. package/src/contracts/execution.ts +10 -0
  63. package/src/contracts/object-cache.ts +8 -0
  64. package/src/contracts/pi-provider.ts +1 -1
  65. package/src/contracts/pi.ts +3 -1
  66. package/src/contracts/prefix-cache.ts +13 -3
  67. package/src/contracts/scheduling.ts +9 -0
  68. package/src/engine/execution-plan.ts +23 -12
  69. package/src/engine/execution-tasks.ts +44 -0
  70. package/src/engine/namespaced-cache.ts +13 -0
  71. package/src/engine/scheduler.ts +22 -2
  72. package/src/fill/fill-session.ts +14 -3
  73. package/src/generate.ts +28 -22
  74. package/src/grammar.ts +13 -0
  75. package/src/inference/prefill.ts +6 -0
  76. package/src/kv-scheme.ts +22 -0
  77. package/src/kv-store.ts +115 -79
  78. package/src/lab/README.md +5 -1
  79. package/src/lab/paged-kv/paged-attention.ts +117 -0
  80. package/src/lab/paged-kv/paged-kv-rows.ts +33 -5
  81. package/src/lab/paged-kv/paged-kv.ts +108 -42
  82. package/src/md-modules.d.ts +5 -0
  83. package/src/mlx/array.ts +37 -0
  84. package/src/mlx/ffi.ts +19 -0
  85. package/src/mlx/normalized-argmax.ts +116 -0
  86. package/src/mlx/ops.ts +97 -10
  87. package/src/model/batched-turboquant-kv.ts +6 -5
  88. package/src/model/delayed-quantized-kv.ts +3 -3
  89. package/src/model/delayed-rotating-quantized-kv.ts +1 -1
  90. package/src/model/delayed-turboquant-kv.ts +6 -4
  91. package/src/model/factory.ts +26 -0
  92. package/src/model/fingerprint.ts +12 -0
  93. package/src/model/full-prefill-row.ts +1 -1
  94. package/src/model/gemma4-base.ts +25 -6
  95. package/src/model/gemma4.ts +82 -2
  96. package/src/model/kv-attention-view.ts +4 -1
  97. package/src/model/profile.ts +29 -4
  98. package/src/model/quantized-append-attention.ts +31 -0
  99. package/src/model/qwen3-mrope.ts +7 -9
  100. package/src/model/qwen3_5.ts +99 -37
  101. package/src/model/support.ts +8 -0
  102. package/src/model/token-groups.ts +66 -0
  103. package/src/model/trellis-linear.ts +2 -2
  104. package/src/model/turboquant-codec.ts +6 -0
  105. package/src/model/whisper.ts +435 -0
  106. package/src/native/mic_capture.swift +92 -0
  107. package/src/native-pack.ts +6 -5
  108. package/src/pi-web.ts +9 -1
  109. package/src/prompt-cache.ts +265 -43
  110. package/src/runtime-config.ts +1 -5
  111. package/src/safetensors.ts +9 -2
  112. package/src/sampler.ts +5 -4
  113. package/src/serve/audio-routes.ts +323 -0
  114. package/src/serve/chat-request.ts +2 -0
  115. package/src/serve/chat-stage.ts +17 -12
  116. package/src/serve/completion-executor.ts +9 -1
  117. package/src/serve/discovery-routes.ts +17 -1
  118. package/src/serve/generated-token-history.ts +57 -0
  119. package/src/serve/generation-gateway.ts +52 -11
  120. package/src/serve/model-binding.ts +7 -1
  121. package/src/serve/preparation.ts +3 -3
  122. package/src/serve/prompt-response-trace.ts +14 -0
  123. package/src/serve/request-plan.ts +2 -0
  124. package/src/serve/request-prep.ts +38 -21
  125. package/src/serve/transcription-server.ts +64 -0
  126. package/src/serve/transcription-service.ts +502 -0
  127. package/src/server.ts +132 -164
  128. package/src/spec/assistant-rows.ts +1 -0
  129. package/src/spec/deepspec-rows.ts +1 -1
  130. package/src/spec/dflash-source.ts +1 -1
  131. package/src/spec/glm52-mtp-source.ts +1 -0
  132. package/src/spec/ngram-source.ts +27 -5
  133. package/src/spec/projected-draft-rows.ts +2 -2
  134. package/src/spec/qwen-mtp-rows.ts +11 -0
  135. package/src/spec/qwen-mtp-source.ts +3 -0
  136. package/src/spec/serve-loop.ts +5 -3
  137. package/src/spec/source.ts +14 -1
  138. package/src/spec/two-model-rows.ts +1 -0
  139. package/src/spec/two-model.ts +2 -2
  140. package/src/ssd-cache.ts +76 -17
  141. package/src/ssd-durability.ts +20 -22
  142. package/src/storage/host-buffer.ts +24 -0
  143. package/src/storage/kv-writer.ts +83 -0
  144. package/src/storage/kv-writer.worker.js +264 -0
  145. package/src/storage/retention-policy.ts +37 -0
  146. package/src/tiered-prompt-cache.ts +45 -0
  147. package/src/vision/embedder.ts +8 -0
  148. package/src/vision/prompt.ts +101 -65
  149. package/src/vision/qwen3vl-prompt.ts +101 -64
  150. package/src/vision/qwen3vl-tower.ts +11 -0
  151. package/src/vision/siglip.ts +8 -0
  152. package/src/web/app.html +12 -0
  153. package/src/web/app.js +160 -0
  154. package/src/web/src/chat.ts +4 -1
  155. package/src/web/src/composer.ts +3 -0
  156. package/src/web/src/voice.ts +141 -0
  157. package/src/web/sw.js +1 -1
  158. /package/docs/{planning → archive/planning}/release-notes-v0.4.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) |
@@ -370,15 +375,15 @@ 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
377
  the final stream chunk. Spec-eligible requests are text-only on base weights.
373
- In the unreleased shared executor, Qwen grouped MTP, prompt lookup and standalone drafting support logprobs and
378
+ In the shared executor, Qwen grouped MTP, prompt lookup and standalone drafting support logprobs and
374
379
  bf16, uniform KV4/KV8 or per-layer affine KV, or TurboQuant. Qwen MTP and prompt lookup also support
375
380
  positive library conversion thresholds for uniform KV4/KV8. Per-layer mixed KV and
376
381
  adapters remain incompatible with speculation. Strict legacy serial execution
377
382
  retains its narrower bf16/KV4 policy. The KV4 compatibility control is
378
383
  `MLX_BUN_QWEN_SPEC_KV4=0`.
379
384
  Ineligible requests decode normally and omit the field. Qwen MTP reuses paired
380
- target/draft prefill and completed decode state through the shared RAM/SSD cache by default in the
381
- unreleased source, reported in `cached_tokens`. Configured SSD persistence
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
382
387
  supports reuse after restart; `MLX_BUN_MTP_PROMPT_CACHE=0` disables reuse.
383
388
  Generated checkpoints cover processed tokens and use queued SSD persistence.
384
389
  Long-conversation performance acceptance remains open. Qwen prompt lookup also
@@ -406,19 +411,38 @@ using its qualified append chunk size, then resumes sampling
406
411
  (design: [speculative-decoding.md](../design/speculative-decoding.md) §"Token
407
412
  fast-forwarding"). Injected tokens are billed in `completion_tokens` like any
408
413
  other generated token; `decodeSteps` counts ordinary sampling steps, so
409
- `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.
410
416
  Injected positions still run through the model layers. The field is absent when the feature is off or the
411
- request's shape refuses it (fixed `seed`, `logprobs`, structured output,
412
- 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.
413
426
 
414
427
  Under `MLX_BUN_FILL=echo` a second, weaker source joins: spans copied from
415
428
  earlier in the same session, carried under policy `verify`. Those ride the same
416
- 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
417
430
  their rejected tail rewound — so `verifyAccepted + verifyRejected` is what the
418
431
  echo index proposed and `injected` is what survived. `wastedSamples` counts
419
432
  only ASSERT fills (a verify fill consumes the in-flight sample as its first
420
433
  position's check instead of discarding it).
421
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
+
422
446
  An explicit `seed` controls a request-local random stream and can use
423
447
  continuous execution. Repeating a request reproduces sampling at the same
424
448
  model arithmetic and batch composition. Different batch shapes can change
@@ -764,6 +788,97 @@ curl localhost:8080/v1/embeddings -H 'content-type: application/json' \
764
788
  -d '{"input": ["the cat sat on the mat", "a kitten by the window"]}'
765
789
  ```
766
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
+
767
882
  ## GET /v1
768
883
 
769
884
  API index for discovery tooling:
@@ -771,7 +886,8 @@ API index for discovery tooling:
771
886
  ```jsonc
772
887
  { "name": "mlx-bun", "version": "<package version>", "model": "<served model id>",
773
888
  "endpoints": ["POST /v1/chat/completions", "POST /v1/completions", "POST /v1/messages",
774
- "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",
775
891
  "GET/POST/DELETE /v1/adapters", "GET /health", "GET /stats", "GET /fit",
776
892
  "GET /library", "GET /downloads"] }
777
893
  ```
@@ -863,11 +979,11 @@ vision/audio, adapters, and training are not emulated by the serving port.
863
979
  ```jsonc
864
980
  {
865
981
  "server": { "owner": "serve" | "pi-session" | "embedded", "model": "...", "started_at": 0 },
866
- "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 },
867
983
  "response_store": { "entries": 0, "bytes": 0, "max_bytes": 33554432, "ttl_ms": 3600000 },
868
984
  "kv_quant": { "mode": "bf16" | "uniform-kv8" | "turbo k8v3" | "mixed (kv_config.json)",
869
985
  "layers": { "kv4": 8, "bf16": 40 }, // turbo: { "turbo-k8v3": 8, "bf16": 40 }
870
- "attention": { "global": 10, "sliding_window": 38 } },
986
+ "attention": { "global": 10, "sliding_window": 38 }, "recurrent_layers": 0 },
871
987
  // present only when --ssd-cache is on:
872
988
  "ssd_cache": { "dir": "...", "entries": 0, "bytes": 0, "max_bytes": 0,
873
989
  "restores": 0, "spills": 0, "restore_ms_last": 0, "demotions": 0,
@@ -926,6 +1042,12 @@ appears only when `--kv-quant config` was passed explicitly and the
926
1042
  checkpoint ships a per-layer `kv_config.json` — per-layer KV quantization
927
1043
  is never selected automatically.
928
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
+
929
1051
  `pending_snapshots` counts prompt-cache entries scheduled for persistence but
930
1052
  not yet confirmed by the atomic SSD store. `longest_durable_prefix_tokens` is
931
1053
  the longest prefix already present in the SSD index, not merely queued.
@@ -1030,6 +1152,50 @@ makes it the default for requests that send no `adapter` field; an
1030
1152
  explicit `adapter` (including `"none"`) always wins, and hot-swap via
1031
1153
  these endpoints is unchanged.
1032
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
+
1033
1199
  ## POST /admin/cache/flush
1034
1200
 
1035
1201
  Forces every pending SSD prompt-cache snapshot through the serial write queue
@@ -1653,7 +1819,11 @@ repetition_context_size?, presence_penalty?, frequency_penalty?, seed?}`
1653
1819
  `delete_session {path}`, `regenerate`, `edit_resend {text}`,
1654
1820
  `switch_sibling {entryId}`, `context {context}` (app-aware assistant, below).
1655
1821
 
1656
- 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),
1657
1827
  `turn_start`, `text_delta {delta}`, `thinking_delta {delta}`, `tool_start
1658
1828
  {callId, tool, args}`, `tool_approval_request {callId, tool, args}`,
1659
1829
  `tool_update {callId, chunk}`, `tool_end {callId, ok, result}`, `turn_end