mlx-bun 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (364) hide show
  1. package/README.md +71 -588
  2. package/docs/README.md +91 -0
  3. package/docs/{design → archive/investigations}/audio-input-plan.md +4 -4
  4. package/docs/{design → archive/investigations}/diffusion-gemma-port.md +3 -3
  5. package/docs/{design → archive/investigations}/faithful-l1-consolidation.md +4 -4
  6. package/docs/{design → archive/investigations}/minicpm5-decode-megakernel.md +11 -11
  7. package/docs/{design → archive/investigations}/optimization_plan.md +1 -1
  8. package/docs/{design → archive/investigations}/pre-colibri-stabilization.md +5 -5
  9. package/docs/{design → archive/investigations}/the-dreaming-handoff.md +2 -2
  10. package/docs/design/batching.md +569 -0
  11. package/docs/design/decode-speed-program.md +3178 -169
  12. package/docs/design/dreaming-nightly-pipeline.md +675 -282
  13. package/docs/design/generic-model-support.md +579 -334
  14. package/docs/design/kv-cache.md +680 -0
  15. package/docs/design/orpo-training.md +673 -1239
  16. package/docs/design/speculative-decoding.md +1627 -0
  17. package/docs/design/turboquant.md +2102 -0
  18. package/docs/design/unified-engine-frontier-plan.md +1831 -727
  19. package/docs/design/web-chat-redesign.md +666 -1083
  20. package/docs/planning/PRODUCT_ROADMAP.md +34 -1
  21. package/docs/planning/release-notes-v0.3.0.md +35 -0
  22. package/docs/planning/video-series.md +2 -2
  23. package/docs/reference/benchmarks.md +1162 -0
  24. package/docs/reference/cli.md +394 -159
  25. package/docs/reference/distribution.md +321 -182
  26. package/docs/reference/environment.md +348 -0
  27. package/docs/reference/glossary.md +39 -0
  28. package/docs/reference/library-api.md +566 -168
  29. package/docs/reference/memory.md +227 -64
  30. package/docs/reference/models.md +104 -6
  31. package/docs/reference/server-api.md +886 -425
  32. package/docs/reference/server-config.md +638 -330
  33. package/docs/reference/training.md +393 -254
  34. package/docs/reference/troubleshooting.md +166 -0
  35. package/package.json +9 -4
  36. package/src/audio/decode.ts +2 -2
  37. package/src/audio/features.ts +2 -2
  38. package/src/backends/mlx/autoregressive.ts +108 -0
  39. package/src/backends/mlx/batch-group.ts +1646 -0
  40. package/src/backends/mlx/diffusion.ts +39 -0
  41. package/src/backends/mlx/draft-target.ts +47 -0
  42. package/src/backends/mlx/evaluation.ts +105 -0
  43. package/src/backends/mlx/fill-append.ts +45 -0
  44. package/src/backends/mlx/gateway-binding.ts +71 -0
  45. package/src/backends/mlx/graph.ts +38 -0
  46. package/src/backends/mlx/memory-client.ts +189 -0
  47. package/src/backends/mlx/memory-guard.ts +40 -0
  48. package/src/backends/mlx/methods.ts +90 -0
  49. package/src/backends/mlx/model-prompt.ts +164 -0
  50. package/src/backends/mlx/model-serving.ts +79 -0
  51. package/src/backends/mlx/prefill.ts +33 -0
  52. package/src/backends/mlx/request-policy.ts +20 -0
  53. package/src/backends/mlx/rollback.ts +51 -0
  54. package/src/backends/mlx/serial-executor.ts +273 -0
  55. package/src/backends/mlx/speculative.ts +114 -0
  56. package/src/backends/mlx/state-views.ts +26 -0
  57. package/src/backends/mlx/text-engine.ts +73 -0
  58. package/src/cli.ts +58 -25
  59. package/src/client.ts +66 -0
  60. package/src/config.ts +21 -4
  61. package/src/contracts/completion.ts +16 -0
  62. package/src/contracts/execution.ts +49 -0
  63. package/src/contracts/generation.ts +103 -0
  64. package/src/contracts/host.ts +7 -0
  65. package/src/contracts/jobs.ts +66 -0
  66. package/src/contracts/pi-provider.ts +30 -0
  67. package/src/contracts/pi.ts +259 -0
  68. package/src/contracts/resources.ts +18 -0
  69. package/src/contracts/scheduling.ts +27 -0
  70. package/src/contracts/tsconfig.json +14 -0
  71. package/src/diffusion/diffusion-generate.ts +218 -153
  72. package/src/embed.ts +1 -1
  73. package/src/engine/admission.ts +59 -0
  74. package/src/engine/cancellation.ts +38 -0
  75. package/src/engine/engine.ts +56 -0
  76. package/src/engine/execution-coordinator.ts +70 -0
  77. package/src/engine/execution-plan.ts +51 -0
  78. package/src/engine/resources.ts +42 -0
  79. package/src/engine/scheduler.ts +67 -0
  80. package/src/engine/session.ts +247 -0
  81. package/src/eval/README.md +44 -1
  82. package/src/eval/kl.ts +236 -3
  83. package/src/eval/reference-logits.ts +207 -0
  84. package/src/eval/runner.ts +8 -96
  85. package/src/fill/echo-index.ts +188 -0
  86. package/src/fill/fill-session.ts +452 -0
  87. package/src/fill/proposal.ts +68 -0
  88. package/src/fill/schema-rows.ts +506 -0
  89. package/src/fill/tool-boundary.ts +103 -0
  90. package/src/fit.ts +1 -1
  91. package/src/generate.ts +656 -268
  92. package/src/grammar.ts +12 -7
  93. package/src/harness-pi.ts +14 -33
  94. package/src/hub-rest.ts +4 -4
  95. package/src/index.ts +7 -0
  96. package/src/inference/denoising.ts +17 -0
  97. package/src/inference/graph.ts +33 -0
  98. package/src/inference/prefill.ts +39 -0
  99. package/src/inference/rollback.ts +7 -0
  100. package/src/jobs/db.ts +4 -0
  101. package/src/jobs/index.ts +2 -0
  102. package/src/jobs/job-entry.ts +12 -12
  103. package/src/jobs/runner.ts +98 -13
  104. package/src/jobs/task-client.ts +19 -0
  105. package/src/jobs/types.ts +2 -58
  106. package/src/kv-store.ts +109 -49
  107. package/src/lab/README.md +18 -0
  108. package/src/{assets → lab/curve}/curve-designer.html +2 -2
  109. package/src/{curve-sampler.ts → lab/curve/curve-sampler.ts} +2 -2
  110. package/src/{expert-trace.ts → lab/expert-trace/expert-trace.ts} +5 -5
  111. package/src/{model → lab/paged-kv}/paged-kv.ts +9 -6
  112. package/src/library.ts +40 -0
  113. package/src/lora.ts +18 -0
  114. package/src/media-fetch.ts +1 -1
  115. package/src/memory/article.ts +1 -1
  116. package/src/memory/chunk.ts +1 -1
  117. package/src/memory/model.ts +30 -169
  118. package/src/memory/prompts.ts +1 -1
  119. package/src/memory/rest.ts +1 -1
  120. package/src/memory/schedule.ts +1 -1
  121. package/src/memory/stages.ts +4 -1
  122. package/src/memory/vault.ts +1 -3
  123. package/src/mlx/array.ts +9 -10
  124. package/src/mlx/compile.ts +12 -2
  125. package/src/mlx/ffi.ts +58 -11
  126. package/src/mlx/materialize.ts +23 -0
  127. package/src/mlx/metal-kernel.ts +10 -11
  128. package/src/mlx/ops.ts +33 -13
  129. package/src/mlx/quantized-rows.ts +36 -0
  130. package/src/mlx/token-bitmask.ts +52 -0
  131. package/src/mlx/turboquant-kv-decode.ts +82 -0
  132. package/src/mlx/turboquant-kv-inverse.ts +122 -0
  133. package/src/mlx/turboquant-ops.ts +33 -3
  134. package/src/mlx/turboquant-tables.ts +2 -2
  135. package/src/model/batched-mask.ts +8 -2
  136. package/src/model/batched-quant.ts +1 -1
  137. package/src/model/batched-rotating-quant.ts +2 -2
  138. package/src/model/batched-rotating.ts +1 -1
  139. package/src/model/compiled-decode.ts +1 -1
  140. package/src/model/diffusion-gemma.ts +8 -8
  141. package/src/model/factory.ts +76 -33
  142. package/src/model/fingerprint.ts +1 -1
  143. package/src/model/flash-attention.ts +3 -3
  144. package/src/model/gemma4-base.ts +26 -9
  145. package/src/model/gemma4.ts +6 -6
  146. package/src/model/generated/gemma4-12b.ts +2 -2
  147. package/src/model/generated/gemma4-26b.ts +2 -2
  148. package/src/model/generated/gemma4-e4b.ts +2 -2
  149. package/src/model/glm52-dsa.ts +13 -9
  150. package/src/model/glm52-memory.ts +1 -1
  151. package/src/model/implementation.ts +69 -0
  152. package/src/model/minicpm5.ts +6 -8
  153. package/src/model/profile.ts +12 -2
  154. package/src/model/qwen-append.ts +12 -0
  155. package/src/model/qwen-conv.ts +52 -0
  156. package/src/model/qwen3_5-checkpoint.ts +164 -0
  157. package/src/model/qwen3_5.ts +140 -56
  158. package/src/model/support.ts +1 -1
  159. package/src/model/trellis-balanced-scatter.ts +51 -0
  160. package/src/model/trellis-linear.ts +672 -0
  161. package/src/model/trellis-shared-m.ts +151 -0
  162. package/src/model/trellis-shared-scatter.ts +144 -0
  163. package/src/model/trellis-splitk-prefill.ts +158 -0
  164. package/src/model/trellis-tiled-prefill.ts +136 -0
  165. package/src/model/trellis-vector-expand.ts +62 -0
  166. package/src/model/trellis-wide-prefill.ts +94 -0
  167. package/src/model/universal/dense.ts +1 -1
  168. package/src/model/universal/modules.ts +8 -1
  169. package/src/native-pack.ts +6 -5
  170. package/src/pi-provider.ts +6 -64
  171. package/src/pi-web.ts +8 -315
  172. package/src/prompt-cache.ts +96 -53
  173. package/src/quantize/allocator.ts +1 -1
  174. package/src/quantize/config-writer.ts +10 -3
  175. package/src/quantize/rotate.ts +4 -4
  176. package/src/quantize/trellis.ts +586 -0
  177. package/src/responses.ts +28 -1
  178. package/src/runtime-config.ts +18 -4
  179. package/src/sampler.ts +4 -4
  180. package/src/serve/batch-scheduler.ts +8 -1671
  181. package/src/serve/chat-request.ts +391 -0
  182. package/src/serve/chat-stage.ts +195 -0
  183. package/src/serve/checkpoint-identity.ts +64 -0
  184. package/src/serve/completion-executor.ts +15 -7
  185. package/src/serve/discovery-routes.ts +12 -13
  186. package/src/serve/generation-gateway.ts +154 -193
  187. package/src/serve/http.ts +159 -0
  188. package/src/serve/inference-request.ts +79 -0
  189. package/src/serve/isolate.ts +297 -105
  190. package/src/serve/lab-routes.ts +3 -0
  191. package/src/serve/lane-registry.ts +2 -1
  192. package/src/serve/model-admin-routes.ts +7 -5
  193. package/src/serve/model-binding.ts +43 -0
  194. package/src/serve/model-host.ts +512 -0
  195. package/src/serve/openai-wire.ts +103 -0
  196. package/src/serve/parent-application.ts +56 -0
  197. package/src/serve/pipeline.ts +33 -0
  198. package/src/serve/preparation.ts +33 -0
  199. package/src/serve/request-plan.ts +19 -15
  200. package/src/serve/request-prep.ts +346 -0
  201. package/src/serve/response-lease.ts +24 -0
  202. package/src/serve/responses-client.ts +69 -0
  203. package/src/serve/session-completion-engine.ts +110 -0
  204. package/src/serve/session-search.ts +1 -1
  205. package/src/serve/static-routes.ts +5 -5
  206. package/src/serve/text-completion-stage.ts +104 -0
  207. package/src/serve/token-streams.ts +519 -0
  208. package/src/server.ts +317 -2649
  209. package/src/spec/assistant-source.ts +10 -44
  210. package/src/spec/deepspec-source.ts +6 -6
  211. package/src/spec/dflash-source.ts +8 -8
  212. package/src/spec/dspark/ab-stats.ts +3 -3
  213. package/src/spec/dspark/calibration.ts +2 -2
  214. package/src/spec/dspark/deepspec-module.ts +2 -2
  215. package/src/spec/dspark/generate-dflash.ts +2 -2
  216. package/src/spec/dspark/generate.ts +2 -2
  217. package/src/spec/dspark/loader.ts +1 -1
  218. package/src/spec/dspark/module-dflash.ts +4 -3
  219. package/src/spec/dspark/module.ts +3 -3
  220. package/src/spec/dspark/quantize-drafter.ts +1 -1
  221. package/src/spec/generate.ts +1 -1
  222. package/src/spec/glm52-mtp-source.ts +1 -1
  223. package/src/spec/ngram-source.ts +1 -1
  224. package/src/spec/prefix-state.ts +45 -0
  225. package/src/spec/qwen-mtp-source.ts +166 -69
  226. package/src/spec/serve-loop.ts +154 -156
  227. package/src/spec/source.ts +42 -10
  228. package/src/spec/two-model.ts +1 -1
  229. package/src/ssd-cache.ts +80 -12
  230. package/src/ssd-durability.ts +25 -13
  231. package/src/tool-call.ts +51 -14
  232. package/src/train/flash-cce.ts +2 -2
  233. package/src/train/forward.ts +6 -0
  234. package/src/train/loss.ts +1 -1
  235. package/src/train/prefix-shared.ts +5 -1
  236. package/src/train/segmented.ts +5 -3
  237. package/src/train/trainer.ts +6 -6
  238. package/src/vision/prompt.ts +1 -1
  239. package/src/vision/qwen3vl-tower.ts +52 -10
  240. package/src/web/app.html +1 -1
  241. package/src/web/app.js +5 -3
  242. package/src/web/src/adapters-panel.ts +3 -3
  243. package/src/web/src/assistant.ts +2 -36
  244. package/src/web/src/chat.ts +4 -4
  245. package/src/web/src/composer.ts +4 -4
  246. package/src/web/src/hub.ts +1 -1
  247. package/src/web/src/markdown.ts +0 -0
  248. package/src/web/src/memory-panel.ts +1 -1
  249. package/src/web/src/model-picker.ts +1 -1
  250. package/src/web/src/palette.ts +1 -1
  251. package/src/web/src/protocol.ts +3 -39
  252. package/src/web/src/sessions.ts +1 -1
  253. package/src/web/src/ui-catalog.ts +4 -18
  254. package/src/web-assets.ts +47 -0
  255. package/src/weights.ts +55 -9
  256. package/docs/archive/investigations/curve-runs/bisector-route.json +0 -98
  257. package/docs/archive/investigations/curve-runs/bisector-trace.json +0 -1393
  258. package/docs/archive/investigations/curve-runs/curve-terrain.json +0 -1179
  259. package/docs/archive/investigations/curve-runs/curve-terrain.svg +0 -42
  260. package/docs/archive/investigations/hlg-ab.html +0 -34
  261. package/docs/archive/investigations/hlg-explorer.html +0 -238
  262. package/docs/archive/investigations/hlg-figs/cliff.svg +0 -1
  263. package/docs/archive/investigations/hlg-figs/cross.svg +0 -1
  264. package/docs/archive/investigations/hlg-figs/edges.svg +0 -1
  265. package/docs/archive/investigations/hlg-figs/k20-grids.svg +0 -1
  266. package/docs/archive/investigations/hlg-figs/k20-peaks.svg +0 -1
  267. package/docs/archive/investigations/hlg-figs/lw-curve.svg +0 -1
  268. package/docs/archive/investigations/hlg-figs/terrain.svg +0 -1
  269. package/docs/archive/investigations/hlg-report.html +0 -262
  270. package/docs/archive/investigations/hlg-runs/hlg-ab.json +0 -149
  271. package/docs/archive/investigations/hlg-runs/hlg-blind-prompts.md +0 -87
  272. package/docs/archive/investigations/hlg-runs/hlg-blind.json +0 -107
  273. package/docs/archive/investigations/hlg-runs/hlg-cliff.json +0 -197
  274. package/docs/archive/investigations/hlg-runs/hlg-cross.json +0 -505
  275. package/docs/archive/investigations/hlg-runs/hlg-edge.json +0 -388
  276. package/docs/archive/investigations/hlg-runs/hlg-eval1.json +0 -25
  277. package/docs/archive/investigations/hlg-runs/hlg-frontier.json +0 -86
  278. package/docs/archive/investigations/hlg-runs/hlg-grid.json +0 -242
  279. package/docs/archive/investigations/hlg-runs/hlg-grid2.json +0 -343
  280. package/docs/archive/investigations/hlg-runs/hlg-lw-coarse.json +0 -110
  281. package/docs/archive/investigations/hlg-runs/hlg-map.json +0 -383
  282. package/docs/archive/investigations/hlg-runs/run0-original.log +0 -159
  283. package/docs/archive/investigations/hlg-runs/runA-recommended.log +0 -131
  284. package/docs/archive/investigations/hlg-runs/runAA-cliff-cpm5.log +0 -26
  285. package/docs/archive/investigations/hlg-runs/runAB-grid2-e4b.log +0 -18
  286. package/docs/archive/investigations/hlg-runs/runAC-peak-e4b-partial.log +0 -25
  287. package/docs/archive/investigations/hlg-runs/runAD-ab-samples.log +0 -8
  288. package/docs/archive/investigations/hlg-runs/runAE-edge-e4b.log +0 -51
  289. package/docs/archive/investigations/hlg-runs/runAF-logit-calibration.log +0 -10
  290. package/docs/archive/investigations/hlg-runs/runAG-eval-config2.log +0 -16
  291. package/docs/archive/investigations/hlg-runs/runB-gain-variants.log +0 -192
  292. package/docs/archive/investigations/hlg-runs/runC-pivots.log +0 -162
  293. package/docs/archive/investigations/hlg-runs/runD-oetf.log +0 -132
  294. package/docs/archive/investigations/hlg-runs/runE-eotf.log +0 -162
  295. package/docs/archive/investigations/hlg-runs/runF-shaper-working.log +0 -86
  296. package/docs/archive/investigations/hlg-runs/runG-shaper-sweep.log +0 -101
  297. package/docs/archive/investigations/hlg-runs/runH-wsweep.log +0 -176
  298. package/docs/archive/investigations/hlg-runs/runI-asweep.log +0 -176
  299. package/docs/archive/investigations/hlg-runs/runJ-knobmap.log +0 -49
  300. package/docs/archive/investigations/hlg-runs/runK-frontier.log +0 -19
  301. package/docs/archive/investigations/hlg-runs/runL-widemap.log +0 -65
  302. package/docs/archive/investigations/hlg-runs/runM-frontier-emb.log +0 -15
  303. package/docs/archive/investigations/hlg-runs/runN-verify.log +0 -116
  304. package/docs/archive/investigations/hlg-runs/runO-frontier-seed5000.log +0 -15
  305. package/docs/archive/investigations/hlg-runs/runP-grid-AxSm.log +0 -15
  306. package/docs/archive/investigations/hlg-runs/runQ-grid-tgW.log +0 -15
  307. package/docs/archive/investigations/hlg-runs/runR-blind-transcript.log +0 -79
  308. package/docs/archive/investigations/hlg-runs/runS-cross-coherence.log +0 -33
  309. package/docs/archive/investigations/hlg-runs/runT-cross-cpm5.log +0 -33
  310. package/docs/archive/investigations/hlg-runs/runU-cross-12b.log +0 -33
  311. package/docs/archive/investigations/hlg-runs/runV-lw-e4b.log +0 -22
  312. package/docs/archive/investigations/hlg-runs/runW-lw-12b.log +0 -22
  313. package/docs/archive/investigations/hlg-runs/runX-lw-cpm5.log +0 -22
  314. package/docs/archive/investigations/hlg-runs/runY-cliff-e4b.log +0 -26
  315. package/docs/archive/investigations/hlg-runs/runZ-cliff-12b.log +0 -26
  316. package/docs/archive/mlx-bun-lab-report.html +0 -275
  317. package/docs/archive/wwdc-mlx-bun.html +0 -848
  318. package/docs/design/adapters-end-to-end.md +0 -205
  319. package/docs/design/batching-perf-path.md +0 -255
  320. package/docs/design/batching-v2-plan.md +0 -529
  321. package/docs/design/bucketing-stage.md +0 -197
  322. package/docs/design/compat-cli-surface-design.md +0 -230
  323. package/docs/design/docs-reorg-plan.md +0 -165
  324. package/docs/design/dspark-serving-program.md +0 -371
  325. package/docs/design/dspark-speculative-decoding.md +0 -318
  326. package/docs/design/grammar-spec-batching-integration.md +0 -301
  327. package/docs/design/memory-inference-path.md +0 -422
  328. package/docs/design/memory-synthesis.md +0 -287
  329. package/docs/design/memory-system.md +0 -237
  330. package/docs/design/mlx-lm-serving-execution-seam.md +0 -81
  331. package/docs/design/mlx-lm-tool-parity-plan.md +0 -545
  332. package/docs/design/omlx-adoption-map.md +0 -98
  333. package/docs/design/orpo-dynamic-lambda.md +0 -220
  334. package/docs/design/orpo-future-enhancements.md +0 -340
  335. package/docs/design/paged-kv-cache.md +0 -214
  336. package/docs/design/parallel-slots.md +0 -567
  337. package/docs/design/parity-tier-dag.md +0 -161
  338. package/docs/design/repo-cleanup-plan.md +0 -231
  339. package/docs/design/runtime-isolation.md +0 -263
  340. package/docs/design/segmented-backward-training.md +0 -543
  341. package/docs/design/spec-decode-larger-targets.md +0 -229
  342. package/docs/design/ssd-kv-cold-tier.md +0 -415
  343. package/docs/design/structured-output.md +0 -484
  344. package/docs/design/tauri-desktop-app.md +0 -127
  345. package/docs/design/the-dreaming-master-plan.md +0 -445
  346. package/docs/design/turboquant-kv.md +0 -235
  347. package/docs/design/turboquant-weights.md +0 -467
  348. package/docs/design/w4a16-compute-precision-spike.md +0 -232
  349. package/docs/design/web-chat-beat-matrix.md +0 -315
  350. package/docs/design/write-pipeline-entity.md +0 -468
  351. package/docs/planning/IDEAS.md +0 -69
  352. package/docs/reference/embedding.md +0 -197
  353. package/docs/reference/features-matrix.md +0 -101
  354. package/docs/reference/orpo-quickstart.md +0 -135
  355. package/src/flags.ts +0 -11
  356. /package/docs/{design → archive/investigations}/colibri-glm52-port.md +0 -0
  357. /package/docs/{planning → archive/planning}/chunk-task-roadmap.md +0 -0
  358. /package/docs/{planning → archive/planning}/curve-sampler-research-plan.md +0 -0
  359. /package/docs/{planning → archive/planning}/memory-docs-and-dag-plan.md +0 -0
  360. /package/docs/{planning → archive/planning}/pi-integration-review.md +0 -0
  361. /package/docs/{planning → archive/planning}/release-notes-v0.2.0.md +0 -0
  362. /package/src/{memory-plan.ts → admission-plan.ts} +0 -0
  363. /package/{docs/archive/investigations → src/serve/assets}/curve-terrain.html +0 -0
  364. /package/{docs/dag → src/serve/assets}/training-inference-map.html +0 -0
@@ -0,0 +1,56 @@
1
+ import { handleStaticRoute } from "./static-routes";
2
+ import { STATIC_ROUTE_ASSETS } from "../web-assets";
3
+ import { createResponsesClient } from "./responses-client";
4
+ import type { DisposableResource } from "../contracts/resources";
5
+ import type { JobStore } from "../jobs/db";
6
+ import { handleLabRoute, matchLabRoute } from "./lab-routes";
7
+ import { handleAdminRoute, matchAdminRoute } from "./admin-routes";
8
+
9
+ /** CPU application state stays in the parent when model workers restart/evict.
10
+ * Native inspection/merge/export routes still execute inside a model worker. */
11
+ export function createParentApplication(options: {
12
+ acquireGpu(signal: AbortSignal): Promise<DisposableResource>;
13
+ serverPort(): number | undefined;
14
+ invalidateLibrary(): void;
15
+ createJobStore?(): Promise<JobStore>;
16
+ }) {
17
+ const responses = createResponsesClient();
18
+ let jobs: Promise<JobStore> | undefined;
19
+ let closed = false;
20
+ const ensureJobs = () => {
21
+ if (closed) return Promise.reject(new Error("application host is closed"));
22
+ return jobs ??= (async () => {
23
+ const { JobStore, registerRunner } = await import("../jobs");
24
+ const store = options.createJobStore ? await options.createJobStore() : new JobStore();
25
+ store.markZombies();
26
+ const { datasetRunner } = await import("../dataset/job");
27
+ registerRunner("dataset", datasetRunner);
28
+ return store;
29
+ })();
30
+ };
31
+ return {
32
+ get responseStats() { return responses.stats; },
33
+ async handle(request: Request, forward?: (request: Request) => Promise<Response>): Promise<Response | null> {
34
+ const url = new URL(request.url);
35
+ const staticResponse = handleStaticRoute(url, request, STATIC_ROUTE_ASSETS);
36
+ if (staticResponse) return staticResponse;
37
+ if (forward && request.method === "POST" && url.pathname === "/v1/responses")
38
+ return responses.forward(request, forward);
39
+ const admin = matchAdminRoute(request.method, url.pathname);
40
+ if (admin) return handleAdminRoute(url, request, { ensureJobs, invalidateLibrary: options.invalidateLibrary });
41
+ const route = matchLabRoute(request.method, url.pathname);
42
+ if (route !== "quantize-submit" && route !== "finetune-submit" &&
43
+ route !== "dataset-submit" && route !== "dataset-templates") return null;
44
+ return handleLabRoute(url, request, { ensureJobs, acquireGpu: options.acquireGpu,
45
+ serverPort: options.serverPort, invalidateLibrary: options.invalidateLibrary });
46
+ },
47
+ async close() {
48
+ closed = true;
49
+ if (!jobs) return;
50
+ const store = await jobs;
51
+ const { closeSubprocessJobs, closeInProcessJobs } = await import("../jobs");
52
+ await Promise.all([closeSubprocessJobs(store), closeInProcessJobs(store)]);
53
+ store.close();
54
+ },
55
+ };
56
+ }
@@ -0,0 +1,33 @@
1
+ // Shared contracts of the request pipeline. Each stage is one configured
2
+ // program — `run(input) → output` — and the server composes them in order,
3
+ // like a shell pipe:
4
+ //
5
+ // new ChatRequest(body) → ChatStage.run → InferenceRequest
6
+ // → InferenceStage.run → InferenceResult → wire JSON/SSE
7
+ //
8
+ // A stage knows only the interface it reads and the one it writes.
9
+ import type { CompletionEvent } from "./completion-sink";
10
+ import type { CompletionUsage } from "./completion-executor";
11
+ import type { PromptResponseTrace } from "./prompt-response-trace";
12
+
13
+ /** A request a stage refuses. `status` is the HTTP status the wire adapter
14
+ * answers with; `body` the protocol-neutral error object (adapters reshape
15
+ * it: OpenAI `{error}`, Anthropic `{type:"error"}`, Responses `{error}`). */
16
+ export class RequestError extends Error {
17
+ constructor(
18
+ readonly status: number,
19
+ message: string,
20
+ readonly body: Record<string, unknown> = { message },
21
+ ) {
22
+ super(message);
23
+ }
24
+ }
25
+
26
+ /** What flows back to the caller while a request is in flight: cancellation,
27
+ * tracing, and (for streaming surfaces) events + live usage. */
28
+ export interface RunControl {
29
+ signal?: AbortSignal;
30
+ trace?: PromptResponseTrace;
31
+ onEvents?(events: readonly CompletionEvent[]): void | false | Promise<void | false>;
32
+ onUsageProgress?(usage: Readonly<CompletionUsage>): void;
33
+ }
@@ -0,0 +1,33 @@
1
+ /** Host-side admission before grammar/media preparation can allocate native
2
+ * resources. The implementation shares the generation execution domain. */
3
+ export interface PreparationExecutor {
4
+ reserve?(kind: "media" | "constraint", signal?: AbortSignal): Promise<import("../contracts/resources").DisposableResource>;
5
+ run<T>(prepare: () => Promise<T>, signal?: AbortSignal): Promise<T>;
6
+ }
7
+
8
+ import { AdmissionPool } from "../engine/admission";
9
+ import { CancellationSource } from "../engine/cancellation";
10
+
11
+ export async function acquireReservation(pool: Pick<AdmissionPool, "acquire">, signal?: AbortSignal) {
12
+ signal?.throwIfAborted();
13
+ const cancellation = new CancellationSource();
14
+ const abort = () => cancellation.cancel("requested");
15
+ signal?.addEventListener("abort", abort, { once: true });
16
+ try { return await pool.acquire(cancellation); }
17
+ finally { signal?.removeEventListener("abort", abort); }
18
+ }
19
+
20
+ /** One retained media preparation at a time; grammar rows may form a batch. */
21
+ export function createPreparationExecutor(
22
+ run: PreparationExecutor["run"], grammarCapacity: number,
23
+ ): PreparationExecutor & { close(): void } {
24
+ const media = new AdmissionPool(1);
25
+ const constraints = new AdmissionPool(Math.max(1, grammarCapacity));
26
+ return {
27
+ run,
28
+ async reserve(kind, signal) {
29
+ return acquireReservation(kind === "media" ? media : constraints, signal);
30
+ },
31
+ close() { media.close(); constraints.close(); },
32
+ };
33
+ }
@@ -1,28 +1,31 @@
1
+ import { snapshotGenerationPolicy } from "../backends/mlx/request-policy";
1
2
  import type { GenerateOptions } from "../generate";
2
3
  import type { RequestShape } from "./generation-gateway";
3
-
4
- export interface DisposableResource {
5
- dispose(): void;
6
- }
4
+ import type { DisposableResource } from "../contracts/resources";
5
+ import { disposeResources, ownResource } from "../engine/resources";
6
+ export type { DisposableResource } from "../contracts/resources";
7
7
 
8
8
  export class RequestOwnership {
9
- #resources = new Set<DisposableResource>();
10
- #transferred = false;
9
+ #owner = ownResource(new Set<DisposableResource>(), disposeResources);
10
+ #reservations = ownResource(new Set<DisposableResource>(), disposeResources);
11
+
12
+ /** Reservations live until completion, including after native ownership transfers. */
13
+ retain<T extends DisposableResource>(reservation: T): T {
14
+ this.#reservations.borrow().add(reservation);
15
+ return reservation;
16
+ }
11
17
 
12
18
  own<T extends DisposableResource | null | undefined>(resource: T): T {
13
- if (resource) this.#resources.add(resource);
19
+ if (resource) this.#owner.borrow().add(resource);
14
20
  return resource;
15
21
  }
16
22
 
17
23
  transfer(): void {
18
- this.#transferred = true;
19
- this.#resources.clear();
24
+ this.#owner.transfer();
20
25
  }
21
26
 
22
27
  dispose(): void {
23
- if (this.#transferred) return;
24
- for (const resource of this.#resources) resource.dispose();
25
- this.#resources.clear();
28
+ disposeResources([{ dispose: () => this.#owner.close() }, { dispose: () => this.#reservations.close() }]);
26
29
  }
27
30
  }
28
31
 
@@ -123,6 +126,7 @@ export function planRequest(input: RequestPlanInput): PlanRequestResult {
123
126
 
124
127
  const options = { ...input.options };
125
128
  options.maxTokens = Math.min(input.requestedMaxTokens, available);
129
+ options.seedWasExplicit = input.userSeed;
126
130
  if (input.adapterIds.length) options.adapters = [...input.adapterIds];
127
131
  const captureLogprobs = !input.stream && (input.wantLogprobs || input.topLogprobs > 0);
128
132
  if (captureLogprobs) {
@@ -147,9 +151,9 @@ export function planRequest(input: RequestPlanInput): PlanRequestResult {
147
151
  };
148
152
 
149
153
  return new RequestPlan({
150
- promptIds: input.promptIds,
151
- options,
152
- shape,
154
+ promptIds: Object.freeze([...input.promptIds]) as number[],
155
+ options: snapshotGenerationPolicy(options),
156
+ shape: Object.freeze(shape),
153
157
  captureLogprobs,
154
158
  wantLogprobs: input.wantLogprobs,
155
159
  topLogprobs: input.topLogprobs,
@@ -0,0 +1,346 @@
1
+ // Per-request preparation shared by every completion surface (chat,
2
+ // raw text, /signal, /generate): request → GenerateOptions with the
3
+ // server/model defaults folded in, chat-template rendering + prompt ids,
4
+ // the stable prompt-cache boundary probe, grammar compilation, and the
5
+ // tool-call stream router. Resolved ONCE per server (createRequestPrep) and
6
+ // captured by the route handlers. Extracted from src/server.ts (repo-taming
7
+ // Phase 4).
8
+ import type { ToolDefinition } from "../chat-template";
9
+ import {
10
+ fillEchoConfig,
11
+ fillMaxSpan,
12
+ FillSession,
13
+ resolveFillMode,
14
+ } from "../fill/fill-session";
15
+ import { compileStrictFillRows, type StrictRowPlan } from "../fill/schema-rows";
16
+ import type { GenerateOptions } from "../generate";
17
+ import { compileGrammarRequest, grammarEnabled, type GrammarRequest } from "../grammar";
18
+ import type { KvSchemeOptions } from "../kv-scheme";
19
+ import { isMiniCPM5Config } from "../model/support";
20
+ import type { HlgConfig } from "../sampler";
21
+ import {
22
+ nextDefaultSeed,
23
+ normalizeMessages,
24
+ parseLogitBias,
25
+ promptEndsInOpenThink,
26
+ resolveHlg,
27
+ type ChatRequestParams,
28
+ } from "./chat-request";
29
+ import type { ServingContext } from "./model-host";
30
+ import { selectToolStreamMode, ToolAwareStream, type ToolStreamMode } from "./token-streams";
31
+
32
+ /** The ServerOptions fields request preparation reads (server-wide
33
+ * defaults a request field overrides). */
34
+ export interface RequestPrepOptions {
35
+ defaultThinking?: boolean;
36
+ defaultTemperature?: number;
37
+ defaultTopP?: number;
38
+ defaultTopK?: number;
39
+ hlg?: HlgConfig;
40
+ pagedKv?: { blockSize?: number };
41
+ }
42
+
43
+ export function createRequestPrep(input: {
44
+ ctx: ServingContext;
45
+ serverOptions: RequestPrepOptions;
46
+ /** The server-wide KV scheme (resolveKvScheme(...).generationOptions),
47
+ * spread into every request's GenerateOptions. */
48
+ kvScheme: KvSchemeOptions;
49
+ /** `--max-tokens` / the GLM memory plan's generation cap; undefined = the
50
+ * surface's own default (chat 65 536, raw completion 512). */
51
+ defaultGeneratedTokens: number | undefined;
52
+ }) {
53
+ const { ctx, serverOptions, kvScheme, defaultGeneratedTokens } = input;
54
+
55
+ // XTC never removes EOS or the newline token: mlx_lm.server passes
56
+ // [tokenizer.eos_token_id, tokenizer.encode("\n")] as xtc_special_tokens.
57
+ // We pass the flat equivalent — ALL configured EOS ids plus the bare newline
58
+ // id(s), encoded WITHOUT special tokens (mlx-lm's encode("\n") can drag a
59
+ // BOS along on some tokenizers; protecting BOS from XTC is a no-op, so the
60
+ // flat/no-BOS form is behaviorally identical and cleaner).
61
+ const xtcSpecialTokens = [
62
+ ...ctx.model.config.eosTokenIds,
63
+ ...ctx.tokenizer.encode("\n", false),
64
+ ];
65
+
66
+ // Effective enable_thinking for a request, with the same precedence the chat
67
+ // template uses (extracted so sampling and template rendering can't disagree):
68
+ // explicit chat_template_kwargs.enable_thinking → reasoning_effort ("none" =
69
+ // off) → server --thinking default → model default (MiniCPM5 → off). undefined
70
+ // means "not a switchable-thinking model / leave the template default".
71
+ const resolveEnableThinking = (req: ChatRequestParams): boolean | undefined => {
72
+ const explicit = req.chat_template_kwargs?.enable_thinking;
73
+ if (typeof explicit === "boolean") return explicit;
74
+ const effort = req.reasoning_effort;
75
+ if (effort !== undefined) return effort !== "none";
76
+ if (serverOptions.defaultThinking !== undefined) return serverOptions.defaultThinking;
77
+ return isMiniCPM5Config(ctx.model.config) ? false : undefined;
78
+ };
79
+
80
+ const toOptions = (req: ChatRequestParams): GenerateOptions & { stopSequences: string[] } => {
81
+ // Sampling follows the thinking state — which the web UI's thinking button
82
+ // drives via enable_thinking. Model authors publish a SINGLE
83
+ // generation_config temperature (the think-mode value) but recommend a
84
+ // cooler one for direct, no-think replies (MiniCPM5 card: 0.9 think / 0.7
85
+ // no-think, top_p 0.95 for both). So with no explicit temperature set, a
86
+ // no-think turn runs at most NO_THINK_TEMPERATURE while a think turn keeps
87
+ // the model's hotter configured default. An explicit request/CLI
88
+ // temperature always wins. top_p is unchanged by mode.
89
+ const NO_THINK_TEMPERATURE = 0.7;
90
+ const genTemp = ctx.genDefaults.temperature ?? 0.7;
91
+ const defaultTemp = resolveEnableThinking(req) === false
92
+ ? Math.min(genTemp, NO_THINK_TEMPERATURE)
93
+ : genTemp;
94
+ return {
95
+ // No invented cap: unset means "generate until EOS or the admitted
96
+ // context is exhausted" — admission (planRequest) is the real limit
97
+ // and clamps to maxSafeContext − promptLen. Only an explicit request
98
+ // max_tokens or the operator's --max-tokens narrows it further.
99
+ maxTokens: req.max_completion_tokens ?? req.max_tokens ??
100
+ defaultGeneratedTokens,
101
+ temperature: req.temperature ?? serverOptions.defaultTemperature ?? defaultTemp,
102
+ topP: req.top_p ?? serverOptions.defaultTopP ?? ctx.genDefaults.topP ?? 0,
103
+ topK: req.top_k ?? serverOptions.defaultTopK ?? ctx.genDefaults.topK ?? 0,
104
+ seed: req.seed ?? nextDefaultSeed(),
105
+ repetitionPenalty: req.repetition_penalty ?? ctx.genDefaults.repetitionPenalty,
106
+ // mlx_lm.server sampling extensions (defaults mirror server.py: min_p /
107
+ // xtc off, context windows 20). parseLogitBias throws on non-numeric
108
+ // input — callers surface it as a 400 (mlx-lm's coercion error).
109
+ minP: req.min_p ?? 0,
110
+ xtcProbability: req.xtc_probability ?? 0,
111
+ xtcThreshold: req.xtc_threshold ?? 0,
112
+ ...(req.xtc_probability ? { xtcSpecialTokens } : {}),
113
+ logitBias: parseLogitBias(req.logit_bias),
114
+ repetitionContextSize: req.repetition_context_size ?? 20,
115
+ presencePenalty: req.presence_penalty ?? 0,
116
+ presenceContextSize: req.presence_context_size ?? 20,
117
+ frequencyPenalty: req.frequency_penalty ?? 0,
118
+ frequencyContextSize: req.frequency_context_size ?? 20,
119
+ hlg: resolveHlg(req.hlg, serverOptions.hlg),
120
+ // generate() yields token ids; stop sequences match on decoded text
121
+ // (they can span token boundaries), so the StopMatcher sits at the
122
+ // decode layer below and halts the loop via onToken → false.
123
+ stopSequences: (typeof req.stop === "string" ? [req.stop] : req.stop ?? [])
124
+ .filter((s) => typeof s === "string" && s.length > 0),
125
+ ...kvScheme,
126
+ // Paged KV is a server-wide mode like the kv scheme above (v1:
127
+ // serial bf16 gemma4 — the createServer refusals hold the invariants).
128
+ ...(serverOptions.pagedKv ? { pagedKv: serverOptions.pagedKv } : {}),
129
+ };
130
+ };
131
+
132
+ /** Compile a grammar controller from the request's structured-output fields
133
+ * (response_format / guided_* / structured_outputs). Returns null when no
134
+ * constraint is requested OR when compile failed (degrade path). On degrade,
135
+ * `degradeHint` carries a human description for the system-prompt injection
136
+ * + Warning header (oMLX parity — never 500). Honors MLX_BUN_GRAMMAR=0. */
137
+ const compileGrammarForRequest = async (
138
+ req: ChatRequestParams,
139
+ ): Promise<{ controller: import("../grammar").GrammarController | null; degradeHint: string | null }> => {
140
+ if (!grammarEnabled()) {
141
+ return {
142
+ controller: null,
143
+ degradeHint: "grammar compilation disabled by MLX_BUN_GRAMMAR=0",
144
+ };
145
+ }
146
+ const r = await compileGrammarRequest(
147
+ req as GrammarRequest,
148
+ ctx.tokenizer,
149
+ ctx.model.config.text.vocabSize,
150
+ );
151
+ if (!r) return { controller: null, degradeHint: null };
152
+ return { controller: r.controller, degradeHint: r.degradeHint };
153
+ };
154
+
155
+ // Map OpenAI reasoning_effort levels onto the Qwen3.8 template's supported
156
+ // set (xhigh|medium|low — the template raises on anything else). "none"
157
+ // means thinking off (handled by resolveEnableThinking), so no depth is
158
+ // passed. Only consumed for templates with readsReasoningEffort.
159
+ const qwenReasoningEffort = (
160
+ effort: ChatRequestParams["reasoning_effort"],
161
+ ): "xhigh" | "medium" | "low" | undefined => {
162
+ switch (effort) {
163
+ case "minimal":
164
+ case "low":
165
+ return "low";
166
+ case "medium":
167
+ return "medium";
168
+ case "high":
169
+ case "xhigh":
170
+ return "xhigh";
171
+ default:
172
+ return undefined; // "none" / unset → template default (xhigh)
173
+ }
174
+ };
175
+
176
+ const templateOptionsFor = (req: ChatRequestParams, tools: ToolDefinition[] | null) => {
177
+ // enableThinking resolution (and its precedence) lives in
178
+ // resolveEnableThinking so template rendering and sampling stay in sync.
179
+ return {
180
+ tools,
181
+ enableThinking: resolveEnableThinking(req),
182
+ reasoningEffort: qwenReasoningEffort(req.reasoning_effort),
183
+ preserveThinking: req.chat_template_kwargs?.preserve_thinking,
184
+ };
185
+ };
186
+
187
+ const promptIdsFor = (
188
+ req: ChatRequestParams,
189
+ tools: ToolDefinition[] | null,
190
+ ): { ids: number[]; startInThinking: boolean } => {
191
+ const opts = templateOptionsFor(req, tools);
192
+ const rendered = ctx.template.render(normalizeMessages(req.messages), opts);
193
+ const ids = ctx.tokenizer.encode(rendered);
194
+ // template includes <bos>; tokenizer post-processor also prepends one
195
+ const trimmed = ids[0] === ids[1] && ids[0] === ctx.tokenizer.bosTokenId ? ids.slice(1) : ids;
196
+ return { ids: trimmed, startInThinking: promptEndsInOpenThink(rendered) };
197
+ };
198
+
199
+ /** STABLE cache boundary: the prompt's tail is a generation primer (e.g.
200
+ * 12B's `<|channel>thought` tokens) that the NEXT turn's re-render does
201
+ * not contain — a cache entry ending there can never prefix-match again
202
+ * once the rings wrap. Probe by rendering this conversation as if a
203
+ * reply existed; the common prefix is what every future turn preserves.
204
+ * The probe is EXPENSIVE (a second full render + encode — ~150 ms at
205
+ * 16k with our tokenizer; 30-50% of the 12B ctx-repeat TTFT,
206
+ * 2026-07-06b), but the PRIMER LENGTH it measures is constant per
207
+ * template mode: the divergence sits after a special-token turn
208
+ * delimiter, and special tokens break BPE merges, so message content
209
+ * can't shift it. So the full probe runs ONCE per mode and later
210
+ * requests pay a subtraction. (A wrong boundary is a QUALITY knob, not
211
+ * a correctness one — any prefix ≤ len-1 is a valid snapshot point.) */
212
+ const primerLenByMode = new Map<string, number>();
213
+ const stableLenFor = (
214
+ req: ChatRequestParams,
215
+ tools: ToolDefinition[] | null,
216
+ trimmed: number[],
217
+ ): number => {
218
+ const opts = templateOptionsFor(req, tools);
219
+ const mode = `${opts.enableThinking}|${!!tools?.length}|${opts.reasoningEffort ?? ""}|${opts.preserveThinking ?? ""}`;
220
+ const primer = primerLenByMode.get(mode);
221
+ if (primer !== undefined) return Math.max(0, trimmed.length - primer);
222
+ let stableLen = trimmed.length;
223
+ try {
224
+ const probe = ctx.tokenizer.encode(
225
+ ctx.template.render(
226
+ [...normalizeMessages(req.messages), { role: "assistant", content: "x" }],
227
+ opts,
228
+ ),
229
+ );
230
+ const probeTrimmed =
231
+ probe[0] === probe[1] && probe[0] === ctx.tokenizer.bosTokenId ? probe.slice(1) : probe;
232
+ let i = 0;
233
+ const n = Math.min(trimmed.length, probeTrimmed.length);
234
+ while (i < n && trimmed[i] === probeTrimmed[i]) i++;
235
+ stableLen = i;
236
+ // Memoize the mode's primer length (sanity-capped: a "primer" longer
237
+ // than 64 tokens means the probe diverged for content reasons —
238
+ // don't generalize that).
239
+ const p = trimmed.length - stableLen;
240
+ if (p >= 0 && p <= 64) primerLenByMode.set(mode, p);
241
+ } catch { /* probe is best-effort; full boundary is the fallback */ }
242
+ return stableLen;
243
+ };
244
+
245
+ const toolStreamMode = (tools: ToolDefinition[] | null): ToolStreamMode =>
246
+ // Gemma-4 keeps the token-level sentinel path; every other family
247
+ // (MiniCPM5, Qwen3/3.5, Tier-0 generics) emits tool calls as DECODED TEXT
248
+ // — see selectToolStreamMode for why sentinel ids must stay family-gated.
249
+ selectToolStreamMode(ctx.model.config.modelType, !!tools?.length);
250
+
251
+ const toolRouter = (
252
+ tools: ToolDefinition[] | null,
253
+ onParseFailure?: () => void,
254
+ ): ToolAwareStream =>
255
+ new ToolAwareStream(ctx.tokenizer, toolStreamMode(tools), tools, onParseFailure);
256
+
257
+ /** Strict fill rows are a function of (template mode, tool signature) only —
258
+ * they are token spans of the assistant turn, which opens with a special
259
+ * token, so message content cannot shift them (the same argument that lets
260
+ * stableLenFor memoize its primer length). Compiling costs several template
261
+ * renders + encodes, so it happens ONCE per mode. */
262
+ const fillRowsByMode = new Map<string, StrictRowPlan>();
263
+ const toolsSignature = (tools: ToolDefinition[]): string =>
264
+ JSON.stringify(tools.map((t) => [
265
+ t.function?.name,
266
+ Object.keys((t.function?.parameters?.properties ?? {}) as Record<string, unknown>),
267
+ (t.function?.parameters?.required as unknown) ?? null,
268
+ (t.function?.parameters?.additionalProperties as unknown) ?? null,
269
+ ]));
270
+
271
+ /** Token fast-forwarding (docs/design/speculative-decoding.md "Token
272
+ * fast-forwarding"): compile this request's determined-span table, or null
273
+ * when the feature is off or the request's shape refuses it.
274
+ *
275
+ * Refusals owned here (body-level); the ones only ChatStage can see —
276
+ * a compiled grammar, media prompts, a mounted draft model, a quantized KV
277
+ * scheme — are applied there. Continuous (batch) placement needs no
278
+ * refusal: generate() is the only fill site, so a batched request simply
279
+ * does not fill.
280
+ *
281
+ * MLX_BUN_FILL=echo additionally arms the echo index (K3c, Lab tier): the
282
+ * strict rows keep policy "assert", and copied spans ride the same
283
+ * mechanism under policy "verify". */
284
+ const fillPlanFor = (
285
+ req: ChatRequestParams,
286
+ tools: ToolDefinition[] | null,
287
+ promptIds: number[],
288
+ ): FillSession | null => {
289
+ const mode = resolveFillMode();
290
+ if (mode === "off") return null;
291
+ if (!tools?.length) return null;
292
+ // A user-fixed seed means reproducibility: the sampler's step index would
293
+ // skip the injected positions, so an identical request could not replay.
294
+ if (req.seed !== undefined) return null;
295
+ // Injected tokens are never sampled — they have no logprob row.
296
+ if (req.logprobs === true) return null;
297
+ if (typeof req.top_logprobs === "number" && req.top_logprobs > 0) return null;
298
+ // Structured output owns forced-token production (grammar jump-forward).
299
+ if (req.response_format != null || req.guided_grammar || req.guided_regex ||
300
+ req.guided_choice?.length || req.structured_outputs != null) return null;
301
+
302
+ const opts = templateOptionsFor(req, tools);
303
+ const key = `${opts.enableThinking}|${opts.reasoningEffort ?? ""}|` +
304
+ `${opts.preserveThinking ?? ""}|${toolsSignature(tools)}`;
305
+ let plan = fillRowsByMode.get(key);
306
+ if (plan === undefined) {
307
+ plan = compileStrictFillRows({
308
+ template: ctx.template,
309
+ tokenizer: ctx.tokenizer,
310
+ messages: normalizeMessages(req.messages),
311
+ tools,
312
+ renderOptions: opts,
313
+ });
314
+ fillRowsByMode.set(key, plan);
315
+ }
316
+ // The echo tier can carry a request whose template yields no strict rows;
317
+ // strict-only mode cannot.
318
+ if (!plan.rows.length && mode !== "echo") return null;
319
+ return new FillSession(
320
+ {
321
+ rows: plan.rows,
322
+ echo: mode === "echo" ? fillEchoConfig() : null,
323
+ delimiters: new Set(plan.delimiters),
324
+ eos: ctx.model.config.eosTokenIds,
325
+ },
326
+ promptIds,
327
+ {
328
+ maxSpan: fillMaxSpan(), decode: (ids) => ctx.tokenizer.decode([...ids]),
329
+ strictContext: plan.createContext?.(promptIds),
330
+ },
331
+ );
332
+ };
333
+
334
+ return {
335
+ resolveEnableThinking,
336
+ toOptions,
337
+ compileGrammarForRequest,
338
+ templateOptionsFor,
339
+ promptIdsFor,
340
+ stableLenFor,
341
+ toolRouter,
342
+ fillPlanFor,
343
+ };
344
+ }
345
+
346
+ export type RequestPrep = ReturnType<typeof createRequestPrep>;
@@ -0,0 +1,24 @@
1
+ import type { DisposableResource } from "../contracts/resources";
2
+
3
+ /** Keep host activity owned through response EOF/error/cancel, without buffering
4
+ * the response. The parent additionally drains worker cleanup before GPU jobs. */
5
+ export function retainResponseLease(response: Response, lease: DisposableResource): Response {
6
+ if (!response.body) { lease.dispose(); return response; }
7
+ const reader = response.body.getReader();
8
+ let closed = false;
9
+ const release = () => {
10
+ if (closed) return;
11
+ closed = true;
12
+ reader.releaseLock(); lease.dispose();
13
+ };
14
+ return new Response(new ReadableStream<Uint8Array>({
15
+ async pull(controller) {
16
+ try {
17
+ const item = await reader.read();
18
+ if (item.done) { release(); controller.close(); }
19
+ else controller.enqueue(item.value);
20
+ } catch (error) { release(); controller.error(error); }
21
+ },
22
+ async cancel(reason) { try { await reader.cancel(reason); } finally { release(); } },
23
+ }), { status: response.status, statusText: response.statusText, headers: response.headers });
24
+ }
@@ -0,0 +1,69 @@
1
+ import { ResponseStore, resolveResponsesConversation, type ResponseHistory, type ResponsesRequest } from "../responses";
2
+
3
+ /** Application-owned conversation history over a data-only HTTP host. Workers
4
+ * translate/run the request; the parent stores only completed response records. */
5
+ export function createResponsesClient(store: ResponseHistory = new ResponseStore()) {
6
+ return {
7
+ get stats() { return { entries: store.size, bytes: store.totalBytes, max_bytes: store.maxBytes, ttl_ms: store.ttlMs }; },
8
+ async forward(request: Request, forward: (request: Request) => Promise<Response>): Promise<Response> {
9
+ const error = (status: number, message: string) => Response.json({ error: {
10
+ message, type: "invalid_request_error", param: null, code: null,
11
+ } }, { status });
12
+ let body: ResponsesRequest;
13
+ try { body = await request.json() as ResponsesRequest; }
14
+ catch { return error(400, "invalid JSON body"); }
15
+ if (!body || typeof body !== "object" || Array.isArray(body)) return error(400, "invalid JSON body");
16
+ let conversation: ReturnType<typeof resolveResponsesConversation>;
17
+ try { conversation = resolveResponsesConversation(body, store); }
18
+ catch (e) { return error(404, (e as Error).message); }
19
+ const headers = new Headers(request.headers);
20
+ headers.delete("content-length");
21
+ headers.set("x-mlx-bun-response-owner", "parent");
22
+ const response = await forward(new Request(request.url, {
23
+ method: "POST", headers, signal: request.signal,
24
+ body: JSON.stringify({ ...conversation.body, previous_response_id: undefined }),
25
+ }));
26
+ if (!response.ok) return response;
27
+ const remember = (value: Record<string, unknown>) => {
28
+ if (typeof value.id === "string" && Array.isArray(value.output))
29
+ store.put(value.id, { input: conversation.input, output: value.output, instructions: conversation.instructions });
30
+ };
31
+ const restorePrevious = (value: Record<string, unknown>) => ({ ...value, previous_response_id: conversation.previousId });
32
+ if (!body.stream) {
33
+ const value = await response.json() as Record<string, unknown>;
34
+ remember(value);
35
+ const headers = new Headers(response.headers); headers.delete("content-length");
36
+ return Response.json(restorePrevious(value), { status: response.status, headers });
37
+ }
38
+ if (!response.body) return response;
39
+ const decoder = new TextDecoder(), encoder = new TextEncoder();
40
+ let pending = "";
41
+ const rewrite = (frame: string): string => frame.replace(/^data: (.+)$/m, (line, data: string) => {
42
+ if (data === "[DONE]") return line;
43
+ const event = JSON.parse(data) as Record<string, unknown>;
44
+ if (event.response && typeof event.response === "object") {
45
+ const value = event.response as Record<string, unknown>;
46
+ if (event.type === "response.completed") remember(value);
47
+ event.response = restorePrevious(value);
48
+ }
49
+ return `data: ${JSON.stringify(event)}`;
50
+ });
51
+ const outputHeaders = new Headers(response.headers); outputHeaders.delete("content-length");
52
+ return new Response(response.body.pipeThrough(new TransformStream<Uint8Array, Uint8Array>({
53
+ transform(chunk, controller) {
54
+ pending += decoder.decode(chunk, { stream: true });
55
+ let split: number;
56
+ while ((split = pending.indexOf("\n\n")) !== -1) {
57
+ const frame = pending.slice(0, split);
58
+ pending = pending.slice(split + 2);
59
+ controller.enqueue(encoder.encode(`${rewrite(frame)}\n\n`));
60
+ }
61
+ },
62
+ flush(controller) {
63
+ pending += decoder.decode();
64
+ if (pending) controller.enqueue(encoder.encode(rewrite(pending)));
65
+ },
66
+ })), { status: response.status, headers: outputHeaders });
67
+ },
68
+ };
69
+ }