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
@@ -1,4 +1,11 @@
1
- // Runtime isolation — the parent half (docs/design/runtime-isolation.md).
1
+ import { ExecutionCoordinator } from "../engine/execution-coordinator";
2
+ import { AdmissionPool, AdmissionRejected } from "../engine/admission";
3
+ import { acquireReservation } from "./preparation";
4
+ import { createParentApplication } from "./parent-application";
5
+ import type { DisposableResource } from "../contracts/resources";
6
+ import { retainResponseLease } from "./response-lease";
7
+ import type { EngineHost } from "../contracts/host";
8
+ // Runtime isolation — the parent half (docs/reference/server-config.md).
2
9
  //
3
10
  // ARCHITECTURE (decision 2026-07-05, deviating from the doc's original
4
11
  // structured-IPC sketch — recorded there): the inference ENGINE is the
@@ -33,11 +40,17 @@ import { join } from "node:path";
33
40
 
34
41
  /** How to (re)start the engine child. `argv[0]` is the executable. */
35
42
  export interface EngineSpec {
43
+ /** Parent-owned activity lease; covers startup and complete response bodies. */
44
+ acquire?: (signal: AbortSignal) => Promise<DisposableResource>;
36
45
  argv: string[];
37
46
  socketPath: string;
38
47
  /** Max ms to wait for the child's /health after spawn (weights load —
39
48
  * large models take a while). Default 15 min. */
40
49
  readyTimeoutMs?: number;
50
+ /** Bound automatic restarts in a rolling window, including startup failure. */
51
+ maxRestarts?: number;
52
+ restartWindowMs?: number;
53
+ restartDelayMs?: number;
41
54
  }
42
55
 
43
56
  const HOP_BY_HOP = new Set([
@@ -50,17 +63,23 @@ export function defaultSocketPath(): string {
50
63
  }
51
64
 
52
65
  /** The persistent engine child: spawn, health-gate, respawn-on-crash. */
53
- export class EngineChild {
66
+ export class EngineChild implements EngineHost<Request, Response> {
54
67
  readonly spec: EngineSpec;
55
68
  #proc: ReturnType<typeof Bun.spawn> | null = null;
56
69
  #ready: Promise<void>;
57
70
  #stopping = false;
71
+ #lifetime = new AbortController();
58
72
  restarts = 0;
59
73
  #lastSpawnAt = 0;
74
+ #restartTimes: number[] = [];
75
+ #backoff: ReturnType<typeof setTimeout> | undefined;
76
+ #wakeBackoff: (() => void) | undefined;
77
+ #startups = new Set<Promise<void>>();
60
78
 
61
79
  constructor(spec: EngineSpec) {
62
- this.spec = spec;
80
+ this.spec = Object.freeze({ ...spec, argv: [...spec.argv] });
63
81
  this.#ready = this.#spawn();
82
+ void this.#ready.catch(() => {}); // failures remain observable through ready/forward
64
83
  }
65
84
 
66
85
  get ready(): Promise<void> {
@@ -71,38 +90,78 @@ export class EngineChild {
71
90
  return this.#proc?.pid ?? null;
72
91
  }
73
92
 
74
- async #spawn(): Promise<void> {
75
- this.#lastSpawnAt = Date.now();
76
- try { unlinkSync(this.spec.socketPath); } catch {}
77
- const proc = Bun.spawn(this.spec.argv, {
78
- stdio: ["ignore", "inherit", "inherit"], // load progress → user's terminal
79
- env: process.env,
80
- });
81
- this.#proc = proc;
82
- void proc.exited.then((code) => {
83
- if (this.#stopping || this.#proc !== proc) return;
84
- console.error(`[isolate] engine exited (code ${code}) — respawning`);
85
- this.restarts++;
86
- // Crash-loop backoff: an engine that dies within 10 s of spawning
87
- // (bad flags, OOM on load) waits 5 s before the retry.
88
- const delay = Date.now() - this.#lastSpawnAt < 10_000 ? 5_000 : 0;
89
- this.#ready = new Promise((r) => setTimeout(r, delay)).then(() => this.#spawn());
90
- });
91
- // Health-gate: poll the child's /health over the socket until it
92
- // answers — model load happens behind this.
93
- const deadline = Date.now() + (this.spec.readyTimeoutMs ?? 15 * 60_000);
94
- while (Date.now() < deadline) {
95
- if (proc.killed || this.#proc !== proc) throw new Error("engine died during startup");
96
- try {
97
- const r = await fetch("http://engine/health", {
98
- unix: this.spec.socketPath,
99
- signal: AbortSignal.timeout(2_000),
100
- } as RequestInit & { unix: string });
101
- if (r.ok) return;
102
- } catch { /* not up yet */ }
103
- await new Promise((r) => setTimeout(r, 250));
93
+ #spawn(): Promise<void> {
94
+ const work = this.#start();
95
+ this.#startups.add(work);
96
+ void work.finally(() => this.#startups.delete(work)).catch(() => {});
97
+ return work;
98
+ }
99
+
100
+ async #start(): Promise<void> {
101
+ if (this.#stopping) throw new Error("engine host is closed");
102
+ const lease = await this.spec.acquire?.(this.#lifetime.signal);
103
+ let starting: ReturnType<typeof Bun.spawn> | undefined;
104
+ let healthy = false;
105
+ try {
106
+ if (this.#stopping) throw new Error("engine host is closed");
107
+ this.#lastSpawnAt = Date.now();
108
+ try { unlinkSync(this.spec.socketPath); } catch {}
109
+ const proc = Bun.spawn(this.spec.argv, {
110
+ stdio: ["ignore", "inherit", "inherit"], // load progress → user's terminal
111
+ env: process.env,
112
+ });
113
+ this.#proc = proc;
114
+ starting = proc;
115
+ void proc.exited.then((code) => {
116
+ if (this.#stopping || this.#proc !== proc) return;
117
+ const now = Date.now();
118
+ const window = this.spec.restartWindowMs ?? 60_000;
119
+ this.#restartTimes = this.#restartTimes.filter((time) => now - time < window);
120
+ if (this.#restartTimes.length >= (this.spec.maxRestarts ?? 3)) {
121
+ this.#ready = Promise.reject(new Error(`engine restart limit reached after exit ${code}`));
122
+ void this.#ready.catch(() => {});
123
+ return;
124
+ }
125
+ this.#restartTimes.push(now);
126
+ console.error(`[isolate] engine exited (code ${code}) — respawning`);
127
+ this.restarts++;
128
+ // Crash-loop backoff: an engine that dies within 10 s of spawning
129
+ // (bad flags, OOM on load) waits 5 s before the retry.
130
+ const delay = this.spec.restartDelayMs ?? (Date.now() - this.#lastSpawnAt < 10_000 ? 5_000 : 0);
131
+ this.#ready = new Promise<void>((resolve) => {
132
+ this.#wakeBackoff = resolve;
133
+ this.#backoff = setTimeout(resolve, delay);
134
+ }).then(() => {
135
+ this.#backoff = undefined; this.#wakeBackoff = undefined;
136
+ return this.#spawn();
137
+ });
138
+ void this.#ready.catch(() => {});
139
+ });
140
+ // Health-gate: poll the child's /health over the socket until it
141
+ // answers — model load happens behind this.
142
+ const deadline = Date.now() + (this.spec.readyTimeoutMs ?? 15 * 60_000);
143
+ while (Date.now() < deadline) {
144
+ if (this.#stopping || proc.killed || proc.exitCode !== null || this.#proc !== proc)
145
+ throw new Error("engine died during startup");
146
+ try {
147
+ const r = await fetch("http://engine/health", {
148
+ unix: this.spec.socketPath,
149
+ signal: AbortSignal.timeout(2_000),
150
+ } as RequestInit & { unix: string });
151
+ if (r.ok) { healthy = true; return; }
152
+ } catch { /* not up yet */ }
153
+ await new Promise((r) => setTimeout(r, 250));
154
+ }
155
+ proc.kill();
156
+ throw new Error("engine did not become healthy in time");
157
+ } finally {
158
+ if (!healthy && starting && starting.exitCode === null) {
159
+ starting.kill();
160
+ const force = setTimeout(() => { if (starting!.exitCode === null) starting!.kill("SIGKILL"); }, 3000);
161
+ try { await starting.exited; } finally { clearTimeout(force); }
162
+ }
163
+ lease?.dispose();
104
164
  }
105
- throw new Error("engine did not become healthy in time");
106
165
  }
107
166
 
108
167
  /** Forward one request to the child; the response streams through.
@@ -124,33 +183,81 @@ export class EngineChild {
124
183
  }
125
184
 
126
185
  async #forwardOnce(request: Request): Promise<Response> {
127
- await this.#ready;
128
- const headers = new Headers();
129
- request.headers.forEach((v, k) => {
130
- if (!HOP_BY_HOP.has(k.toLowerCase())) headers.set(k, v);
186
+ if (this.#stopping) throw new Error("engine host is closed");
187
+ request.signal.throwIfAborted();
188
+ await new Promise<void>((resolve, reject) => {
189
+ const abort = () => { request.signal.removeEventListener("abort", abort); reject(request.signal.reason); };
190
+ request.signal.addEventListener("abort", abort, { once: true });
191
+ this.#ready.then(() => { request.signal.removeEventListener("abort", abort); resolve(); },
192
+ (error) => { request.signal.removeEventListener("abort", abort); reject(error); });
131
193
  });
132
- const proxied = await fetch(request.url, {
133
- method: request.method,
134
- headers,
135
- body: request.body,
136
- signal: request.signal, // client abort → child sees the disconnect
137
- redirect: "manual",
138
- unix: this.spec.socketPath,
139
- // Streaming request bodies need half-duplex; buffered bodies ignore it.
140
- duplex: "half",
141
- } as RequestInit & { unix: string; duplex: string });
142
- const outHeaders = new Headers();
143
- proxied.headers.forEach((v, k) => {
144
- if (!HOP_BY_HOP.has(k.toLowerCase())) outHeaders.set(k, v);
145
- });
146
- return new Response(proxied.body, { status: proxied.status, headers: outHeaders });
194
+ if (this.#stopping) throw new Error("engine host is closed");
195
+ request.signal.throwIfAborted();
196
+ const signal = AbortSignal.any([request.signal, this.#lifetime.signal]);
197
+ const lease = await this.spec.acquire?.(signal);
198
+ try {
199
+ signal.throwIfAborted();
200
+ const headers = new Headers();
201
+ request.headers.forEach((v, k) => {
202
+ if (!HOP_BY_HOP.has(k.toLowerCase())) headers.set(k, v);
203
+ });
204
+ const proxied = await fetch(request.url, {
205
+ method: request.method,
206
+ headers,
207
+ body: request.body,
208
+ signal, // client abort → child sees the disconnect
209
+ redirect: "manual",
210
+ unix: this.spec.socketPath,
211
+ // Streaming request bodies need half-duplex; buffered bodies ignore it.
212
+ duplex: "half",
213
+ } as RequestInit & { unix: string; duplex: string });
214
+ const outHeaders = new Headers();
215
+ proxied.headers.forEach((v, k) => {
216
+ if (!HOP_BY_HOP.has(k.toLowerCase())) outHeaders.set(k, v);
217
+ });
218
+ const response = new Response(proxied.body, { status: proxied.status, headers: outHeaders });
219
+ return lease ? retainResponseLease(response, lease) : response;
220
+ } catch (error) { lease?.dispose(); throw error; }
221
+ }
222
+
223
+ /** A live UDS response owns the worker's native lease. Disconnect releases it
224
+ * even if the parent crashes. External cancellation applies to acquisition;
225
+ * the returned owner alone releases an admitted lease after job death. */
226
+ async reserveExecution(signal: AbortSignal): Promise<DisposableResource> {
227
+ if (!this.#proc || this.#proc.exitCode !== null) return { dispose() {} };
228
+ const abort = new AbortController();
229
+ const cancel = () => abort.abort(signal.reason);
230
+ signal.throwIfAborted();
231
+ signal.addEventListener("abort", cancel, { once: true });
232
+ try {
233
+ const response = await fetch("http://engine/admin/lease", {
234
+ method: "POST", unix: this.spec.socketPath, signal: abort.signal,
235
+ } as RequestInit & { unix: string });
236
+ if (!response.ok) { await response.body?.cancel(); throw new Error(`worker lease failed (${response.status})`); }
237
+ return { dispose() { abort.abort(); void response.body?.cancel().catch(() => {}); } };
238
+ } catch (error) { abort.abort(); throw error; }
239
+ finally { signal.removeEventListener("abort", cancel); }
147
240
  }
148
241
 
149
242
  stop(): void {
150
243
  this.#stopping = true;
244
+ this.#lifetime.abort(new Error("engine host is closed"));
245
+ clearTimeout(this.#backoff);
246
+ this.#wakeBackoff?.();
151
247
  this.#proc?.kill();
152
248
  try { unlinkSync(this.spec.socketPath); } catch {}
153
249
  }
250
+
251
+ async close(): Promise<void> {
252
+ this.stop();
253
+ const process = this.#proc;
254
+ const force = process ? setTimeout(() => { if (process.exitCode === null) process.kill("SIGKILL"); }, 3000) : undefined;
255
+ try {
256
+ // Startup owns an admission lease even before a child exists. Earlier
257
+ // crashed attempts can still be unwinding after #ready switches to a retry.
258
+ await Promise.allSettled([this.#ready, ...this.#startups, ...(process ? [process.exited] : [])]);
259
+ } finally { if (force) clearTimeout(force); }
260
+ }
154
261
  }
155
262
 
156
263
  /** Model resolution the parent injects (Registry-backed in production;
@@ -158,7 +265,7 @@ export class EngineChild {
158
265
  * drop-in ignore semantics (served by the default model, like mlx-lm). */
159
266
  export type ModelResolver = (query: string) => { repoId: string; path: string } | null;
160
267
 
161
- /** Child-per-model pool (P2, runtime-isolation.md): LRU residency over
268
+ /** Child-per-model pool (P2, docs/reference/server-config.md): LRU residency over
162
269
  * engine children. Eviction is graceful and lossless — POST /admin/drain
163
270
  * (gateway quiesce + demote the prompt cache to the SSD tier, keyed by
164
271
  * that model's own fingerprint dir) and only then stop the child; the
@@ -170,6 +277,9 @@ export class ModelPool {
170
277
  #children = new Map<string, EngineChild>(); // repoId → child
171
278
  #lru: string[] = []; // most-recent last
172
279
  #spawning = new Map<string, Promise<EngineChild>>();
280
+ #coldStarts = new AdmissionPool(1);
281
+ #starting = new Set<EngineChild>();
282
+ #stopping = false;
173
283
  readonly defaultKey: string;
174
284
 
175
285
  constructor(
@@ -210,40 +320,55 @@ export class ModelPool {
210
320
  /** Route a request's `model` field to a child, spawning on first use.
211
321
  * undefined/empty/unknown → the default child (drop-in semantics). */
212
322
  async childFor(modelField: string | undefined | null): Promise<EngineChild> {
213
- if (!modelField) return this.#use(this.defaultKey);
323
+ if (this.#stopping) throw new Error("model pool is closed");
324
+ modelField ||= this.defaultKey;
214
325
  // Already resident under this exact id?
215
326
  if (this.#children.has(modelField)) return this.#use(modelField);
216
327
  const rec = (() => {
217
328
  try { return this.opts.resolve(modelField); } catch { return null; }
218
329
  })();
219
- if (!rec) return this.#use(this.defaultKey); // unknown → ignore, like mlx-lm
220
- if (this.#children.has(rec.repoId)) return this.#use(rec.repoId);
221
- const inflight = this.#spawning.get(rec.repoId);
222
- if (inflight) return inflight.then(() => this.#use(rec.repoId));
330
+ if (!rec && modelField !== this.defaultKey) return this.childFor(this.defaultKey);
331
+ const resolved = rec ?? { repoId: this.defaultKey, path: "" };
332
+ if (this.#children.has(resolved.repoId)) return this.#use(resolved.repoId);
333
+ const inflight = this.#spawning.get(resolved.repoId);
334
+ if (inflight) return inflight.then(() => this.#use(resolved.repoId));
223
335
  const spawnP = (async () => {
224
- const sock = this.#socketFor(rec.repoId);
225
- const child = new EngineChild({
226
- argv: [...this.opts.selfArgv, ...engineArgvForModel(this.opts.rawArgs, sock, rec.path)],
336
+ const admission = await this.#coldStarts.acquire();
337
+ try {
338
+ if (this.#stopping) throw new Error("model pool is closed");
339
+ const sock = this.#socketFor(resolved.repoId);
340
+ const child = new EngineChild(resolved.repoId === this.defaultKey ? this.opts.defaultChild.spec : {
341
+ argv: [...this.opts.selfArgv, ...engineArgvForModel(this.opts.rawArgs, sock, resolved.path)],
227
342
  socketPath: sock,
343
+ acquire: this.opts.defaultChild.spec.acquire,
228
344
  });
229
- // SPAWN-OVERLAP: the old model keeps serving while this loads.
230
- await child.ready;
231
- this.#children.set(rec.repoId, child);
232
- this.#lru.push(rec.repoId);
233
- await this.#evictOverCap();
234
- return child;
345
+ this.#starting.add(child);
346
+ try {
347
+ await child.ready;
348
+ if (this.#stopping) throw new Error("model pool is closed");
349
+ this.#children.set(resolved.repoId, child);
350
+ this.#lru.push(resolved.repoId);
351
+ await this.#evictOverCap();
352
+ return child;
353
+ } catch (error) {
354
+ await child.close();
355
+ throw error;
356
+ } finally { this.#starting.delete(child); }
357
+ } finally { admission.dispose(); }
235
358
  })();
236
- this.#spawning.set(rec.repoId, spawnP);
359
+ this.#spawning.set(resolved.repoId, spawnP);
237
360
  try {
238
361
  return await spawnP;
239
362
  } finally {
240
- this.#spawning.delete(rec.repoId);
363
+ this.#spawning.delete(resolved.repoId);
241
364
  }
242
365
  }
243
366
 
244
367
  #use(key: string): EngineChild {
245
368
  this.#bump(key);
246
- return this.#children.get(key)!;
369
+ const child = this.#children.get(key);
370
+ if (!child) throw new Error(`model ${key} is no longer resident`);
371
+ return child;
247
372
  }
248
373
 
249
374
  /** Evict least-recently-used children over the cap: drain + demote (the
@@ -253,23 +378,38 @@ export class ModelPool {
253
378
  const victim = this.#lru.find((k) => this.#children.has(k));
254
379
  if (!victim || this.#children.size <= 1) return;
255
380
  const child = this.#children.get(victim)!;
256
- this.#children.delete(victim);
257
- this.#lru = this.#lru.filter((k) => k !== victim);
381
+ const lease = await child.spec.acquire?.(AbortSignal.timeout(120_000));
258
382
  try {
259
- await fetch("http://engine/admin/drain", {
260
- method: "POST",
261
- unix: child.spec.socketPath,
262
- signal: AbortSignal.timeout(120_000),
263
- } as RequestInit & { unix: string });
264
- } catch { /* best-effort — state also persists via evict-spill */ }
265
- child.stop();
266
- console.log(`[isolate] evicted engine ${victim} (pool cap ${this.opts.poolMax})`);
383
+ // Another overlapping eviction may have completed while we waited.
384
+ if (this.#children.get(victim) !== child) continue;
385
+ this.#children.delete(victim);
386
+ this.#lru = this.#lru.filter((k) => k !== victim);
387
+ try {
388
+ const response = await fetch("http://engine/admin/drain", {
389
+ method: "POST", unix: child.spec.socketPath,
390
+ signal: AbortSignal.timeout(120_000),
391
+ } as RequestInit & { unix: string });
392
+ await response.arrayBuffer();
393
+ } catch { /* best-effort — state also persists via evict-spill */ }
394
+ await child.close();
395
+ console.log(`[isolate] evicted engine ${victim} (pool cap ${this.opts.poolMax})`);
396
+ } finally { lease?.dispose(); }
267
397
  }
268
398
  }
269
399
 
270
400
  stopAll(): void {
271
- for (const c of this.#children.values()) c.stop();
401
+ this.#stopping = true;
402
+ this.#coldStarts.close();
403
+ for (const child of new Set([...this.#children.values(), ...this.#starting])) child.stop();
272
404
  this.#children.clear();
405
+ this.#lru = [];
406
+ }
407
+
408
+ async close(): Promise<void> {
409
+ const children = new Set([...this.#children.values(), ...this.#starting]);
410
+ this.stopAll();
411
+ await Promise.all([...children].map((child) => child.close()));
412
+ await Promise.allSettled(this.#spawning.values());
273
413
  }
274
414
  }
275
415
 
@@ -280,6 +420,8 @@ const MODEL_ROUTED = new Set([
280
420
  ]);
281
421
 
282
422
  export interface ProxyServerOptions {
423
+ /** Application-store injection for embedded hosts and model-free tests. */
424
+ createJobStore?: () => Promise<import("../jobs/db").JobStore>;
283
425
  port: number;
284
426
  hostname?: string;
285
427
  engine: EngineSpec;
@@ -301,11 +443,61 @@ export function startProxyServer(opts: ProxyServerOptions): {
301
443
  server: ReturnType<typeof Bun.serve>;
302
444
  engine: EngineChild;
303
445
  pool: ModelPool | null;
446
+ close(): Promise<void>;
304
447
  } {
305
- const engine = new EngineChild(opts.engine);
448
+ const coordinator = new ExecutionCoordinator();
449
+ const acquire = (mode: "shared" | "exclusive", signal: AbortSignal) =>
450
+ acquireReservation({ acquire: (cancellation) => coordinator.acquire(mode, cancellation) }, signal);
451
+ const engine = new EngineChild({ ...opts.engine, async acquire(signal) {
452
+ const parent = await acquire("shared", signal);
453
+ try {
454
+ const supplied = await opts.engine.acquire?.(signal);
455
+ return { dispose() { try { supplied?.dispose(); } finally { parent.dispose(); } } };
456
+ } catch (error) { parent.dispose(); throw error; }
457
+ } });
306
458
  const pool = opts.pool
307
459
  ? new ModelPool({ ...opts.pool, defaultChild: engine })
308
460
  : null;
461
+ const children = () => pool ? pool.residentKeys.map((key) => pool.child(key)!).filter(Boolean) : [engine];
462
+ const forwardRequest = async (request: Request): Promise<Response> => {
463
+ const url = new URL(request.url);
464
+ // P2 routing: generation endpoints carry `model` in the JSON body —
465
+ // buffer it (small), pick/spawn that model's child, forward the
466
+ // buffered body. Everything else rides the default engine.
467
+ if (pool && request.method === "POST" && MODEL_ROUTED.has(url.pathname)) {
468
+ const raw = await request.text();
469
+ let modelField: string | undefined;
470
+ try { modelField = (JSON.parse(raw) as { model?: string }).model; } catch { /* malformed → default child answers with its own 400 */ }
471
+ const target = await pool.childFor(modelField);
472
+ return await target.forward(new Request(request.url, {
473
+ method: request.method,
474
+ headers: request.headers,
475
+ body: raw,
476
+ signal: request.signal,
477
+ }));
478
+ }
479
+ return await (pool ? await pool.childFor(undefined) : engine).forward(request);
480
+ };
481
+ const application = createParentApplication({
482
+ createJobStore: opts.createJobStore,
483
+ serverPort: () => server.port,
484
+ async acquireGpu(signal) {
485
+ const lease = await acquire("exclusive", signal);
486
+ try {
487
+ // Network cancellation is not a native completion fence. Explicitly
488
+ // drain every resident worker before the subprocess may touch the GPU.
489
+ const results = await Promise.allSettled(children().map((child) => child.reserveExecution(signal)));
490
+ const held = results.filter((result) => result.status === "fulfilled").map((result) => result.value);
491
+ const failed = results.find((result) => result.status === "rejected");
492
+ if (failed) { for (const worker of held) worker.dispose(); throw failed.reason; }
493
+ return { dispose() { for (const worker of held) worker.dispose(); lease.dispose(); } };
494
+ } catch (error) { lease.dispose(); throw error; }
495
+ },
496
+ invalidateLibrary() {
497
+ for (const child of children()) void child.forward(new Request("http://engine/library?refresh=1"))
498
+ .then((response) => response.arrayBuffer()).catch(() => {});
499
+ },
500
+ });
309
501
  const server = Bun.serve({
310
502
  port: opts.port,
311
503
  ...(opts.hostname ? { hostname: opts.hostname } : {}),
@@ -317,32 +509,26 @@ export function startProxyServer(opts: ProxyServerOptions): {
317
509
  { error: { message: "WebSocket chat is not proxied under --isolate yet; run without --isolate for the web chat UI" } },
318
510
  { status: 501 },
319
511
  );
320
- if (url.pathname === "/engine" && request.method === "GET")
512
+ if (url.pathname === "/engine" && request.method === "GET") {
513
+ // A pool may have evicted and recreated the default worker. Inspect
514
+ // current residency without starting a worker merely for diagnostics.
515
+ const current = pool ? pool.child(pool.defaultKey) : engine;
321
516
  return Response.json({
322
517
  isolated: true,
323
- pid: engine.pid,
324
- restarts: engine.restarts,
325
- socket: engine.spec.socketPath,
518
+ response_store: application.responseStats,
519
+ pid: current?.pid ?? null,
520
+ restarts: current?.restarts ?? null,
521
+ socket: current?.spec.socketPath ?? null,
326
522
  ...(pool ? { pool: { resident: pool.residentKeys, default: pool.defaultKey } } : {}),
327
523
  });
524
+ }
328
525
  try {
329
- // P2 routing: generation endpoints carry `model` in the JSON body —
330
- // buffer it (small), pick/spawn that model's child, forward the
331
- // buffered body. Everything else rides the default engine.
332
- if (pool && request.method === "POST" && MODEL_ROUTED.has(url.pathname)) {
333
- const raw = await request.text();
334
- let modelField: string | undefined;
335
- try { modelField = (JSON.parse(raw) as { model?: string }).model; } catch { /* malformed → default child answers with its own 400 */ }
336
- const target = await pool.childFor(modelField);
337
- return await target.forward(new Request(request.url, {
338
- method: request.method,
339
- headers: request.headers,
340
- body: raw,
341
- signal: request.signal,
342
- }));
343
- }
344
- return await engine.forward(request);
526
+ const local = await application.handle(request, forwardRequest);
527
+ if (local) return local;
528
+ return await forwardRequest(request);
345
529
  } catch (e) {
530
+ if (e instanceof AdmissionRejected) return Response.json(
531
+ { error: { message: e.message, type: "resource_admission", code: "queue_full" } }, { status: 429 });
346
532
  if (request.signal.aborted) return new Response(null, { status: 499 });
347
533
  return Response.json(
348
534
  {
@@ -356,7 +542,13 @@ export function startProxyServer(opts: ProxyServerOptions): {
356
542
  }
357
543
  },
358
544
  });
359
- return { server, engine, pool };
545
+ let closing: Promise<void> | undefined;
546
+ return { server, engine, pool, close: () => closing ??= (async () => {
547
+ await server.stop(true);
548
+ coordinator.close();
549
+ await application.close();
550
+ if (pool) await pool.close(); else await engine.close();
551
+ })() };
360
552
  }
361
553
 
362
554
  /** serve's value-taking flags — needed to tell a flag VALUE from the bare
@@ -368,7 +560,7 @@ const SERVE_VALUE_FLAGS = new Set([
368
560
  "--batch", "--decode-concurrency", "--kv-quant",
369
561
  "--kv-group-size", "--quantized-kv-start", "--kv-budget",
370
562
  "--memory-budget", "--prompt-cache", "--ssd-cache", "--ssd-cache-max",
371
- "--ssd-demote-idle", "--adapter", "--max-tokens", "--unix",
563
+ "--ssd-demote-idle", "--generation-checkpoint", "--adapter", "--max-tokens", "--unix",
372
564
  "--model-pool", "--compiled-decode", "--compiled-activations",
373
565
  "--fused-sdpa", "--thinking",
374
566
  ]);
@@ -2,6 +2,7 @@ import type { JobStore } from "../jobs";
2
2
 
3
3
  export interface LabRouteDeps {
4
4
  ensureJobs(): Promise<JobStore>;
5
+ acquireGpu?: (signal: AbortSignal) => Promise<import("../contracts/resources").DisposableResource>;
5
6
  serverPort(): number | undefined;
6
7
  invalidateLibrary(): void;
7
8
  }
@@ -103,6 +104,7 @@ export async function handleLabRoute(
103
104
  "finetune",
104
105
  { ...body, adapter_path: adapterPath },
105
106
  adapterPath,
107
+ { acquire: deps.acquireGpu },
106
108
  );
107
109
  return Response.json({ ok: true, job_id: jobId, adapter_path: adapterPath });
108
110
  }
@@ -332,6 +334,7 @@ async function submitQuantize(request: Request, deps: LabRouteDeps): Promise<Res
332
334
  rotate_weights: body.rotate_weights,
333
335
  rotation_seed: body.rotation_seed,
334
336
  }, outDir, {
337
+ acquire: deps.acquireGpu,
335
338
  onComplete: () => deps.invalidateLibrary(),
336
339
  });
337
340
  return Response.json({ ok: true, job_id: jobId, output_dir: outDir });
@@ -16,7 +16,8 @@
16
16
  // a same-process debugging/telemetry aid, not a durable store — entries for
17
17
  // long-dead requests must not leak memory over a long-running server.
18
18
 
19
- export type Lane = "serial" | "serial+spec" | "batched";
19
+ import type { Lane } from "../contracts/pi";
20
+ export type { Lane } from "../contracts/pi";
20
21
 
21
22
  const MAX_ENTRIES = 512;
22
23
  const lanes = new Map<string, Lane>();
@@ -1,6 +1,6 @@
1
- import type { ServerContext } from "../server";
1
+ import { modelServingBinding } from "../backends/mlx/model-serving";
2
+ import type { ServingContext } from "./model-host";
2
3
  import type { GenerationGateway } from "./generation-gateway";
3
- import { embedMany, isEmbeddingModel } from "../embed";
4
4
  import { listAvailableAdapters } from "../lora";
5
5
 
6
6
  export type ModelAdminRoute =
@@ -29,7 +29,7 @@ export function matchModelAdminRoute(method: string, pathname: string): ModelAdm
29
29
  export async function handleModelAdminRoute(
30
30
  url: URL,
31
31
  request: Request,
32
- ctx: ServerContext,
32
+ ctx: ServingContext,
33
33
  gateway: Pick<GenerationGateway, "runExclusive">,
34
34
  ): Promise<Response | null> {
35
35
  const route = matchModelAdminRoute(request.method, url.pathname);
@@ -37,7 +37,9 @@ export async function handleModelAdminRoute(
37
37
 
38
38
  switch (route.kind) {
39
39
  case "embeddings": {
40
- if (!isEmbeddingModel(ctx.model)) {
40
+ const binding = modelServingBinding(ctx);
41
+ const embed = binding.embed?.bind(binding);
42
+ if (!embed) {
41
43
  return Response.json({
42
44
  error: {
43
45
  message: `served model "${ctx.modelId}" is not an embedding model; ` +
@@ -66,7 +68,7 @@ export async function handleModelAdminRoute(
66
68
  }, { status: 400 });
67
69
  }
68
70
  const instruction = typeof body.instruction === "string" ? body.instruction : undefined;
69
- const results = embedMany(ctx.model, ctx.tokenizer, inputs, instruction);
71
+ const results = await gateway.runExclusive(async () => embed(inputs, instruction), undefined, request.signal);
70
72
  let totalTokens = 0;
71
73
  const data = results.map((result, index) => {
72
74
  totalTokens += result.tokens;