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
package/src/cli.ts CHANGED
@@ -74,6 +74,8 @@ Commands:
74
74
  generate One-shot raw text generation from a local model (no server)
75
75
  benchmark Measure decode/prefill speed of OUR stack on this machine
76
76
  embed Text embeddings from a local embedding model (Qwen3-Embedding)
77
+ transcribe Speech-to-text from an audio file with a local Whisper model
78
+ dictate Push-to-talk: stream your microphone into Whisper, print/copy/type the text
77
79
  evals Show recent benchmark runs (all stacks)
78
80
  help Show help for a command (also: mlx-bun <command> --help)
79
81
 
@@ -120,10 +122,11 @@ const SERVER_FLAGS = `Server options:
120
122
  --ssd-cache-verify Verify tensor hashes on every restore (reads all
121
123
  bytes eagerly — integrity paranoia only)
122
124
  --generation-checkpoint <tokens>
123
- Snapshot an in-flight serial generation to the SSD
125
+ Queue an in-flight generation snapshot to the SSD
124
126
  cache every N emitted tokens. Repeating the same
125
127
  request after restart replays and resumes it.
126
- Requires --ssd-cache and --batch 1.
128
+ Requires --ssd-cache; supports ordinary requests
129
+ with shared cache layouts.
127
130
  --isolate Run the inference engine as a CHILD process and
128
131
  keep this process a pure UI/API proxy — the UI
129
132
  stays instant under GPU load and survives engine
@@ -163,14 +166,27 @@ Model & quality:
163
166
  request's \`adapter\` field, incl. "none", wins).
164
167
  --adapter-path is accepted as the mlx_lm.server
165
168
  alias. Hot-swap via POST /v1/adapters unchanged.
169
+ --whisper-model <p|query> Whisper checkpoint for /v1/audio/transcriptions
170
+ [default: the first downloaded whisper model,
171
+ resolved on the first request]. Serving a whisper
172
+ model ALONE starts a transcription-only server.
173
+ --preload Transcription-only server: load the Whisper
174
+ weights before listening instead of on the first
175
+ request.
176
+ --whisper-idle-unload <s> Keep the Whisper weights for this many idle seconds
177
+ after a take before disposing them. [default: 0 —
178
+ released right after every take, so the chat model
179
+ has the memory back before it prefills; a cold
180
+ take costs ~150 ms more than a warm one]
181
+ --whisper-resident Never dispose the Whisper weights (sotto-style).
166
182
  --draft-model <query> Speculative decoding: a drafter proposes tokens
167
183
  the main model verifies (exact results, faster
168
184
  decode when drafts land). The artifact's kind is
169
185
  auto-detected — a full same-tokenizer model
170
186
  (mlx_lm.server parity), a Gemma "-assistant"
171
187
  KV-borrowing drafter, or a DSpark checkpoint.
172
- Serial lane only — with --batch N a mounted draft
173
- routes every request serial, like mlx_lm.server.
188
+ Supported draft providers use shared batched
189
+ execution; ordinary requests can batch too.
174
190
  --draft-kind <kind> Override draft-artifact detection:
175
191
  two-model | assistant | dspark | deepspec | mtp | ngram
176
192
  (deepspec = DeepSeek's released Gemma4DSparkModel
@@ -205,9 +221,8 @@ Model & quality:
205
221
  only; sliding-window layers stay bf16)]
206
222
  --paged-kv OPTIONAL vLLM-style paged KV storage (block pool +
207
223
  gather before the stock SDPA; env MLX_BUN_PAGED_KV=1).
208
- v1: serial only (pins --batch 1 unless --batch is
209
- given), Gemma4-family, bf16 — refuses --batch N>1,
210
- --kv-quant, --draft-model; bypasses the prompt
224
+ Gemma4-family, bf16; supports --batch N.
225
+ Refuses --kv-quant and --draft-model; bypasses the prompt
211
226
  cache; runs uncompiled decode. Bit-exact with the
212
227
  plain path; expect a small decode cost at batch=1
213
228
  (the gather copy). docs/design/kv-cache.md
@@ -268,6 +283,74 @@ Kill switches (bit-exact A/B levers — each selects a slower same-parity path):
268
283
  bit-exact with the resident path`;
269
284
 
270
285
  const HELP: Record<string, string> = {
286
+ dictate: `mlx-bun dictate — push-to-talk dictation from your microphone
287
+
288
+ Usage: mlx-bun dictate [query] [options]
289
+
290
+ Captures the mic through the AVAudioEngine sidecar (16 kHz mono; macOS asks
291
+ for Microphone permission on first use) and streams it into a Whisper
292
+ transcription session while you speak — every finished 30 s window is
293
+ transcribed during capture, so the result lands about one window after you
294
+ stop. Silence never runs Whisper (Silero VAD).
295
+
296
+ Modes:
297
+ (default) Press Enter to start a take, Enter again to stop;
298
+ q + Enter or Ctrl-C quits
299
+ --hotkey [keycode] Hold a key instead (default 61 = Right Option, like
300
+ sotto); needs Input Monitoring for your terminal
301
+ Backend:
302
+ [query] / --model <p> Whisper model for in-process transcription (default:
303
+ the first downloaded whisper checkpoint)
304
+ --server <url> Use a running mlx-bun server's /v1/audio/sessions
305
+ instead of loading the model here
306
+ --idle-unload <s> Release the weights after this many idle seconds
307
+ (0 = right after each take; default 30); --resident
308
+ keeps them loaded
309
+ Decoding:
310
+ --language <code> default en; "auto" detects
311
+ --beam-size <n> beam search (default greedy)
312
+ --prompt "…" initial prompt / --vocabulary "a,b,c" hint terms
313
+ --no-vad skip the Silero gate
314
+ Output (any combination; default prints):
315
+ --copy copy the transcript to the clipboard (pbcopy)
316
+ --type type it into the frontmost app via System Events
317
+ keystrokes (needs Accessibility for your terminal);
318
+ --type-delay <s> waits first so you can Cmd-Tab
319
+ (default 1 in Enter mode, 0 with --hotkey)`,
320
+ transcribe: `mlx-bun transcribe — speech-to-text with a local Whisper model (no server)
321
+
322
+ Usage: mlx-bun transcribe <audio-file> [query] [options]
323
+
324
+ Loads a Whisper checkpoint (mlx-community/whisper-*) and prints the
325
+ transcript. WAV is decoded natively; any other container CoreAudio reads
326
+ (mp3/m4a/flac/ogg/aiff/…) is transcoded with afconvert first.
327
+
328
+ Model:
329
+ [query] / --model <p> Whisper model; omitted = the first downloaded
330
+ \`whisper\` checkpoint (errors with an mlx-bun get
331
+ suggestion when none)
332
+ Decoding:
333
+ --language <code> ISO code or name ("en", "japanese"); "auto" or omitted
334
+ detects the language from the first 30 s
335
+ --task translate Translate to English instead of transcribing
336
+ --beam-size <n> Beam search width (default: greedy)
337
+ --temperature <t> Sampling temperature; the default runs the
338
+ (0, 0.2, …, 1.0) fallback ladder like mlx-whisper
339
+ --no-fallback Temperature 0 only, no retry ladder
340
+ --prompt "…" Initial prompt (vocabulary hints / style)
341
+ --no-timestamps Decode text only (<|notimestamps|>)
342
+ --no-condition Do not condition each window on the previous text
343
+ --vad Silero VAD gate: skip Whisper when no speech is
344
+ detected (weights: ggml-org/whisper-vad in the HF cache
345
+ or --vad-model <path>); --vad-trim crops to speech
346
+ --word-timestamps Word-level timestamps (verbose_json / verbose output)
347
+ --faithful Run the oracle-parity graph instead of the fast path
348
+ --audio-ctx <n> Lab: encode only n of the 1500 encoder positions
349
+ (whisper.cpp -ac); cheaper for short clips, degrades
350
+ below ~1024 — off by default
351
+ Output:
352
+ --format <f> text (default) · json · verbose_json · srt · vtt
353
+ --verbose Print each segment with its time range as it decodes`,
271
354
  pi: `mlx-bun pi — drop into a coding-agent session on a local model
272
355
 
273
356
  Usage: mlx-bun pi [options] [message...]
@@ -1012,22 +1095,28 @@ function serverRuntimeFlags(): { port: number; serverOptions: import("./server")
1012
1095
  if (route.kvQuant !== undefined) serverOptions.kvQuant = route.kvQuant;
1013
1096
  if (route.turboQuant !== undefined) serverOptions.turboQuant = route.turboQuant;
1014
1097
  // --paged-kv (env: MLX_BUN_PAGED_KV=1): OPTIONAL vLLM-style block-pool KV
1015
- // storage, default off (docs/design/kv-cache.md). Serial-only in v1:
1016
- // with no explicit --batch, pin --batch 1 (the default is 8); an explicit
1017
- // --batch N>1 is refused loudly by createServer rather than downgraded.
1098
+ // storage, default off (docs/design/kv-cache.md). The state policy owns
1099
+ // block pools and cache reuse; paging preserves the selected batch size.
1018
1100
  if (flag("paged-kv") || runtimeValue("MLX_BUN_PAGED_KV") === "1") {
1019
1101
  const bsRaw = opt("paged-kv-block-size");
1020
1102
  serverOptions.pagedKv = bsRaw !== null ? { blockSize: Number(bsRaw) } : {};
1021
- if (batchRaw === null) {
1022
- serverOptions.batch = 1;
1023
- console.log("[paged-kv] serial-only in v1 — pinning --batch 1");
1024
- }
1025
1103
  }
1026
1104
  // Bind loopback unless asked otherwise (mlx_lm.server parity); --host
1027
1105
  // 0.0.0.0 is the explicit opt-in for LAN exposure. The chat-UI open and
1028
1106
  // `mlx-bun pi` attach both go through localhost, so loopback-only is
1029
1107
  // transparent to them.
1030
1108
  serverOptions.hostname = opt("host", "127.0.0.1")!;
1109
+ // Speech-to-text companion: explicit checkpoint + residency policy.
1110
+ const whisperIdleRaw = opt("whisper-idle-unload");
1111
+ if (whisperIdleRaw !== null) {
1112
+ const n = Number(whisperIdleRaw);
1113
+ if (!Number.isFinite(n) || n < 0) {
1114
+ console.error(`--whisper-idle-unload expects seconds >= 0 (got "${whisperIdleRaw}")`);
1115
+ process.exit(1);
1116
+ }
1117
+ serverOptions.whisperIdleUnloadSec = n;
1118
+ }
1119
+ if (flag("whisper-resident")) serverOptions.whisperResident = true;
1031
1120
  // Server-wide default for the chat template's enable_thinking variable
1032
1121
  // (CPM/MiniCPM5 and other hybrid-reasoning models). Unset ⇒ the model's
1033
1122
  // own default (false for MiniCPM5). onOff accepts true|false|on|off|1|0.
@@ -1642,29 +1731,44 @@ switch (cmd) {
1642
1731
  // straight from that path; anything else resolves like the positional
1643
1732
  // fuzzy query. Precedence: --model > positional > --query > auto-pick.
1644
1733
  const { m, picked } = await resolveModelAuto(opt("model") ?? positional(0) ?? opt("query"));
1734
+ if (m.modelType === "whisper") {
1735
+ // Transcription-only server: no chat model, weights paged in on demand.
1736
+ const sNative0 = step("native runtime");
1737
+ await ensureNative(sNative0);
1738
+ sNative0.done("native runtime ready");
1739
+ const { createTranscriptionServer } = await import("./serve/transcription-server");
1740
+ const { server, service } = await createTranscriptionServer({
1741
+ modelDir: m.path, modelId: m.repoId, port: rt.port,
1742
+ ...(rt.serverOptions.hostname ? { hostname: rt.serverOptions.hostname } : {}),
1743
+ ...(rt.serverOptions.unixSocket ? { unixSocket: rt.serverOptions.unixSocket } : {}),
1744
+ idleUnloadSec: rt.serverOptions.whisperIdleUnloadSec,
1745
+ resident: rt.serverOptions.whisperResident,
1746
+ preload: flag("preload"),
1747
+ });
1748
+ const where = rt.serverOptions.unixSocket ?? `http://${rt.serverOptions.hostname === "0.0.0.0" ? "localhost" : rt.serverOptions.hostname}:${server.port}`;
1749
+ box([
1750
+ `${style.bold(m.repoId)} ${style.dim("· transcription-only server")}`,
1751
+ `POST ${where}/v1/audio/transcriptions ${style.dim(`· ${service.stats.idle_unload_sec === null ? "always resident" : service.idleUnloadSec === 0 ? "released after every take" : `idle unload ${service.idleUnloadSec}s`}${service.resident ? " · loaded" : " · loads on first request"}`)}`,
1752
+ ]);
1753
+ const stop = () => { service.dispose(); server.stop(true); process.exit(0); };
1754
+ process.once("SIGINT", stop);
1755
+ process.once("SIGTERM", stop);
1756
+ break;
1757
+ }
1758
+ const whisperQuery = opt("whisper-model");
1759
+ if (whisperQuery !== null) {
1760
+ const { m: wm } = await resolveModelAuto(whisperQuery);
1761
+ if (wm.modelType !== "whisper") {
1762
+ console.error(`--whisper-model ${whisperQuery} resolved to ${wm.repoId} (model_type ${wm.modelType}), not a Whisper checkpoint`);
1763
+ process.exit(1);
1764
+ }
1765
+ rt.serverOptions.whisperModelDir = wm.path;
1766
+ rt.serverOptions.whisperModelId = wm.repoId;
1767
+ }
1645
1768
  const sFit = step("assessing fit");
1646
1769
  const config = await loadModelConfig(m.path);
1647
1770
  const report = fit(config, m.sizeBytes, 8192, undefined, undefined, m.expertsBytes);
1648
1771
  sFit.done(`${style.bold(m.repoId)} ${style.dim(`· ${gb(m.sizeBytes)} · ~${report.predictedDecodeTps.toFixed(0)} tok/s predicted`)}${picked ? style.dim(" · auto-picked (override: mlx-bun serve <query>)") : ""}`);
1649
- // Near-ceiling advisory (admission doctrine: advise, never refuse). A
1650
- // model near the DEFAULT Metal wired ceiling (~75% of RAM) dies with an
1651
- // UNCATCHABLE async-GPU-OOM on long prefills (M4 24 GB × 17 GB model,
1652
- // 2026-08-20) — surface the standard remedy up front instead.
1653
- {
1654
- const { maxRecommendedWorkingSetSize } = await import("./mlx/ffi");
1655
- const ceiling = maxRecommendedWorkingSetSize();
1656
- const sysctlRaw = Bun.spawnSync(["sysctl", "-n", "iogpu.wired_limit_mb"]).stdout.toString().trim();
1657
- const limitIsDefault = sysctlRaw === "0" || sysctlRaw === "";
1658
- if (limitIsDefault && ceiling > 0 && m.sizeBytes > 0.8 * ceiling) {
1659
- const ramGb = Number(Bun.spawnSync(["sysctl", "-n", "hw.memsize"]).stdout.toString().trim()) / 2 ** 30;
1660
- const suggestMb = Math.floor((ramGb - 2.5) * 1024);
1661
- console.error(style.dim(
1662
- ` ! ${gb(m.sizeBytes)} model vs ~${gb(ceiling)} default GPU ceiling — long prompts can\n` +
1663
- ` hit an uncatchable Metal OOM. If that happens, raise the limit first:\n` +
1664
- ` sudo sysctl iogpu.wired_limit_mb=${suggestMb} (resets on reboot)`,
1665
- ));
1666
- }
1667
- }
1668
1772
  const sNative = step("native runtime");
1669
1773
  await ensureNative(sNative);
1670
1774
  sNative.done("native runtime ready");
@@ -1684,9 +1788,8 @@ switch (cmd) {
1684
1788
  }
1685
1789
  // Speculative decoding (mlx_lm.server parity): --draft-model <path|query>
1686
1790
  // resolves like the main model; --num-draft-tokens defaults to 3 (the
1687
- // server's default upstream — mlx_lm.generate's is 2). Serial-lane-only:
1688
- // with --batch N, a mounted draft routes every request serial (upstream
1689
- // is_batchable = draft is None).
1791
+ // server's default upstream — mlx_lm.generate's is 2). Qualified providers
1792
+ // use shared execution; ordinary requests retain their own placement.
1690
1793
  const draftQuery = opt("draft-model");
1691
1794
  let draftModelDir: string | undefined;
1692
1795
  if (draftQuery !== null) {
@@ -1908,6 +2011,200 @@ switch (cmd) {
1908
2011
  break;
1909
2012
  }
1910
2013
 
2014
+ case "transcribe": {
2015
+ // Speech-to-text, one-shot, no server: audio file → Whisper → text.
2016
+ const { openWhisperModel } = await import("./model/factory");
2017
+ const { loadWhisperTokenizer } = await import("./audio/whisper-tokenizer");
2018
+ const { WhisperTranscriber } = await import("./audio/whisper-transcribe");
2019
+ const { formatTranscription, TRANSCRIPTION_FORMATS } = await import("./audio/whisper-format");
2020
+ const { TranscriptionService } = await import("./serve/transcription-service");
2021
+ const { formatTimestamp } = await import("./audio/whisper-format");
2022
+
2023
+ const audioPath = positional(0);
2024
+ if (!audioPath) {
2025
+ console.error("usage: mlx-bun transcribe <audio-file> [query] [--language en] [--format text|json|verbose_json|srt|vtt]");
2026
+ process.exit(1);
2027
+ }
2028
+ const format = (opt("format") ?? "text") as import("./audio/whisper-format").TranscriptionFormat;
2029
+ if (!TRANSCRIPTION_FORMATS.includes(format)) {
2030
+ console.error(`--format must be one of ${TRANSCRIPTION_FORMATS.join(", ")}`);
2031
+ process.exit(1);
2032
+ }
2033
+ const bytes = new Uint8Array(await Bun.file(audioPath).arrayBuffer());
2034
+ const samples = await TranscriptionService.decodeAudio(bytes);
2035
+ const durationSeconds = samples.length / 16_000;
2036
+
2037
+ const modelQuery = opt("model") ?? positional(1) ?? opt("query");
2038
+ let modelDir: string;
2039
+ if (modelQuery && existsSync(join(modelQuery, "config.json"))) modelDir = modelQuery;
2040
+ else if (modelQuery) modelDir = (await resolveModelAuto(modelQuery)).m.path;
2041
+ else {
2042
+ const reg = new Registry();
2043
+ if (reg.list().length === 0) await reg.scan();
2044
+ const w = reg.list().find((r) => r.modelType === "whisper");
2045
+ if (!w) {
2046
+ console.error("no Whisper model downloaded — try: mlx-bun get mlx-community/whisper-large-v3-turbo");
2047
+ process.exit(1);
2048
+ }
2049
+ modelDir = w.path;
2050
+ }
2051
+ let vadGate: { segments: { start: number; end: number }[] } | null = null;
2052
+ if (flag("vad")) {
2053
+ const { SileroVad } = await import("./audio/silero-vad");
2054
+ const tv = performance.now();
2055
+ const vad = SileroVad.load(opt("vad-model"));
2056
+ const { segments } = vad.detect(samples, { threshold: Number(opt("vad-threshold") ?? 0.5) });
2057
+ vadGate = { segments };
2058
+ if (flag("verbose")) console.error(`vad: ${segments.length} speech segment(s) in ${(performance.now() - tv).toFixed(0)} ms`);
2059
+ if (segments.length === 0) { process.stdout.write(format === "text" ? "\n" : JSON.stringify({ text: "", vad: { speech: false, segments: [] } }) + "\n"); break; }
2060
+ }
2061
+ const { model } = await openWhisperModel(modelDir);
2062
+ const tok = await loadWhisperTokenizer(modelDir, model.dims.nVocab);
2063
+ const transcriber = new WhisperTranscriber(model, tok);
2064
+ void vadGate;
2065
+ const langOpt = opt("language");
2066
+ const task = opt("task") === "translate" ? "translate" as const : "transcribe" as const;
2067
+ const beam = opt("beam-size");
2068
+ const temperature = opt("temperature");
2069
+ const verbose = flag("verbose");
2070
+ const t0 = performance.now();
2071
+ const result = await transcriber.transcribe(samples, {
2072
+ language: !langOpt || langOpt === "auto" ? null : langOpt,
2073
+ task,
2074
+ beamSize: beam ? Number(beam) : null,
2075
+ ...(temperature ? { temperature: Number(temperature) } : flag("no-fallback") ? { temperature: 0 } : {}),
2076
+ initialPrompt: opt("prompt"),
2077
+ withoutTimestamps: flag("no-timestamps"),
2078
+ conditionOnPreviousText: !flag("no-condition"),
2079
+ fast: !flag("faithful"),
2080
+ wordTimestamps: flag("word-timestamps"),
2081
+ audioCtx: opt("audio-ctx") ? Number(opt("audio-ctx")) : null,
2082
+ onSegment: verbose
2083
+ ? (s) => console.error(`[${formatTimestamp(s.start, ".")} --> ${formatTimestamp(s.end, ".")}] ${s.text.trim()}`)
2084
+ : undefined,
2085
+ });
2086
+ const elapsed = (performance.now() - t0) / 1000;
2087
+ if (verbose)
2088
+ console.error(`${durationSeconds.toFixed(2)} s audio in ${elapsed.toFixed(3)} s (${(durationSeconds / elapsed).toFixed(1)}× realtime), language ${result.language}`);
2089
+ process.stdout.write(formatTranscription(result, format, durationSeconds, task));
2090
+ transcriber.dispose();
2091
+ model.dispose();
2092
+ break;
2093
+ }
2094
+
2095
+ case "dictate": {
2096
+ const { startMicCapture } = await import("./audio/mic-capture");
2097
+ const { TranscriptionService, defaultWhisperRecord } = await import("./serve/transcription-service");
2098
+ const { style } = await import("./tui");
2099
+ const serverUrl = opt("server");
2100
+ const hotkeyFlag = argv.includes("--hotkey");
2101
+ const hotkey = hotkeyFlag ? Number(opt("hotkey") ?? 61) || 61 : null;
2102
+ const language = opt("language") ?? "en";
2103
+ const beam = opt("beam-size");
2104
+ const vocabulary = opt("vocabulary")?.split(",").map((s) => s.trim()).filter(Boolean);
2105
+ const params = {
2106
+ language: language === "auto" ? null : language,
2107
+ beamSize: beam ? Number(beam) : null,
2108
+ temperature: 0,
2109
+ prompt: opt("prompt"),
2110
+ vocabulary,
2111
+ vad: flag("no-vad") ? null : { threshold: 0.5, minSpeechMs: 120 },
2112
+ };
2113
+ const typeDelay = Number(opt("type-delay") ?? (hotkey ? 0 : 1));
2114
+
2115
+ // backend: in-process service or a running server's sessions
2116
+ let service: InstanceType<typeof TranscriptionService> | null = null;
2117
+ if (!serverUrl) {
2118
+ const q = opt("model") ?? positional(0) ?? opt("query");
2119
+ let modelDir: string; let modelId: string;
2120
+ if (q && existsSync(join(q, "config.json"))) { modelDir = q; modelId = q; }
2121
+ else if (q) { const { m } = await resolveModelAuto(q); modelDir = m.path; modelId = m.repoId; }
2122
+ else {
2123
+ const rec = await defaultWhisperRecord();
2124
+ if (!rec) { console.error("no Whisper model downloaded — try: mlx-bun get mlx-community/whisper-large-v3-turbo"); process.exit(1); }
2125
+ modelDir = rec.path; modelId = rec.repoId;
2126
+ }
2127
+ const idle = opt("idle-unload");
2128
+ service = new TranscriptionService({ modelDir, modelId, idleUnloadSec: idle !== null ? Number(idle) : 30, resident: flag("resident"), log: () => {} });
2129
+ process.stderr.write(style.dim("loading whisper… "));
2130
+ await service.ensureLoaded();
2131
+ service.vad();
2132
+ process.stderr.write(style.dim("ready\n"));
2133
+ }
2134
+ const base = serverUrl?.replace(/\/$/, "");
2135
+ interface Take { feed(pcm: Float32Array): Promise<void>; finish(): Promise<{ text: string; ms: number }>; }
2136
+ const startTake = async (): Promise<Take> => {
2137
+ if (service) {
2138
+ const session = await service.createSession(params);
2139
+ return {
2140
+ feed: async (pcm) => { await session.append(pcm); },
2141
+ finish: async () => { const t0 = performance.now(); const o = await session.finish(); return { text: o.result.text.trim(), ms: performance.now() - t0 }; },
2142
+ };
2143
+ }
2144
+ const r = await fetch(`${base}/v1/audio/sessions`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ language: params.language, beam_size: params.beamSize, temperature: 0, prompt: params.prompt, vocabulary, vad: !!params.vad, vad_min_speech_ms: 120 }) });
2145
+ if (!r.ok) throw new Error(`session create failed: ${r.status} ${await r.text()}`);
2146
+ const { id } = await r.json() as { id: string };
2147
+ return {
2148
+ feed: async (pcm) => { await fetch(`${base}/v1/audio/sessions/${id}/audio`, { method: "POST", headers: { "content-type": "audio/pcm;rate=16000" }, body: new Uint8Array(pcm.buffer.slice(pcm.byteOffset, pcm.byteOffset + pcm.byteLength) as ArrayBuffer) }); },
2149
+ finish: async () => { const t0 = performance.now(); const f = await fetch(`${base}/v1/audio/sessions/${id}/finish`, { method: "POST" }); const b = await f.json() as { text: string }; return { text: (b.text ?? "").trim(), ms: performance.now() - t0 }; },
2150
+ };
2151
+ };
2152
+
2153
+ const deliver = async (text: string) => {
2154
+ if (!text) { console.log(style.dim("(no speech)")); return; }
2155
+ console.log(text);
2156
+ if (flag("copy")) { const p = Bun.spawn(["pbcopy"], { stdin: "pipe" }); p.stdin.write(text); p.stdin.end(); await p.exited; }
2157
+ if (flag("type")) {
2158
+ if (typeDelay > 0) await Bun.sleep(typeDelay * 1000);
2159
+ const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
2160
+ const r = Bun.spawnSync(["osascript", "-e", `tell application "System Events" to keystroke "${escaped}"`], { stderr: "pipe" });
2161
+ if (r.exitCode !== 0) console.error(style.dim(`typing failed (grant Accessibility to your terminal): ${r.stderr.toString().trim()}`));
2162
+ }
2163
+ };
2164
+
2165
+ const mic = await startMicCapture({ hotkey });
2166
+ let take: Take | null = null;
2167
+ let pending: Float32Array[] = []; let pendingLen = 0;
2168
+ let feeding: Promise<void> = Promise.resolve();
2169
+ const flush = () => {
2170
+ if (!take || pendingLen === 0) return;
2171
+ const buf = new Float32Array(pendingLen); let o = 0; for (const p of pending) { buf.set(p, o); o += p.length; }
2172
+ pending = []; pendingLen = 0;
2173
+ const t = take;
2174
+ feeding = feeding.then(() => t.feed(buf)).catch((e) => console.error(style.dim(`feed failed: ${(e as Error).message}`)));
2175
+ };
2176
+ const begin = async () => { if (take) return; take = await startTake(); pending = []; pendingLen = 0; process.stderr.write(style.bold("● recording") + style.dim(hotkey ? " (release to stop)\n" : " (Enter to stop)\n")); };
2177
+ const end = async () => {
2178
+ if (!take) return;
2179
+ flush();
2180
+ await feeding;
2181
+ const t = take; take = null;
2182
+ process.stderr.write(style.dim("transcribing… "));
2183
+ try { const { text, ms } = await t.finish(); process.stderr.write(style.dim(`${ms.toFixed(0)} ms\n`)); await deliver(text); }
2184
+ catch (e) { console.error(`transcription failed: ${(e as Error).message}`); }
2185
+ };
2186
+ if (!hotkey) {
2187
+ console.error(style.dim("Enter: start/stop a take · q: quit"));
2188
+ (async () => {
2189
+ for await (const line of console) {
2190
+ if (line.trim() === "q") { mic.stop(); process.exit(0); }
2191
+ if (take) await end(); else await begin();
2192
+ }
2193
+ })();
2194
+ }
2195
+ process.once("SIGINT", () => { mic.stop(); process.exit(0); });
2196
+ for await (const ev of mic.events) {
2197
+ if (ev.kind === "ready") { if (hotkey) console.error(style.dim(`hold key ${hotkey} to talk · Ctrl-C quits · mic ${ev.info}`)); }
2198
+ else if (ev.kind === "error") { console.error(ev.message); mic.stop(); process.exit(1); }
2199
+ else if (ev.kind === "hotkey") { if (ev.down) await begin(); else await end(); }
2200
+ else if (ev.kind === "pcm" && take) {
2201
+ pending.push(ev.samples); pendingLen += ev.samples.length;
2202
+ if (pendingLen >= 16_000 / 4) flush(); // every 250 ms of audio
2203
+ }
2204
+ }
2205
+ break;
2206
+ }
2207
+
1911
2208
  case "bench":
1912
2209
  case "benchmark": {
1913
2210
  const { banner, step, box, style } = await import("./tui");
@@ -1,6 +1,7 @@
1
1
  /** Request facts only. Native resources remain with the preparation owner. */
2
2
  export interface ExecutionRequirements {
3
3
  readonly hasVision: boolean;
4
+ readonly hasPreparedPrefixIdentity?: boolean;
4
5
  readonly hasAdapters: boolean;
5
6
  readonly hasRepetitionPenalty: boolean;
6
7
  readonly userSeed: boolean;
@@ -17,9 +18,29 @@ export interface ExecutionCapabilities {
17
18
  readonly continuous: boolean;
18
19
  readonly quantizedBatch: boolean;
19
20
  readonly grammarBatch: boolean;
21
+ readonly adapterBatch?: boolean;
22
+ /** Prepared media can enter the ordinary shared execution group. */
23
+ readonly mediaBatch?: boolean;
24
+ readonly mediaPrefixCache?: boolean;
25
+ readonly pagedBatch?: boolean;
20
26
  readonly checkpoints: boolean;
27
+ /** Bound ordinary driver can restore/capture this request configuration. */
28
+ readonly sharedCheckpoints?: boolean;
21
29
  /** Model-qualified speculative execution can retain this request's KV codec. */
22
30
  readonly speculativeKvQuant?: boolean;
31
+ readonly speculativeTurboQuant?: boolean;
32
+ readonly turboQuantBatch?: boolean;
33
+ readonly speculativeLogprobs?: boolean;
34
+ /** The grouped provider supports the target's mounted adapter context. */
35
+ readonly sharedSpeculativeAdapters?: boolean;
36
+ /** Methods supplied by the model's shared execution binding. */
37
+ readonly groupedMethods?: readonly string[];
38
+ /** A shared method accepts request-owned grammar continuation proposals. */
39
+ readonly sharedGrammarProposals?: boolean;
40
+ /** A grouped method can consume this request's strict known continuations. */
41
+ readonly sharedFill?: boolean;
42
+ /** A mounted provider can consume verified external continuations. */
43
+ readonly sharedSpeculativeEcho?: boolean;
23
44
  /** A graph-owned compiled step exists; cache geometry can still decline it. */
24
45
  readonly compiledDecode?: boolean;
25
46
  }
@@ -0,0 +1,8 @@
1
+ /** Exact reusable work, independent of token-prefix matching. The same cache
2
+ * owner chooses residency and persistence for objects and conversation state. */
3
+ export interface ObjectCache<Value> {
4
+ /** Borrow immutable data through an owned lease. A miss permits recompute. */
5
+ take(key: string): Promise<{ value: Value; dispose(): void } | null>;
6
+ /** Successful return transfers ownership, including immediate eviction. */
7
+ put(key: string, value: Value): void;
8
+ }
@@ -13,7 +13,7 @@ export function piModelDefinition(options: PiModelOptions = {}) {
13
13
  name: options.name ?? "mlx-bun (local)",
14
14
  api: "openai-completions" as const,
15
15
  reasoning: options.reasoning ?? false,
16
- compat: { supportsDeveloperRole: false,
16
+ compat: { supportsDeveloperRole: false, sendSessionAffinityHeaders: true,
17
17
  ...(options.reasoning ? { thinkingFormat: "qwen-chat-template" as const } : {}) },
18
18
  input: (options.vision ? ["text", "image"] : ["text"]) as ("text" | "image")[],
19
19
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
@@ -160,7 +160,9 @@ export type ServerMessage =
160
160
  // provider declaration — clients use it instead of probing for a 400.
161
161
  // `thinking`: whether the model has a switchable reasoning channel (drives
162
162
  // the UI's thinking on/off toggle; false hides it).
163
- | { type: "ready"; model: string; vision: boolean; audio: boolean; thinking: boolean; genDefaults: ReadyGenDefaults }
163
+ // transcription: a Whisper checkpoint is configured or resolvable for
164
+ // /v1/audio/sessions — drives the composer's hold-to-talk mic button.
165
+ | { type: "ready"; model: string; vision: boolean; audio: boolean; thinking: boolean; genDefaults: ReadyGenDefaults; transcription?: boolean }
164
166
  | { type: "turn_start" }
165
167
  | { type: "text_delta"; delta: string }
166
168
  | { type: "thinking_delta"; delta: string }
@@ -0,0 +1,33 @@
1
+ /** A reusable prefix and the backend/method state that covers its tokens.
2
+ * The caller owns a returned hit. After releasing or returning its state,
3
+ * it must release any retained backing through retain. */
4
+ export interface PrefixCacheHit<State, Attachment = never> {
5
+ tokens: number[];
6
+ caches: State;
7
+ attachments?: Attachment;
8
+ retain?: () => void;
9
+ }
10
+
11
+ /** Storage is independent of request scheduling and the method producing
12
+ * state. Both prefill and decode can publish processed-token prefixes.
13
+ * State is opaque here; its backend/method defines layout and alignment.
14
+ * Implementations own prefix selection, retention and storage tiers. */
15
+ export interface PrefixCache<State, Attachment = never> {
16
+ /** Give optional retained state back to the allocator at an execution
17
+ * boundary. This never rejects or changes the active request. */
18
+ reclaim?(): void;
19
+ /** Drop session affinity without deleting reusable checkpoints. */
20
+ closeSession?(sessionId: string): void;
21
+ /** Prepare a reusable prefix before execution. The returned release ends
22
+ * request interest; cache residency and IO remain implementation-owned. */
23
+ prefetch?(prompt: number[], ns?: string, sessionId?: string): Promise<() => void>;
24
+ /** Return owned state for a usable session continuation, otherwise the
25
+ * longest usable prefix, or null on a miss. Session identity is an affinity
26
+ * hint and never changes the content or numerical namespace.
27
+ * Namespace distinguishes incompatible artifacts, methods and adapters. */
28
+ take(prompt: number[], ns?: string, sessionId?: string): PrefixCacheHit<State, Attachment> | null;
29
+ /** Transfer state and its optional backing release to storage. If this
30
+ * throws, ownership stays with the caller; successful return transfers
31
+ * ownership even when storage immediately spills or disposes the entry. */
32
+ put(tokens: number[], caches: State, ns?: string, retain?: () => void, attachments?: Attachment, sessionId?: string): void;
33
+ }
@@ -16,3 +16,10 @@ export interface StateView<T> {
16
16
  export interface DisposableResource {
17
17
  dispose(): void;
18
18
  }
19
+
20
+ /** A prepared replacement for live state. Commit once between method rounds;
21
+ * it publishes already-built state without graph work or validation. Dispose
22
+ * releases the unpublished replacement, or the superseded state after commit. */
23
+ export interface PreparedStateChange extends DisposableResource {
24
+ commit(): void;
25
+ }
@@ -0,0 +1,23 @@
1
+ /** Sampling owns score processing, token selection, and request-local history.
2
+ * Score/token representations and sync/device execution belong to the backend;
3
+ * this contract has no scheduling, cache, or tensor-runtime dependency. */
4
+ export interface SampledToken<Token, Metadata> {
5
+ token: Token;
6
+ extras: Metadata | null;
7
+ }
8
+
9
+ export interface SamplingSession<Scores, DeviceToken, Result> {
10
+ /** Optional stateless operation shared across compatible rows and positions.
11
+ * It borrows scores and returns owned device tokens, without advancing history. */
12
+ readonly independent?: { sample(scores: Scores): DeviceToken };
13
+ readonly isPlainGreedy: boolean;
14
+ readonly capturesLogprobs: boolean;
15
+ readonly needsHistory: boolean;
16
+ /** Borrows scores; transfers ownership of the result to the caller. */
17
+ sample(logits: Scores, step: number): Result;
18
+ seedHistory(tokens: readonly number[]): void;
19
+ /** Commit accepted tokens when configured for manual history updates. */
20
+ commitDevice(token: DeviceToken): void;
21
+ commitNumbers(tokens: readonly number[]): void;
22
+ dispose(): void;
23
+ }
@@ -1,9 +1,31 @@
1
+ /** Backend-owned context shared by compatible requests. A key identifies
2
+ * equivalent execution state; enter returns its scoped release. Scheduling
3
+ * observes group readiness without inspecting this context. */
4
+ export interface ExecutionContext {
5
+ readonly key: string;
6
+ enter(): () => void;
7
+ }
8
+
1
9
  /** A backend execution group exposes bounded work units and readiness. Queue
2
10
  * payloads, tensors, cache merging and sampling remain inside its adapter. */
3
11
  export interface ExecutionGroup {
12
+ /** Native preparation may borrow the execution lease between work units.
13
+ * Payloads and task failures belong to the backend's queue. */
14
+ readonly pendingTasks?: number;
15
+ advanceTask?(): Promise<void>;
4
16
  readonly active: number;
5
17
  readonly queued: number;
6
18
  readonly preparing: boolean;
19
+ readonly preparingRows?: number;
20
+ /** The preparation owner can accept another row before its next work unit. */
21
+ readonly canPrepareMore?: boolean;
22
+ /** Work estimates affect grouping only. A request larger than this budget
23
+ * still runs alone; it is never rejected or sent to another executor. */
24
+ readonly preparingTokens?: number;
25
+ readonly nextPreparationTokens?: number;
26
+ readonly maxPreparationTokens?: number;
27
+ /** The latest preparation published output that can be flushed by yielding. */
28
+ readonly preparationPublishedOutput?: boolean;
7
29
  readonly maxActive: number;
8
30
  readonly admissionHeld: boolean;
9
31
  readonly closed: boolean;
@@ -12,6 +34,11 @@ export interface ExecutionGroup {
12
34
  admitNext(): boolean;
13
35
  /** Read-only budget check used to preserve admission before decode. */
14
36
  canBurst(): boolean;
37
+ /** Method-owned token demand for a compatible mixed iteration. Absent when
38
+ * the backend has no packed implementation for the current work. */
39
+ readonly mixedPreparation?: { readonly runningTokens: number; readonly minimumPreparationTokens: number };
40
+ readonly maxIterationTokens?: number;
41
+ advanceMixed?(tokenBudget: number): Promise<void>;
15
42
  advancePreparation(): Promise<void>;
16
43
  advance(): Promise<void>;
17
44
  failActive(error: unknown): void;