mlx-bun 0.4.0 → 1.0.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 (601) hide show
  1. package/README.md +1453 -88
  2. package/THIRD_PARTY_NOTICES.md +882 -0
  3. package/bin/mlx-bun.convert.mjs +4 -0
  4. package/bin/mlx-bun.fuse.mjs +4 -0
  5. package/bin/mlx-bun.generate.mjs +4 -0
  6. package/bin/mlx-bun.lora.mjs +4 -0
  7. package/bin/mlx-bun.mjs +13 -21
  8. package/bin/mlx-bun.server.mjs +4 -0
  9. package/bin/mlx-bun.upload.mjs +4 -0
  10. package/dist/web/app.js +9584 -0
  11. package/package.json +50 -53
  12. package/src/cli/ane-install.ts +76 -0
  13. package/src/cli/args.ts +179 -0
  14. package/src/cli/engine-entry.ts +68 -0
  15. package/src/cli/inference.ts +214 -0
  16. package/src/cli/job-entry.ts +97 -0
  17. package/src/cli/library-host.ts +117 -0
  18. package/src/cli/main.ts +57 -0
  19. package/src/cli/memory-engine.ts +163 -0
  20. package/src/cli/memory.ts +36 -0
  21. package/src/cli/mlx-lm-aliases.ts +309 -0
  22. package/src/cli/model-choice.ts +33 -0
  23. package/src/cli/model-selection.ts +84 -0
  24. package/src/cli/module-host.ts +11 -0
  25. package/src/cli/module-verbs.ts +50 -0
  26. package/src/cli/numerical-policy.ts +12 -0
  27. package/src/cli/perplexity.ts +89 -0
  28. package/src/cli/serve-host.ts +423 -0
  29. package/src/cli/serve-isolated.ts +217 -0
  30. package/src/cli/serve-options.ts +106 -0
  31. package/src/cli/serve-state.ts +205 -0
  32. package/src/cli/serve.ts +500 -0
  33. package/src/cli/served-model-host.ts +102 -0
  34. package/src/cli/server-entry.ts +148 -0
  35. package/src/cli/serving-unit.ts +177 -0
  36. package/src/{tui.ts → cli/terminal.ts} +13 -8
  37. package/src/cli/worker-adapters.ts +34 -0
  38. package/src/cli/worker-entry.ts +272 -0
  39. package/src/cli/worker-unit.ts +179 -0
  40. package/src/engine/adapter-operation.ts +26 -0
  41. package/src/engine/cache-services.ts +114 -0
  42. package/src/engine/completion.ts +39 -0
  43. package/src/engine/generation-gateway.ts +358 -0
  44. package/src/engine/index.ts +59 -0
  45. package/src/engine/kv-budget.ts +120 -0
  46. package/src/engine/media-preparation.ts +232 -0
  47. package/src/engine/model-binding.ts +24 -0
  48. package/src/engine/model-host.ts +513 -0
  49. package/src/engine/model-serving.ts +89 -0
  50. package/src/{serve → engine}/preparation.ts +10 -8
  51. package/src/{serve → engine}/session-completion-engine.ts +6 -6
  52. package/src/engine/telemetry.ts +109 -0
  53. package/src/hub/downloads.ts +116 -0
  54. package/src/jobs/db.ts +16 -7
  55. package/src/jobs/events.ts +29 -0
  56. package/src/jobs/executable.ts +5 -0
  57. package/src/jobs/host.ts +51 -0
  58. package/src/jobs/in-process.ts +55 -0
  59. package/src/{contracts/jobs.ts → jobs/protocol.ts} +7 -9
  60. package/src/jobs/runner.ts +150 -231
  61. package/src/jobs/service.ts +75 -0
  62. package/src/jobs/sse.ts +59 -23
  63. package/src/jobs/worker-memory.ts +46 -0
  64. package/src/jobs/worker-process.ts +163 -0
  65. package/src/jobs/worker-supervisor.ts +258 -0
  66. package/src/modules.ts +58 -0
  67. package/src/publishing/catalog-hub.ts +18 -0
  68. package/src/publishing/credentials.ts +31 -0
  69. package/src/publishing/upload.ts +37 -0
  70. package/src/residency/leased-adapters.ts +21 -0
  71. package/src/residency/local-records.ts +16 -0
  72. package/src/residency/model-residency.ts +405 -0
  73. package/src/residency/record-index.ts +28 -0
  74. package/src/residency/resident-estimate.ts +50 -0
  75. package/src/{anthropic.ts → server/anthropic.ts} +3 -81
  76. package/src/server/cache-routes.ts +36 -0
  77. package/src/{serve → server}/chat-request.ts +12 -10
  78. package/src/{serve → server}/chat-stage.ts +27 -22
  79. package/src/{client.ts → server/client.ts} +28 -2
  80. package/src/{serve → server}/completion-executor.ts +42 -42
  81. package/src/server/decision-body.ts +55 -0
  82. package/src/server/decision-routes.ts +134 -0
  83. package/src/server/decision-wire.ts +228 -0
  84. package/src/{serve → server}/discovery-routes.ts +42 -83
  85. package/src/server/generated-token-history.ts +57 -0
  86. package/src/{serve → server}/http.ts +25 -13
  87. package/src/{serve → server}/inference-request.ts +5 -5
  88. package/src/server/job-routes.ts +20 -0
  89. package/src/server/media-prompt.ts +63 -0
  90. package/src/server/memory-completion-client.ts +46 -0
  91. package/src/server/model-routes.ts +192 -0
  92. package/src/{serve → server}/openai-wire.ts +3 -2
  93. package/src/{serve → server}/pipeline.ts +1 -1
  94. package/src/server/prompt-builder.ts +13 -0
  95. package/src/server/prompt-contracts.ts +17 -0
  96. package/src/server/proxy-routes.ts +187 -0
  97. package/src/server/publishing-routes.ts +41 -0
  98. package/src/{serve → server}/request-plan.ts +8 -8
  99. package/src/{serve → server}/request-prep.ts +82 -68
  100. package/src/{serve → server}/responses-client.ts +30 -13
  101. package/src/{responses.ts → server/responses.ts} +12 -93
  102. package/src/server/routes.ts +205 -0
  103. package/src/server/start.ts +72 -0
  104. package/src/server/status-routes.ts +108 -0
  105. package/src/{serve → server}/text-completion-stage.ts +25 -4
  106. package/src/server/text-prompt.ts +12 -0
  107. package/src/{serve → server}/token-streams.ts +36 -38
  108. package/src/server/worker-routes.ts +311 -0
  109. package/src/storage/paths.ts +61 -0
  110. package/src/web/assets.ts +37 -0
  111. package/src/web/{src → browser}/api.ts +5 -23
  112. package/src/web/browser/chat-host.ts +17 -0
  113. package/src/web/browser/installed-panels.ts +6 -0
  114. package/src/web/browser/main.ts +55 -0
  115. package/src/web/browser/markdown.ts +279 -0
  116. package/src/web/browser/memory-panel.ts +9 -0
  117. package/src/web/browser/palette.ts +64 -0
  118. package/src/web/browser/protocol.ts +13 -0
  119. package/src/web/browser/shell.ts +467 -0
  120. package/src/web/{src → browser}/status.ts +32 -36
  121. package/src/web/build.ts +51 -0
  122. package/src/web/public/app.html +1054 -0
  123. package/src/web/{sw.js → public/sw.js} +4 -3
  124. package/src/web/{vendor → public/vendor}/hljs-theme.css +2 -2
  125. package/src/web/{vendor → public/vendor}/hljs.js +2 -2
  126. package/THIRD_PARTY_LICENSES.md +0 -51
  127. package/docs/README.md +0 -94
  128. package/docs/archive/hlg-sampling.md +0 -350
  129. package/docs/archive/investigations/audio-input-plan.md +0 -356
  130. package/docs/archive/investigations/chunk-finetune-experiment.md +0 -84
  131. package/docs/archive/investigations/colibri-glm52-port.md +0 -1486
  132. package/docs/archive/investigations/colibri-oracle-pin.md +0 -1284
  133. package/docs/archive/investigations/curve-bisector-routing.md +0 -123
  134. package/docs/archive/investigations/decode-roofline-lookagain.md +0 -348
  135. package/docs/archive/investigations/diffusion-gemma-port.md +0 -602
  136. package/docs/archive/investigations/dspark-handoff.md +0 -224
  137. package/docs/archive/investigations/expert-offload-single-user-moe.md +0 -292
  138. package/docs/archive/investigations/faithful-l1-consolidation.md +0 -310
  139. package/docs/archive/investigations/hlg-sampling-investigation.md +0 -970
  140. package/docs/archive/investigations/indexshare-performance-spike.md +0 -357
  141. package/docs/archive/investigations/kernel-perf-review-2026-07.md +0 -171
  142. package/docs/archive/investigations/lab-build-journal.md +0 -157
  143. package/docs/archive/investigations/minicpm5-decode-megakernel.md +0 -489
  144. package/docs/archive/investigations/openjarvis-fit.md +0 -198
  145. package/docs/archive/investigations/openwiki-evaluation.md +0 -249
  146. package/docs/archive/investigations/optimization_plan.md +0 -346
  147. package/docs/archive/investigations/orpo-base-uf-experiment-and-directions.md +0 -120
  148. package/docs/archive/investigations/orpo-flash-cce-pin-leak.md +0 -167
  149. package/docs/archive/investigations/orpo-uf-testing-handoff.md +0 -126
  150. package/docs/archive/investigations/pareto-specialized-runtime-findings.md +0 -92
  151. package/docs/archive/investigations/pi-builtin-investigation.md +0 -308
  152. package/docs/archive/investigations/pre-colibri-stabilization.md +0 -329
  153. package/docs/archive/investigations/qwen38-27b-campaign-log.md +0 -1092
  154. package/docs/archive/investigations/segmented-backward-handoff.md +0 -148
  155. package/docs/archive/investigations/starter-model-port-handoff.md +0 -342
  156. package/docs/archive/investigations/steel-flash-cce-handoff.md +0 -489
  157. package/docs/archive/investigations/the-dreaming-handoff.md +0 -141
  158. package/docs/archive/investigations/trainer-validation-experiment.md +0 -173
  159. package/docs/archive/planning/chat-ui-vision-memory-first.md +0 -199
  160. package/docs/archive/planning/chat-ui-vision.md +0 -50
  161. package/docs/archive/planning/chunk-task-roadmap.md +0 -77
  162. package/docs/archive/planning/curve-sampler-research-plan.md +0 -331
  163. package/docs/archive/planning/journal.md +0 -316
  164. package/docs/archive/planning/memory-docs-and-dag-plan.md +0 -194
  165. package/docs/archive/planning/pi-integration-review.md +0 -178
  166. package/docs/archive/planning/release-notes-v0.0.10.md +0 -57
  167. package/docs/archive/planning/release-notes-v0.0.11.md +0 -239
  168. package/docs/archive/planning/release-notes-v0.0.12.md +0 -80
  169. package/docs/archive/planning/release-notes-v0.0.13.md +0 -77
  170. package/docs/archive/planning/release-notes-v0.0.9.md +0 -122
  171. package/docs/archive/planning/release-notes-v0.2.0.md +0 -49
  172. package/docs/archive/planning/release-notes-v0.3.0.md +0 -35
  173. package/docs/archive/planning/web-ui-pass-plan.md +0 -213
  174. package/docs/archive/planning/website-readme-pass-plan.md +0 -224
  175. package/docs/design/batching.md +0 -1841
  176. package/docs/design/decode-speed-program.md +0 -2349
  177. package/docs/design/dreaming-nightly-pipeline.md +0 -684
  178. package/docs/design/generic-model-support.md +0 -705
  179. package/docs/design/kv-cache.md +0 -824
  180. package/docs/design/orpo-training.md +0 -680
  181. package/docs/design/speculative-decoding.md +0 -1990
  182. package/docs/design/turboquant.md +0 -2212
  183. package/docs/design/unified-engine-frontier-plan.md +0 -1880
  184. package/docs/design/web-chat-redesign.md +0 -695
  185. package/docs/planning/PRODUCT_ROADMAP.md +0 -594
  186. package/docs/planning/ResearchTopics.md +0 -119
  187. package/docs/planning/release-notes-v0.4.0.md +0 -45
  188. package/docs/planning/video-series.md +0 -421
  189. package/docs/reference/benchmarks.md +0 -3613
  190. package/docs/reference/cli.md +0 -585
  191. package/docs/reference/distribution.md +0 -355
  192. package/docs/reference/environment.md +0 -398
  193. package/docs/reference/glossary.md +0 -39
  194. package/docs/reference/library-api.md +0 -674
  195. package/docs/reference/memory.md +0 -260
  196. package/docs/reference/models.md +0 -334
  197. package/docs/reference/server-api.md +0 -1775
  198. package/docs/reference/server-config.md +0 -846
  199. package/docs/reference/training.md +0 -462
  200. package/docs/reference/troubleshooting.md +0 -158
  201. package/src/admission-plan.ts +0 -18
  202. package/src/audio/conformer.ts +0 -647
  203. package/src/audio/decode.ts +0 -133
  204. package/src/audio/features.ts +0 -202
  205. package/src/audio/transcode.ts +0 -67
  206. package/src/backends/mlx/assistant-target.ts +0 -46
  207. package/src/backends/mlx/autoregressive-draft-rows.ts +0 -120
  208. package/src/backends/mlx/autoregressive.ts +0 -108
  209. package/src/backends/mlx/batch-group.ts +0 -1790
  210. package/src/backends/mlx/cache-attachment.ts +0 -67
  211. package/src/backends/mlx/cache-layout.ts +0 -31
  212. package/src/backends/mlx/checkpoint-state.ts +0 -53
  213. package/src/backends/mlx/continuation-persistence.ts +0 -106
  214. package/src/backends/mlx/continuation-request.ts +0 -43
  215. package/src/backends/mlx/continuation.ts +0 -77
  216. package/src/backends/mlx/diffusion.ts +0 -39
  217. package/src/backends/mlx/draft-target.ts +0 -32
  218. package/src/backends/mlx/evaluation.ts +0 -105
  219. package/src/backends/mlx/fill-append.ts +0 -45
  220. package/src/backends/mlx/full-kv-row-append.ts +0 -32
  221. package/src/backends/mlx/full-kv-row-donor.ts +0 -54
  222. package/src/backends/mlx/full-transitioning-kv-rows.ts +0 -65
  223. package/src/backends/mlx/gateway-binding.ts +0 -130
  224. package/src/backends/mlx/graph.ts +0 -38
  225. package/src/backends/mlx/kv-codec.ts +0 -9
  226. package/src/backends/mlx/kv-maintenance.ts +0 -133
  227. package/src/backends/mlx/kv-tensor-rows.ts +0 -212
  228. package/src/backends/mlx/memory-client.ts +0 -189
  229. package/src/backends/mlx/methods.ts +0 -90
  230. package/src/backends/mlx/model-prompt.ts +0 -164
  231. package/src/backends/mlx/model-serving.ts +0 -79
  232. package/src/backends/mlx/prefill-cohort.ts +0 -89
  233. package/src/backends/mlx/prefill-rows.ts +0 -200
  234. package/src/backends/mlx/prefill.ts +0 -33
  235. package/src/backends/mlx/projected-context-rows.ts +0 -72
  236. package/src/backends/mlx/request-policy.ts +0 -20
  237. package/src/backends/mlx/request-state-policy.ts +0 -41
  238. package/src/backends/mlx/rollback.ts +0 -89
  239. package/src/backends/mlx/row-sampling.ts +0 -49
  240. package/src/backends/mlx/serial-executor.ts +0 -242
  241. package/src/backends/mlx/speculative-group.ts +0 -359
  242. package/src/backends/mlx/speculative-round.ts +0 -161
  243. package/src/backends/mlx/speculative.ts +0 -114
  244. package/src/backends/mlx/state-rows.ts +0 -104
  245. package/src/backends/mlx/state-views.ts +0 -46
  246. package/src/backends/mlx/target-layout-capability.ts +0 -35
  247. package/src/backends/mlx/text-engine.ts +0 -73
  248. package/src/backends/mlx/transitioning-kv-rows.ts +0 -165
  249. package/src/chat-template.ts +0 -382
  250. package/src/cli.ts +0 -3168
  251. package/src/config.ts +0 -399
  252. package/src/contracts/completion.ts +0 -16
  253. package/src/contracts/execution.ts +0 -60
  254. package/src/contracts/generation.ts +0 -103
  255. package/src/contracts/host.ts +0 -7
  256. package/src/contracts/pi-provider.ts +0 -30
  257. package/src/contracts/pi.ts +0 -259
  258. package/src/contracts/prefix-cache.ts +0 -23
  259. package/src/contracts/resources.ts +0 -25
  260. package/src/contracts/sampling.ts +0 -23
  261. package/src/contracts/scheduling.ts +0 -45
  262. package/src/contracts/tsconfig.json +0 -14
  263. package/src/dataset/generators.ts +0 -1087
  264. package/src/dataset/index.ts +0 -12
  265. package/src/dataset/job.ts +0 -81
  266. package/src/dataset/llm.ts +0 -71
  267. package/src/dataset/registry.ts +0 -528
  268. package/src/diffusion/diffusion-generate.ts +0 -507
  269. package/src/download.ts +0 -505
  270. package/src/embed.ts +0 -77
  271. package/src/engine/admission.ts +0 -59
  272. package/src/engine/cancellation.ts +0 -38
  273. package/src/engine/engine.ts +0 -56
  274. package/src/engine/execution-coordinator.ts +0 -70
  275. package/src/engine/execution-plan.ts +0 -53
  276. package/src/engine/resources.ts +0 -53
  277. package/src/engine/scheduler.ts +0 -72
  278. package/src/engine/session.ts +0 -247
  279. package/src/eval/README.md +0 -130
  280. package/src/eval/ifeval.ts +0 -81
  281. package/src/eval/kl-prompts.ts +0 -29
  282. package/src/eval/kl.ts +0 -590
  283. package/src/eval/perplexity.ts +0 -164
  284. package/src/eval/quality-db.ts +0 -92
  285. package/src/eval/reference-logits.ts +0 -207
  286. package/src/eval/runner.ts +0 -124
  287. package/src/eval/score.ts +0 -49
  288. package/src/eval/tasks/bfcl.ts +0 -535
  289. package/src/eval/tasks/gsm8k.ts +0 -98
  290. package/src/eval/tasks/hashhop.ts +0 -139
  291. package/src/eval/tasks/humaneval.ts +0 -275
  292. package/src/eval/tasks/ifeval.ts +0 -525
  293. package/src/eval/tasks/mmlu.ts +0 -157
  294. package/src/eval/ultrafeedback.ts +0 -60
  295. package/src/evaldb.ts +0 -199
  296. package/src/expert-io.ts +0 -393
  297. package/src/expert-offload-build.ts +0 -77
  298. package/src/expert-offload.ts +0 -76
  299. package/src/expert-residency.ts +0 -1134
  300. package/src/expert-usage.ts +0 -415
  301. package/src/fill/echo-index.ts +0 -188
  302. package/src/fill/fill-session.ts +0 -452
  303. package/src/fill/proposal.ts +0 -68
  304. package/src/fill/schema-rows.ts +0 -506
  305. package/src/fill/tool-boundary.ts +0 -103
  306. package/src/fit.ts +0 -249
  307. package/src/generate.ts +0 -1346
  308. package/src/grammar.ts +0 -591
  309. package/src/harness-pi.ts +0 -184
  310. package/src/hf-push.ts +0 -628
  311. package/src/hub-rest.ts +0 -242
  312. package/src/index.ts +0 -63
  313. package/src/inference/denoising.ts +0 -17
  314. package/src/inference/draft-acceptance.ts +0 -41
  315. package/src/inference/draft-output.ts +0 -49
  316. package/src/inference/graph.ts +0 -33
  317. package/src/inference/prefill.ts +0 -39
  318. package/src/inference/rollback.ts +0 -7
  319. package/src/jobs/index.ts +0 -31
  320. package/src/jobs/job-entry.ts +0 -107
  321. package/src/jobs/task-client.ts +0 -19
  322. package/src/jobs/types.ts +0 -2
  323. package/src/kv-scheme.ts +0 -257
  324. package/src/kv-store.ts +0 -1099
  325. package/src/lab/README.md +0 -18
  326. package/src/lab/curve/curve-designer.html +0 -303
  327. package/src/lab/curve/curve-sampler.ts +0 -122
  328. package/src/lab/expert-trace/expert-trace.ts +0 -69
  329. package/src/lab/paged-kv/paged-kv-rows.ts +0 -83
  330. package/src/lab/paged-kv/paged-kv.ts +0 -270
  331. package/src/library.ts +0 -40
  332. package/src/lora.ts +0 -408
  333. package/src/md-modules.d.ts +0 -49
  334. package/src/media-fetch.ts +0 -442
  335. package/src/memory/article.ts +0 -558
  336. package/src/memory/chunk-validation.ts +0 -150
  337. package/src/memory/chunk.ts +0 -318
  338. package/src/memory/cluster.ts +0 -351
  339. package/src/memory/coverage.ts +0 -134
  340. package/src/memory/crosslink.ts +0 -0
  341. package/src/memory/db.ts +0 -253
  342. package/src/memory/entity.ts +0 -203
  343. package/src/memory/gate.ts +0 -187
  344. package/src/memory/history.ts +0 -152
  345. package/src/memory/ingest.ts +0 -335
  346. package/src/memory/model.ts +0 -183
  347. package/src/memory/normalize.ts +0 -671
  348. package/src/memory/parse.ts +0 -22
  349. package/src/memory/pipeline.ts +0 -275
  350. package/src/memory/prompts.ts +0 -65
  351. package/src/memory/query.ts +0 -334
  352. package/src/memory/reconcile.ts +0 -734
  353. package/src/memory/redirect.ts +0 -108
  354. package/src/memory/reindex.ts +0 -325
  355. package/src/memory/resolve.ts +0 -307
  356. package/src/memory/rest.ts +0 -245
  357. package/src/memory/route.ts +0 -272
  358. package/src/memory/schedule.ts +0 -165
  359. package/src/memory/stages.ts +0 -777
  360. package/src/memory/synthesize.ts +0 -1638
  361. package/src/memory/tools.ts +0 -749
  362. package/src/memory/vault.ts +0 -1194
  363. package/src/memory/wikify.ts +0 -1064
  364. package/src/mlx/array.ts +0 -388
  365. package/src/mlx/autograd.ts +0 -304
  366. package/src/mlx/checkpoint.ts +0 -149
  367. package/src/mlx/compile.ts +0 -168
  368. package/src/mlx/custom-vjp.ts +0 -188
  369. package/src/mlx/ffi.ts +0 -512
  370. package/src/mlx/materialize.ts +0 -23
  371. package/src/mlx/metal-kernel.ts +0 -159
  372. package/src/mlx/ops.ts +0 -1021
  373. package/src/mlx/quantized-rows.ts +0 -36
  374. package/src/mlx/token-bitmask.ts +0 -52
  375. package/src/mlx/turboquant-kv-decode.ts +0 -82
  376. package/src/mlx/turboquant-kv-inverse.ts +0 -122
  377. package/src/mlx/turboquant-ops.ts +0 -861
  378. package/src/mlx/turboquant-tables.ts +0 -237
  379. package/src/mmap.ts +0 -90
  380. package/src/model/artifact-identity.ts +0 -73
  381. package/src/model/batched-kv.ts +0 -235
  382. package/src/model/batched-mask.ts +0 -279
  383. package/src/model/batched-quant.ts +0 -212
  384. package/src/model/batched-quantized-kv.ts +0 -144
  385. package/src/model/batched-rotating-quant.ts +0 -353
  386. package/src/model/batched-rotating-state.ts +0 -149
  387. package/src/model/batched-rotating.ts +0 -338
  388. package/src/model/batched-row-storage.ts +0 -179
  389. package/src/model/batched-ssm.ts +0 -52
  390. package/src/model/batched-turboquant-kv.ts +0 -93
  391. package/src/model/compiled-decode.ts +0 -712
  392. package/src/model/decoded-kv-donor.ts +0 -12
  393. package/src/model/delayed-quantized-kv.ts +0 -72
  394. package/src/model/delayed-rotating-quantized-kv.ts +0 -93
  395. package/src/model/delayed-turboquant-kv.ts +0 -55
  396. package/src/model/diffusion-gemma.ts +0 -1026
  397. package/src/model/factory.ts +0 -189
  398. package/src/model/fingerprint.ts +0 -33
  399. package/src/model/flash-attention.ts +0 -694
  400. package/src/model/full-prefill-padding.ts +0 -56
  401. package/src/model/full-prefill-row.ts +0 -105
  402. package/src/model/gemma4-base.ts +0 -2351
  403. package/src/model/gemma4.ts +0 -1315
  404. package/src/model/generated/gemma4-12b.ts +0 -429
  405. package/src/model/generated/gemma4-26b.ts +0 -448
  406. package/src/model/generated/gemma4-e4b.ts +0 -717
  407. package/src/model/generated/index.ts +0 -18
  408. package/src/model/glm52-atlas.ts +0 -511
  409. package/src/model/glm52-cache.ts +0 -792
  410. package/src/model/glm52-config.ts +0 -262
  411. package/src/model/glm52-container.ts +0 -305
  412. package/src/model/glm52-coupling.ts +0 -388
  413. package/src/model/glm52-dsa.ts +0 -606
  414. package/src/model/glm52-expert-layout.ts +0 -253
  415. package/src/model/glm52-layout.ts +0 -152
  416. package/src/model/glm52-memory.ts +0 -500
  417. package/src/model/glm52-mla.ts +0 -792
  418. package/src/model/glm52-moe.ts +0 -223
  419. package/src/model/glm52-pilot.ts +0 -537
  420. package/src/model/glm52-reference.ts +0 -445
  421. package/src/model/glm52-residency.ts +0 -557
  422. package/src/model/glm52-resident-weights.ts +0 -135
  423. package/src/model/glm52-streamed-experts.ts +0 -453
  424. package/src/model/glm52-streamed-metal.ts +0 -610
  425. package/src/model/glm52-weights.ts +0 -503
  426. package/src/model/glm52.ts +0 -1132
  427. package/src/model/implementation.ts +0 -69
  428. package/src/model/kv-attention-view.ts +0 -67
  429. package/src/model/minicpm5.ts +0 -376
  430. package/src/model/profile.ts +0 -443
  431. package/src/model/qwen-append.ts +0 -12
  432. package/src/model/qwen-conv.ts +0 -52
  433. package/src/model/qwen3-delta-state.ts +0 -74
  434. package/src/model/qwen3-delta.ts +0 -451
  435. package/src/model/qwen3-moe.ts +0 -404
  436. package/src/model/qwen3-mrope.ts +0 -252
  437. package/src/model/qwen3.ts +0 -277
  438. package/src/model/qwen3_5-checkpoint.ts +0 -164
  439. package/src/model/qwen3_5.ts +0 -819
  440. package/src/model/rotating-kv-donor.ts +0 -35
  441. package/src/model/rotating-kv-layout.ts +0 -262
  442. package/src/model/rotating-row-transaction.ts +0 -35
  443. package/src/model/speculative-rotating-kv.ts +0 -101
  444. package/src/model/ssm-prefill-padding.ts +0 -39
  445. package/src/model/support.ts +0 -102
  446. package/src/model/trellis-balanced-scatter.ts +0 -51
  447. package/src/model/trellis-linear.ts +0 -672
  448. package/src/model/trellis-shared-m.ts +0 -151
  449. package/src/model/trellis-shared-scatter.ts +0 -144
  450. package/src/model/trellis-splitk-prefill.ts +0 -158
  451. package/src/model/trellis-tiled-prefill.ts +0 -136
  452. package/src/model/trellis-vector-expand.ts +0 -62
  453. package/src/model/trellis-wide-prefill.ts +0 -94
  454. package/src/model/turboquant-codec.ts +0 -83
  455. package/src/model/universal/archs.ts +0 -332
  456. package/src/model/universal/dense.ts +0 -612
  457. package/src/model/universal/modules.ts +0 -170
  458. package/src/model/universal/rope.ts +0 -381
  459. package/src/native/expert_io.c +0 -523
  460. package/src/native/frame_extract.swift +0 -140
  461. package/src/native-pack.ts +0 -162
  462. package/src/pi-provider.ts +0 -53
  463. package/src/pi-session.ts +0 -81
  464. package/src/pi-terminal.ts +0 -274
  465. package/src/pi-web.ts +0 -1932
  466. package/src/preflight.ts +0 -158
  467. package/src/prompt-cache.ts +0 -460
  468. package/src/quantize/allocator.ts +0 -627
  469. package/src/quantize/atomic-output.ts +0 -38
  470. package/src/quantize/calibration.ts +0 -181
  471. package/src/quantize/config-writer.ts +0 -145
  472. package/src/quantize/index.ts +0 -84
  473. package/src/quantize/job.ts +0 -162
  474. package/src/quantize/quantizer.ts +0 -666
  475. package/src/quantize/rotate.ts +0 -603
  476. package/src/quantize/safetensors-writer.ts +0 -249
  477. package/src/quantize/sensitivity.ts +0 -216
  478. package/src/quantize/trellis.ts +0 -586
  479. package/src/quantize/weight-transform.ts +0 -292
  480. package/src/registry.ts +0 -602
  481. package/src/runtime-config.ts +0 -81
  482. package/src/safetensors.ts +0 -158
  483. package/src/sampler.ts +0 -1011
  484. package/src/serve/admin-routes.ts +0 -184
  485. package/src/serve/assets/curve-terrain.html +0 -105
  486. package/src/serve/assets/training-inference-map.html +0 -575
  487. package/src/serve/aux-routes.ts +0 -132
  488. package/src/serve/batch-scheduler.ts +0 -8
  489. package/src/serve/checkpoint-identity.ts +0 -64
  490. package/src/serve/generation-gateway.ts +0 -496
  491. package/src/serve/isolate.ts +0 -609
  492. package/src/serve/kv-budget.ts +0 -14
  493. package/src/serve/lab-routes.ts +0 -341
  494. package/src/serve/lane-registry.ts +0 -45
  495. package/src/serve/model-admin-routes.ts +0 -155
  496. package/src/serve/model-binding.ts +0 -43
  497. package/src/serve/model-host.ts +0 -511
  498. package/src/serve/parent-application.ts +0 -56
  499. package/src/serve/prompt-response-trace.ts +0 -147
  500. package/src/serve/response-lease.ts +0 -24
  501. package/src/serve/session-search.ts +0 -326
  502. package/src/serve/static-routes.ts +0 -101
  503. package/src/server.ts +0 -1314
  504. package/src/spec/assistant-rows.ts +0 -92
  505. package/src/spec/assistant-source.ts +0 -126
  506. package/src/spec/cache-identity.ts +0 -22
  507. package/src/spec/deepspec-rows.ts +0 -29
  508. package/src/spec/deepspec-source.ts +0 -145
  509. package/src/spec/dflash-source.ts +0 -149
  510. package/src/spec/drafter.ts +0 -304
  511. package/src/spec/dspark/ab-stats.ts +0 -136
  512. package/src/spec/dspark/calibration.ts +0 -112
  513. package/src/spec/dspark/data-dflash.ts +0 -156
  514. package/src/spec/dspark/data.ts +0 -201
  515. package/src/spec/dspark/deepspec-module.ts +0 -776
  516. package/src/spec/dspark/generate-dflash.ts +0 -245
  517. package/src/spec/dspark/generate.ts +0 -290
  518. package/src/spec/dspark/loader.ts +0 -23
  519. package/src/spec/dspark/loss.ts +0 -213
  520. package/src/spec/dspark/module-dflash.ts +0 -560
  521. package/src/spec/dspark/module.ts +0 -540
  522. package/src/spec/dspark/quantize-drafter.ts +0 -91
  523. package/src/spec/dspark/sample.ts +0 -112
  524. package/src/spec/generate.ts +0 -276
  525. package/src/spec/glm52-mtp-graph.ts +0 -29
  526. package/src/spec/glm52-mtp-rows.ts +0 -132
  527. package/src/spec/glm52-mtp-source.ts +0 -317
  528. package/src/spec/glm52-mtp-state.ts +0 -33
  529. package/src/spec/ngram-source.ts +0 -204
  530. package/src/spec/projected-draft-rows.ts +0 -73
  531. package/src/spec/qwen-mtp-module.ts +0 -171
  532. package/src/spec/qwen-mtp-rows.ts +0 -219
  533. package/src/spec/qwen-mtp-source.ts +0 -383
  534. package/src/spec/qwen-mtp-state.ts +0 -30
  535. package/src/spec/serve-loop.ts +0 -585
  536. package/src/spec/source.ts +0 -215
  537. package/src/spec/two-model-rows.ts +0 -61
  538. package/src/spec/two-model.ts +0 -113
  539. package/src/ssd-cache.ts +0 -372
  540. package/src/ssd-durability.ts +0 -211
  541. package/src/tokenizer.ts +0 -335
  542. package/src/tool-approvals.ts +0 -131
  543. package/src/tool-call.ts +0 -695
  544. package/src/train/dataset.ts +0 -484
  545. package/src/train/diffusion-lora.ts +0 -216
  546. package/src/train/export.ts +0 -32
  547. package/src/train/flash-cce.ts +0 -1234
  548. package/src/train/forward.ts +0 -266
  549. package/src/train/fuse.ts +0 -243
  550. package/src/train/index.ts +0 -46
  551. package/src/train/job.ts +0 -205
  552. package/src/train/lora-params.ts +0 -254
  553. package/src/train/loss.ts +0 -1591
  554. package/src/train/merge.ts +0 -195
  555. package/src/train/optimizer.ts +0 -183
  556. package/src/train/prefix-shared.ts +0 -490
  557. package/src/train/rank.ts +0 -137
  558. package/src/train/segmented.ts +0 -1828
  559. package/src/train/steel-qmm-header.ts +0 -1461
  560. package/src/train/trainer.ts +0 -1221
  561. package/src/train/watch.ts +0 -542
  562. package/src/vision/diffusion-vision.ts +0 -620
  563. package/src/vision/embedder.ts +0 -168
  564. package/src/vision/preprocess.ts +0 -247
  565. package/src/vision/prompt.ts +0 -373
  566. package/src/vision/qwen3vl-preprocess.ts +0 -409
  567. package/src/vision/qwen3vl-prompt.ts +0 -196
  568. package/src/vision/qwen3vl-tower.ts +0 -555
  569. package/src/vision/siglip.ts +0 -599
  570. package/src/vision/video-frames.ts +0 -148
  571. package/src/web/app.html +0 -2143
  572. package/src/web/app.js +0 -5703
  573. package/src/web/skills/memory/SKILL.md +0 -120
  574. package/src/web/skills/web-research/SKILL.md +0 -46
  575. package/src/web/skills.ts +0 -97
  576. package/src/web/src/adapters-panel.ts +0 -288
  577. package/src/web/src/assistant.ts +0 -451
  578. package/src/web/src/chat.ts +0 -1057
  579. package/src/web/src/composer.ts +0 -959
  580. package/src/web/src/dataset.ts +0 -132
  581. package/src/web/src/finetune.ts +0 -267
  582. package/src/web/src/hub.ts +0 -428
  583. package/src/web/src/main.ts +0 -98
  584. package/src/web/src/markdown.ts +0 -0
  585. package/src/web/src/memory-panel.ts +0 -572
  586. package/src/web/src/model-picker.ts +0 -159
  587. package/src/web/src/palette.ts +0 -346
  588. package/src/web/src/protocol.ts +0 -26
  589. package/src/web/src/quantize.ts +0 -167
  590. package/src/web/src/rag.ts +0 -274
  591. package/src/web/src/sessions.ts +0 -469
  592. package/src/web/src/shell.ts +0 -804
  593. package/src/web/src/ui-catalog.ts +0 -78
  594. package/src/web/vendor/README.md +0 -61
  595. package/src/web-assets.ts +0 -47
  596. package/src/web-tools.ts +0 -478
  597. package/src/weights.ts +0 -255
  598. /package/src/{serve → server}/completion-sink.ts +0 -0
  599. /package/src/web/{icon.svg → public/icon.svg} +0 -0
  600. /package/src/web/{manifest.webmanifest → public/manifest.webmanifest} +0 -0
  601. /package/src/web/{vendor → public/vendor}/hljs-LICENSE +0 -0
package/README.md CHANGED
@@ -1,110 +1,1475 @@
1
- # mlx-bun
1
+ # mlx-bun app
2
2
 
3
- A Bun/TypeScript native inference engine and OpenAI-compatible HTTP server
4
- for MLX on Apple Silicon — no Python, no sidecar process. `mlx-bun serve` is
5
- a drop-in for `mlx_lm.server` (same port, endpoints, request fields, and
6
- flags); the correctness contract is logit parity with mlx-lm as the oracle,
7
- verified bit-exact by the test suite. Ships as one signed, notarized binary,
8
- plus a library-first TypeScript API for embedding generation directly in
9
- Bun, Tauri, or Electron apps.
3
+ mlx-bun 1.0.0 is beta and actively developed. APIs and behavior may change;
4
+ breaking changes are called out in release notes.
10
5
 
11
- Docs: **[mlx-bun.dev](https://mlx-bun.dev)**
6
+ The runnable terminal app, server, and web surfaces live here. Default startup
7
+ loads a local model and serves the browser app, Pi web chat, and OpenAI-compatible
8
+ completions through one continuous scheduler. Model-management commands remain
9
+ available separately: `get`, `scan`, `ls`, `fit`, and `gc`.
12
10
 
13
- ## Scope
11
+ After the root native setup, run
12
+ `bun apps/mlx-bun/bin/mlx-bun.mjs serve --model <cached-model-or-directory>`,
13
+ or use `mlx-bun` after the root's `bun run link-cli` step. The launcher checks
14
+ the app manifest's Bun minimum and Apple Silicon macOS before loading app code.
15
+ Help and version work before native setup; inference requires the staged natives.
16
+ Use `serve --help` for accepted options. A terminal session opens the browser
17
+ unless `--no-open` is supplied, focusing an already-open app tab in a running
18
+ Chrome, Arc, Brave, Edge, or Safari (AppleScript) before opening a new one.
19
+ Before selecting a model, startup refuses a TCP port that already serves one
20
+ and names its model (exit 1); port 0 is never checked. Startup without a model
21
+ selects a cached model; if none is supported, it downloads the starter, then hands the recommended model
22
+ to the app's download owner as a background transfer, visible on `GET /downloads`
23
+ and joined at shutdown. A signal before the app exists cancels selection (a
24
+ starter download stays resumable); a signal during model load closes the app as
25
+ soon as it is up. Shutdown handlers are installed as soon as the listener binds.
26
+ A route a surface does not mount returns 404, as do main's lab pages
27
+ (`/curves`, `/curve-terrain`, `/dag`, `/generate`, `/signal`): they are not
28
+ product surface. A request shape the engine cannot run returns a typed 501
29
+ (`UnsupportedExecutionError`); remaining work is tracked in
30
+ [PLAN](../../PLAN.md).
14
31
 
15
- Apple Silicon only — MLX is a Metal framework, so this runs on macOS on M-series
16
- chips by design, nothing else. Serving is single-user: one process serves one
17
- loaded model to one caller at a time (`--isolate --model-pool N` pools
18
- multiple single-model children behind a router; it isn't a multi-tenant
19
- scheduler). Model support is a curated, oracle-backed list, not a generic
20
- "any HF repo" loader — see the roster for what's validated.
32
+ Main's admission and runtime flags keep their units and semantics: `--memory-budget`
33
+ (decimal GB) is the usable envelope for model load, request admission, the process
34
+ allocator limit, optional cache residency, and `/stats.admission.memory_budget_bytes`;
35
+ `--context-length` feeds the context reservation of runtimes that plan memory up front and other models ignore it;
36
+ `--force-wire` and `--allow-private-media` set their library runtime switches for
37
+ the serving process; `--expert-offload` builds `<model>/.mlx-bun-offload` on first
38
+ use and activates it before construction for MoE models, while dense models log
39
+ and continue; the `--hlg-*` family sets the HLG sampling default; and
40
+ `MLX_BUN_SHUTDOWN_TIMEOUT_MS` (finite, positive, else 120 s) bounds shutdown.
41
+ The process-wide settings an app applies (offload routing, the allocator limit,
42
+ the runtime switches) are restored after its engine releases the model, on close
43
+ and on startup failure, so a later app in the same process starts from what it
44
+ found; offload restore only redirects routing and never unmaps borrowed weights.
45
+ `serve` and `generate` accept numerical presets: `--l1` selects KV off
46
+ and unfused SDPA; `--l2` selects model-config KV and fused SDPA, and wins if both
47
+ are given. An explicit `--kv-quant` overrides the preset and sets the kernel
48
+ default (fused for `config`, unfused otherwise); `--fused-sdpa on|off` overrides
49
+ that default. The policy is part of the loaded model's composition, resolved before
50
+ its graph is built (including in isolated model workers); no process setting changes.
21
51
 
22
- ## Install
52
+ `--adapter <dir>` (alias `--adapter-path`) mounts a LoRA adapter right after the
53
+ model loads, before any request, under the directory's basename as its id; it
54
+ becomes the default for requests without an `adapter` field, an explicit
55
+ `adapter` (including `"none"`) still wins, `/v1/adapters` lists it, and a bad
56
+ directory fails startup with `adapter mount failed: …` after releasing the
57
+ model.
58
+ The opt-in [startup adapter test](tests/engine/startup-adapter.test.ts) produces
59
+ a three-step adapter with the fine-tune producer and serves with it; the opt-in
60
+ [DiffusionGemma adapter test](tests/engine/diffusion-adapter.test.ts) serves a
61
+ saved denoising adapter (hot mount and unmount, per-request selection, an
62
+ adapter row beside a base row, the startup default).
63
+ Main's speculative flags are restored with its validation: `--draft-model`
64
+ resolves like the main model (a query never downloads) and its kind is
65
+ auto-detected, `--draft-kind` overrides it (`ngram` is model-free; `mtp` alone
66
+ mounts the bundled companion), `--num-draft-tokens`, `--ngram-max`/`--ngram-min`
67
+ (ngram only; otherwise a warning), and `--mtp on|off` for checkpoints that carry their own draft head.
68
+ `--num-draft-tokens adaptive` chooses each round's draft count, up to the
69
+ drafter's own width, from recent acceptance and the graph's measured verify
70
+ costs; startup refuses it without a drafter or on a graph that declares no costs.
71
+ The opt-in [draft flags test](tests/engine/draft-flags.test.ts) serves with
72
+ ngram drafting and checks the speculation telemetry and exactness against a
73
+ plain run.
74
+ `--paged-kv` (env mirror `MLX_BUN_PAGED_KV=1`) with `--paged-kv-block-size`
75
+ (only alongside paging) composes the paged KV lego when the model loads: every
76
+ request, drafted, grammar, fill, media and adapter rows included, keeps its
77
+ full-attention keys and values in blocks. Pages store what `--kv-quant` names
78
+ for each layer: bf16, affine at the scheme's width (per layer under `config`),
79
+ or TurboQuant. `--paged-kv-direct` reads them with the direct Metal kernel
80
+ (Lab numerics) for decode and windows of up to eight queries; the kernel has
81
+ no compiled form, so that composition decodes without the compiled step.
82
+ The TurboQuant sign tables are generated for the served head dimensions (64, 96, 128,
83
+ 256, 512; `packages/inference/scripts/gen-turboquant-signs.ts`, held to the generator by a
84
+ drift test), so `--kv-quant turbo` composes for every served model; the fused decode kernel
85
+ covers 64, 128, 256 and 512, and 96 decodes on the plain path.
86
+ The graphs that compose their own caches take the scheme and paging too:
87
+ GLM-5.2's compressed MLA state (the latent, RoPE key and DSA index key) is stored
88
+ as the scheme names and paged under `--paged-kv`, and DiffusionGemma's denoising
89
+ caches are the scheme's legos; main accepted the option and served GLM-5.2 in
90
+ bf16 while reporting the requested scheme.
91
+ `--kv-quant config` without the model's `kv_config.json` stays bf16.
92
+ The opt-in [paged KV test](tests/engine/paged-kv.test.ts) serves a Gemma4 and
93
+ a model of another family.
23
94
 
24
- Four ways in; all need an Apple Silicon Mac running macOS.
95
+ Shutdown stops background cache demotion, closes chat sessions, drains active
96
+ HTTP responses, then flushes caches and releases the engine. The CLI bounds this
97
+ with a 120-second deadline; cleanup failures or deadline expiry exit with code 1
98
+ (main exited 0 on timeout). SSD sub-options fail before model loading when saved
99
+ state is off (`--ssd-cache off` or `--prompt-cache 0`) instead of warning and being ignored.
100
+ `MLX_BUN_RD_CONTEXT_LIMIT` is main's enforced context cap, intersected with a
101
+ loaded runtime memory plan (or, under `--memory-budget`, the admission estimate's
102
+ safe context); no CLI flag sets that cap or read-only chat, and `--context-length`
103
+ only sizes a memory-planning runtime's reservation.
104
+ Programmatic composition still accepts explicit context and read-only policy.
25
105
 
26
- ```sh
27
- # Direct download — signed, notarized, no toolchain
28
- curl -fsSL https://mlx-bun.dev/install.sh | sh
106
+ `src/cli/main.ts` dispatches commands; `args.ts` owns accepted options and help;
107
+ the model-management verbs (`get`, `ls`, `scan`, `fit`, `gc`, `upload`) are the models module's; `terminal.ts` owns formatting.
108
+ `model-selection.ts` owns automatic selection policy; its pure choices live in
109
+ `model-choice.ts` ([`mlx-bun/selection`](#selection-entry-mlx-bunselection)).
110
+ `serve.ts` parses the serve flags and owns the process; its composition is
111
+ split in two halves:
112
+ `serve-state.ts` creates the persistent, CPU-only state that outlives a loaded
113
+ model (web assets, the download owner, Responses history, memory, jobs,
114
+ the modules that need job runners or sockets (chat among them), credentials, and their routes) and never imports the engine or a
115
+ native module at runtime; `serve-host.ts` creates the model-scoped host,
116
+ borrowing that state by parameter and lending it an execution lease, library
117
+ invalidation, and the bound port through an attached link. Its
118
+ `startModelHost` is the CLI's loader (runtime switches, expert offload, the
119
+ model context, the startup adapter) and hands the context to
120
+ `startContextHost`, which serves it and any other local model (below): the model
121
+ host, the Whisper companion, the model router, the Pi backend, and the listener.
122
+ A loaded model is a serving unit (`serving-unit.ts`: binding, caches, engine and
123
+ the routes bound to them); the host releases each by the ownership it is given.
124
+ `startApp` (`serve.ts`) composes the state and one host with one close in the
125
+ app's order; `startModelServer` and
126
+ [`mlx-bun/server`](#server-entry-mlx-bunserver) use it.
29
127
 
30
- # Homebrew
31
- brew install joshuarossi/tap/mlx-bun
128
+ ### Model host: residency by memory fit
32
129
 
33
- # bunx — no install, needs Bun >= 1.4.0
34
- bunx mlx-bun
130
+ `residency/model-residency.ts` implements the `modelHost` contract (`@mlx-bun/app-core`)
131
+ for models that generate, over units it does not know the kind of: a loaded model
132
+ (`--in-process`) or a worker process that holds one (the default,
133
+ [Runtime isolation](#runtime-isolation-the-default)). Residency is by memory, not count: a model that fits
134
+ the budget loads beside the others; otherwise the least recently used unpinned,
135
+ unleased model is drained (admission stops, work in flight ends), its saved
136
+ prompt/KV state is flushed durably under `MLX_BUN_HOME/kv`, and its weights are
137
+ released, all before the newcomer loads, so resident bytes never exceed the budget
138
+ during a swap. Acquiring it again reloads it, and the next request finds its
139
+ prefix by token match in the saved state (a swap-back follow-up reports the
140
+ prior conversation as `cached_tokens`). A request for a model that cannot fit
141
+ while every other model is busy waits, and never evicts one mid-request. A model
142
+ bigger than the whole budget is still served alone. The budget is `--model-budget`
143
+ (decimal GB; default 70% of the device's recommended working set, one rule for both
144
+ compositions: `defaultBudgetBytes` in `residency/resident-estimate.ts`, or of the RAM the
145
+ GPU can wire, as `/fit` does, when the device does not say); a model that is not loaded needs
146
+ its weights plus the KV and prefill working set of an 8k context (`residency/resident-estimate.ts`,
147
+ the `/fit` model) and, once loaded, what it
148
+ reports it holds (weights, projected KV, RAM prefix cache), floored by the process's measured
149
+ MLX active memory. A runtime that plans its own memory is served alone. The Whisper companion
150
+ counts against the budget and, when it loads, drains a chat model to make room;
151
+ `--whisper-resident` pins it (it is never a victim).
35
152
 
36
- # From source
37
- git clone https://github.com/joshuarossi/mlx-bun.git && cd mlx-bun
38
- bun install && bun run link-cli
39
- ```
153
+ `server/model-routes.ts` routes by the request's `model`: an exact local id (the ids
154
+ `GET /v1/models` lists) leases that model, and the response body holds the lease until
155
+ it ends, so a model is never released under a stream. Anything else (no id, Pi's `local`,
156
+ a name another server would know) is the current model's, as is every other model-scoped
157
+ path (`/stats`, `/fit`, cache administration, adapters). The current model is the one the
158
+ server started with until `POST /api/hub/serve` (the Models panel) or another
159
+ switch makes a different one current; naming a model in a request never changes it. `GET /v1/models`
160
+ lists every local model with `resident` and `current` (the models module's `/library` marks `serving` and `resident` from the host's own residency); `GET /stats`
161
+ adds `models` (budget, resident bytes, each resident model's bytes, leases and last use).
162
+ Managed jobs and Whisper decoding pause every resident model (`pauseAll`) and hold back new
163
+ loads meanwhile. The host publishes `model.load` (with `resumed` when saved state was found),
164
+ `model.unload` (reason `evicted` or `requested`, whether the flush was durable) and `model.memory`
165
+ on the app's `events` bus, and each unit's engine publishes its request timings and samples through
166
+ `engine/telemetry.ts`; the persistent services' `modelHost` (`served-model-host.ts`) leases the
167
+ current model, whose id follows a switch.
168
+
169
+ `--draft-*`, `--adapter` and `--mtp` belong to the model named at startup and apply
170
+ whenever it is (re)loaded; the other models load plain. Adapters mounted at runtime are
171
+ lost when their model is evicted. `--expert-offload` routes the process's loads through one
172
+ file, so that model is all the process serves. A host built with `createServer` around a
173
+ caller's context serves only that context (never evicted); `POST /api/hub/serve` then still
174
+ answers that a restart is needed. The [residency tests](tests/residency/model-residency.test.ts),
175
+ [router tests](tests/server/model-routes.test.ts) and the opt-in
176
+ [native test](tests/engine/model-host-native.test.ts) (the default isolated mode and `--in-process`)
177
+ cover it.
178
+
179
+ Worker mode serves over a Unix socket a parent supplies, in another process.
180
+ `jobs/worker-process.ts` is the parent-side owner: it spawns the executable
181
+ captured at startup as `__worker` in the compiled binary or the entry script in
182
+ source runs (or `[...command, "__worker"]` for an explicit mlx-bun command),
183
+ writes the launch record as the first stdin line, reads the ready line from
184
+ stdout, and stops the worker with SIGTERM, then SIGKILL after a grace; the end
185
+ of stdin means the parent is gone. `cli/worker-entry.ts` takes two private
186
+ launch forms, and no flag selects either:
187
+
188
+ - The model form `{ version, socketPath, model, options }`, sent by the default
189
+ isolated `serve` (one worker per resident model), runs `startModelHost` alone,
190
+ for that one model, with the persistent services stubbed because the parent
191
+ owns them.
192
+ - The app form `{ kind: "app", version, socketPath, argv }` runs serve
193
+ arguments (`["--model", model, ...]`) through the CLI's `parseCommand` and
194
+ `runServe`, so the whole app listens on the socket instead of TCP.
195
+ `openIsolatedHost` ([`mlx-bun/engine`](#engine-entry-mlx-bunengine)) sends
196
+ it. `validateAppLaunchArgv` (`serve.ts`) is the CLI's strict
197
+ parse with three differences: `--host`, `--port`, and `--no-open` are
198
+ accepted and never steer the socket bind; the app always loads its models in
199
+ its own process (`--in-process` is implied and `--isolate` is ignored), because a nested
200
+ isolated app would bind TCP instead of the socket; and a
201
+ missing or empty model is refused, because automatic selection may download
202
+ the starter model. SIGTERM, SIGINT, or the end of stdin during startup
203
+ aborts through runServe's startup signal and announces nothing. After ready,
204
+ any of them runs runServe's shutdown under the CLI's deadline
205
+ (`MLX_BUN_SHUTDOWN_TIMEOUT_MS`, default 120 s), and its exit code is the
206
+ worker's. `openIsolatedHost` passes that budget to the supervisor as
207
+ `drainTimeoutMs` and `graceMs` (the supervisor's defaults, 10 s and 3 s,
208
+ serve the model form).
209
+
210
+ `server/worker-routes.ts` answers `GET /health`, `POST /admin/lease`,
211
+ `POST /admin/drain`, `POST /admin/memory/complete`, `POST /admin/adapters` and `GET /admin/events` (the
212
+ worker's own event stream, NDJSON) ahead of the routes on
213
+ the socket only: a TCP listener answers 404 for all of them, and `/engine`
214
+ under `--in-process` is 404 too. In the app form its `/health` replaces
215
+ discovery's, and the transcription-only app, which has no execution lease, has
216
+ no `/admin/lease` route (404). The memory route (see
217
+ [Memory synthesis](#memory-synthesis)) is the model form's: the app form's
218
+ synthesis runs in the app itself, so there it is 404.
219
+
220
+ Both launch forms and the ready line carry the app's package version (the one
221
+ `--version` prints). A worker exits 2 with `worker protocol version mismatch`
222
+ when the launch record carries a different package version. The check compares
223
+ package versions only: it detects a different package version, and two builds
224
+ with the same package version are not told apart. The parent rejects a ready
225
+ line that does not echo its version, and a worker's exit 2 (a refused launch
226
+ record) puts the worker's last stderr line into the rejection.
227
+
228
+ In the app form, memory synthesis runs on the app's own task model, as in
229
+ `serve`, and dataset jobs reach the model through the attached host's own
230
+ `/v1/chat/completions` over its socket (the request's
231
+ `127.0.0.1:<--port>` is a placeholder), so they call the same app, and so does the
232
+ chat module (its Pi SDK takes a base URL rather than a fetch, so it talks to the module's
233
+ private loopback, which leases the model host's `generate` per request, over the socket
234
+ here). An embedding host that forwards Request/Response pairs cannot carry `/ws/chat`,
235
+ because it is a WebSocket upgrade.
236
+
237
+ The app form opens the CLI's user stores under HOME. Its first jobs request
238
+ opens the jobs database and marks every queued or running job as a zombie,
239
+ including another server's. The vault, sessions, skills, and credentials are
240
+ shared without locks, and a worker's job children are not stopped when the
241
+ worker crashes. These behaviors predate the app form and match main. Tests
242
+ compose the app form with a temporary HOME and every storage override.
243
+
244
+ ## Runtime isolation (the default)
245
+
246
+ `serve` runs one worker process per resident model and keeps the app up in this
247
+ process, which loads no model; `--in-process` opts out and loads the models in the
248
+ serving process (the composition every earlier release had). `--isolate` is
249
+ accepted, ignored, and warns: it is the default now. Crash isolation is the point: a
250
+ model that crashes the native runtime takes down its own worker, not the app or the
251
+ other models. It costs a process per model and a loopback hop per request.
252
+
253
+ **Process layout.** `serve.ts` resolves the model as usual, then `startModelServer`
254
+ composes `cli/serve-isolated.ts` instead of the direct host: the same persistent
255
+ `createAppState` (web app, download owner, Responses history, memory, jobs,
256
+ sessions, credentials, publishing), the model router and a proxy, and one worker per
257
+ resident model (`cli/worker-unit.ts`), each supervised by `jobs/worker-supervisor.ts`
258
+ over `jobs/worker-process.ts` on its own Unix socket. A worker's launch record pins
259
+ its model by path and carries the parsed serve options (draft and Whisper queries
260
+ already resolved, `isolate` cleared, `inProcess` set; `--draft-*`, `--adapter` and
261
+ `--mtp` go to the startup model's worker only), so a worker never re-resolves a query;
262
+ every exported `MLX_BUN_*` variable reaches it unchanged (that includes
263
+ `MLX_BUN_HOME`, so every worker shares `kv/`), and its output is forwarded to this
264
+ process's log with a `[worker]` prefix. Sockets live in a private `mlx-worker-*`
265
+ temp directory (0700, socket 0600) that this process removes on close. This process
266
+ loads no engine or native module: `serve.ts` imports the model half only inside the
267
+ in-process composition, and the [composition test](tests/serve-isolated.test.ts)
268
+ gates both the static closure and the runtime with tripwire mocks.
269
+
270
+ **Residency.** This process runs the same residency manager as `--in-process`
271
+ ([Model host](#model-host-residency-by-memory-fit)) over workers. A model that
272
+ fits `--model-budget` gets a worker beside the others; otherwise the least recently
273
+ used unpinned, unleased model is drained (`POST /admin/drain`, then SIGTERM), its
274
+ worker flushes its saved state and exits, and only then does the newcomer's worker
275
+ spawn, so resident bytes never exceed the budget. A worker gets the CLI's shutdown
276
+ budget (`MLX_BUN_SHUTDOWN_TIMEOUT_MS`, default 120 s, for the drain and again for
277
+ the stop) and is never killed before its flush is durable; it exits 0 when the flush
278
+ was durable and 3 when it was not, which the `model.unload` event reports as
279
+ `flushed`. Naming an evicted model again spawns a worker that resumes from
280
+ `MLX_BUN_HOME/kv` (`cached_tokens` on the next turn). Each worker measures its own MLX
281
+ memory (active, cache and peak bytes, and the device's recommended working set), since
282
+ this process loads no native module: it reports them on `/health` (`memory`) and as a
283
+ `worker.memory` line in its `/admin/events` stream at connect, on each second it changed,
284
+ and after every finished request (the line is consumed here, never republished on the
285
+ bus). A worker counts as active plus cache bytes from its first report (never below the
286
+ weights it reported (the checkpoint size for a transcription companion): MLX maps weights
287
+ lazily, so a worker that has not run yet reads low),
288
+ in place of the `/fit` estimate, which stands only before that report (a model that has not
289
+ loaded yet needs its estimate) and again while the worker is down. Before it decides who
290
+ fits, the residency manager asks every resident worker for a current reading (`GET /health`),
291
+ so a reading the stream has not delivered yet still counts; `/engine` and `/health` list each
292
+ worker's `memory` (`active_bytes`, `cache_bytes`, `peak_bytes`, or `null`), and
293
+ `GET /stats` `models` counts the measured bytes. The default budget is 70% of the working
294
+ set the startup model's worker reports, the in-process rule (`--model-budget` overrides it).
295
+ A consumer that runs long on a worker holds that model's residency lease for as long as
296
+ it runs, so the worker is never drained under it: memory synthesis (a run holds the current
297
+ model's lease from its first stage call to its last; in-process it runs on its own task model and
298
+ holds none) and, in either composition, modules that generate through the model host (each
299
+ `modelHost` lease on the served model holds the residency lease until it is released). Whisper is a worker of its own (the
300
+ transcription-only server, `--whisper-resident` pins it), started on the first audio
301
+ request and drained like a chat model when memory is short.
302
+ Its worker preserves `--whisper-idle-unload` and `--whisper-resident`; a ready
303
+ worker can release idle Whisper weights. `/v1/models` lists the configured companion
304
+ before it starts, keeps its resolved id, and reports its weights' actual residency.
305
+
306
+ **Readiness.** The listener binds after the startup model's worker reports ready (up
307
+ to 15 minutes for large models, as main), so startup succeeds or fails the way the
308
+ in-process composition does: a worker that dies before its first ready line rejects
309
+ startup with its exit and is never retried; the browser opens once the model serves.
310
+ Other workers spawn when a request or the hub names their model, and that request
311
+ waits for the ready line. The Pi backend learns the current model's capabilities,
312
+ generation defaults, and enforced context window from its worker's `/v1/models` and
313
+ `/stats`, again after each switch, and again for a respawned worker.
314
+
315
+ **Application state.** The web app, Pi chat, the Responses history, jobs,
316
+ downloads, sessions, memory (the vault and the synthesis pipeline; its task
317
+ model is the current worker's), the chat module's tool approvals, and the models module (library, hub, adapters
318
+ and cache cleanup, which protects each resident model's reported snapshot uses) live here and survive worker restarts. The chat module runs in this process and reaches the model through the served model host over this listener's
319
+ proxy, so web chat works under isolation (main answered 501 on `/ws/chat`); its
320
+ `local` model is the current one. Managed jobs pause every resident worker through
321
+ each worker's execution lease.
322
+
323
+ **Routing and proxying.** `server/model-routes.ts` is the same router as in-process,
324
+ over worker units: a model-scoped POST is leased to the worker of the model its body
325
+ names (the lease lasts until the response ends, so a stream is never cut by an
326
+ eviction), anything else is the current model's, `GET /v1/models` and
327
+ `/stats` add residency, and `/v1/audio/*` and `/admin/transcription/*` go to Whisper's
328
+ worker. `server/proxy-routes.ts` is one route group mounted after the persistent groups:
329
+ the hop-by-hop headers are stripped, bodies stream through unchanged, a client abort
330
+ aborts the proxied request (`499`), and a worker that cannot take a request answers
331
+ `502 { error: { type: "engine_unavailable", state } }`. `POST /v1/responses` goes
332
+ through `server/responses-client.ts`: `previous_response_id` resolves against this
333
+ process's store, the worker receives the resolved conversation without one (and the
334
+ `x-mlx-bun-response-owner: parent` header), and the completed record is remembered
335
+ here, so conversations survive a worker restart. This process answers `GET /engine`
336
+ (`{ isolated, state, pid, restarts, socket, model, last_exit, response_store, workers }`,
337
+ the current model's worker first and `workers` listing every resident one with its
338
+ `id`, `role`, `pid`, `state`, `restarts` and `socket`; `state` is `starting`, `ready`,
339
+ `restarting`, `exhausted`, `closed`, or `none` when no worker is resident),
340
+ `GET /health` (`{ status: "ok", isolated, engine, workers }`), `GET /stats` (the current
341
+ worker's body with the residency block, this process's `response_store` and an `engine`
342
+ report; with no worker, 200 with only this process's part and an `unavailable` message),
343
+ and `/admin/lease`, `/admin/drain`, `/admin/memory/complete`, `/admin/events`, `/admin/adapters` and
344
+ `/v1/memory/synthesize` stay socket-only or this process's own and are not forwarded. The hub's `POST /api/hub/serve` is answered here: it acquires the
345
+ model (spawning its worker, draining another when memory is short) and makes it current;
346
+ `404` for an id that is not local, `502` for a failed spawn.
347
+
348
+ **Events.** Each worker publishes its engine's events (request timings, samples,
349
+ prefix and KV counters, load progress) on its own bus and serves them on
350
+ `GET /admin/events`; this process subscribes to every worker's stream and republishes
351
+ onto the app's bus, so the metrics module sees all of them. The residency manager
352
+ here reports `model.load`, `model.unload` and `model.memory` (a worker's own load and
353
+ unload events are dropped), and a respawned worker's stream is resubscribed.
354
+
355
+ **Crashes.** A worker's unexpected exit is respawned by its supervisor with that
356
+ worker's model (the one that was resident, not the startup model) within main's
357
+ budget: at most three restarts in a rolling 60-second window. A worker respawns at once
358
+ after a single crash, even one within 10 seconds of its spawn; a worker that dies within
359
+ 10 seconds of its spawn again straight after such a death waits 5 seconds before the retry. The other workers
360
+ and the app are untouched. A managed job's execution lease is a connection-owned
361
+ `POST /admin/lease` inside each worker, so a respawn waits until it is released and a
362
+ reload never shares the GPU with a job. Requests in flight when a worker dies end with
363
+ `502`; an SSE response ends with the protocol's own error frame naming the cause
364
+ (OpenAI `data: {"error"}`, Anthropic `event: error`, Responses `event: error`), so a
365
+ client parser ends with an error rather than a silent truncation and Pi ends the turn
366
+ with an error frame instead of replaying the generation. While a worker is down,
367
+ new requests for its model answer 502 with the reason and `- retry shortly`; a worker
368
+ that exhausts its budget is reported `exhausted` and is replaced by a fresh spawn on
369
+ the next request that names its model (a broken unit is evicted, never leased). The
370
+ [supervisor](tests/jobs/worker-supervisor.test.ts), [proxy](tests/server/proxy-routes.test.ts)
371
+ and [composition](tests/serve-isolated.test.ts) tests drive these paths against a fake
372
+ worker over a real socket; the opt-in [native test](tests/engine/model-host-native.test.ts)
373
+ kills a real worker mid-request.
374
+
375
+ **Shutdown.** Synthesis runs, jobs, and downloads stop while the workers are alive, chat
376
+ sessions and HTTP responses drain, then every worker is drained, sent SIGTERM (killed
377
+ only after the CLI's shutdown budget), and joined, and the socket directory is removed; a
378
+ respawn in progress is joined too. A worker's own close aborts and joins its memory calls,
379
+ then closes its task model, before its engine.
380
+
381
+ **Deviations from main.** Main bound the listener before the engine loaded and
382
+ made every request wait on readiness, including across restarts, retrying bodyless
383
+ GET/HEAD once; here the listener binds after the first ready line and a request
384
+ during a restart fails fast with 502 so the UI can report the restart instead of
385
+ hanging, and nothing is retried. Main answered 501 on `/ws/chat`; Pi lives in the
386
+ parent here. Main's parent `/health` was the child's; here it is this process's with
387
+ the workers'. Pi's own SDK policy still retries a request refused with 502 before
388
+ generation started (three attempts, 2/4/8 s), which rides out a fast respawn; a
389
+ generation that started is never replayed. A Whisper checkpoint as the main model
390
+ serves the transcription-only server in this process, as before.
391
+
392
+ The CLI uses public library APIs. It does not own cache indexing, downloads,
393
+ fit calculations, model graphs, or numerical execution.
394
+
395
+ From the repository root, run `bun apps/mlx-bun/src/cli/main.ts --help`.
396
+ The [executable behavior examples](tests/hub-cli.test.ts) demonstrate scanning,
397
+ listing, fit estimates, and safe GC against a temporary synthetic cache with
398
+ native MLX blocked. Run them with `bun run --filter mlx-bun test`.
399
+ `bun scripts/verify-packages.ts --app-only` repeats those tests through an
400
+ installed package artifact, compiling only the app microphone helper and never
401
+ loading MLX or accessing audio.
402
+
403
+ `gc` previews changes unless `--yes` is supplied; `--dry-run` always prevents
404
+ deletion. Cache location and Hugging Face credentials follow the
405
+ [hub library](../../packages/hub/README.md). The app and public libraries use MIT
406
+ licensing and version 1.0.0 for the first workspace release. The root workspace,
407
+ website and transcription example app remain private.
408
+
409
+ ## One-shot inference
410
+
411
+ `mlx-bun generate [query] --prompt "…"` (alias `gen`) prints generated text and
412
+ exits. It uses the model's chat template when available; `--raw` feeds the prompt
413
+ verbatim. Main's greedy sampling, 256-token default cap, optional sampler/KV
414
+ settings, and unfiltered generated text are CLI policy. Model-author serving
415
+ sampling defaults do not replace them. Tier and compiled-step switches are not
416
+ exposed (compiled decode is a composition field the loader resolves), and the
417
+ request runs on the same continuous engine at capacity one, a batch of one.
418
+ `generate --help` lists the accepted settings, including every `--kv-quant`
419
+ scheme `serve` takes and `--quantized-kv-start`.
420
+
421
+ `mlx-bun embed [query] --text "…"` prints a vector per text, or one OpenAI-style
422
+ list with `--json`. Without text it reads nonempty stdin lines; `--instruct`
423
+ retains the query instruction. Without a model query it selects the first cached
424
+ embedding model. Generate resolves a local directory or registry query directly.
425
+ Neither command downloads models, opens a listener, or starts the browser.
426
+
427
+ `cli/inference.ts` owns argument policy, model selection, and terminal output.
428
+ The model host allows a missing template for these consumers; serving still
429
+ requires one. Shared text prompt construction preserves the CLI's explicit
430
+ no-added-specials encoding and HTTP's existing duplicate-BOS correction.
431
+ Both commands close the owned engine after success, failure, or cancellation.
432
+ [CPU examples](tests/inference-cli.test.ts) cover behavior and composition and
433
+ run against the installed artifact in `verify-packages --app-only`;
434
+ real-weight correctness and speed remain separate verification.
435
+
436
+ ## mlx-lm compatibility (`mlx-bun.<cmd>`)
437
+
438
+ mlx-bun is a superset of mlx-lm's command line under its own names. Each alias
439
+ runs the matching verb with `mlx_lm.<cmd>`'s argument spellings translated, and
440
+ still accepts the verb's own options; `mlx-bun.<cmd> --help` lists what it maps,
441
+ accepts without effect, and refuses. An explicit path behaves as in mlx-lm; a
442
+ default output path does not use the working directory (`mlx_model`,
443
+ `fused_model`, `adapters`) but stays under `~/.mlx-bun`, and every other default
444
+ is the verb's, not mlx-lm's (for example `generate` stops at 256 tokens, not 100).
445
+
446
+ | Alias | Verb | mlx_lm flags translated |
447
+ | --- | --- | --- |
448
+ | `mlx-bun.server` | `serve` | `--model --adapter-path --host --port --decode-concurrency --max-tokens --temp --top-p --top-k --draft-model --num-draft-tokens`; headless (`--no-open`) unless told otherwise |
449
+ | `mlx-bun.generate` | `generate` | `--model --prompt/-p (- reads stdin) --max-tokens/-m --temp --top-p --top-k --min-p --min-tokens-to-keep --xtc-probability --xtc-threshold --seed --system-prompt --adapter-path --ignore-chat-template --kv-bits (4 or 8, from token 5000) --trust-remote-code --verbose` |
450
+ | `mlx-bun.convert` | `convert` | `--hf-path/--model --mlx-path -q --q-bits --q-group-size --dtype -d/--dequantize --upload-repo --trust-remote-code` (4/8-bit, group 32/64, affine) |
451
+ | `mlx-bun.fuse` | `fuse` | `--model --adapter-path --save-path --dequantize --upload-repo` |
452
+ | `mlx-bun.lora` | `train` (SFT) | `--model --train --data --fine-tune-type lora --optimizer adam/adamw --num-layers --batch-size --iters --val-batches --learning-rate --steps-per-report --steps-per-eval --grad-accumulation-steps --resume-adapter-file --adapter-path --save-every --max-seq-length --grad-checkpoint --seed --mask-prompt -c/--config` (YAML) |
453
+ | `mlx-bun.upload` | `upload` | `--path --upload-repo` |
454
+
455
+ Not supported, each an error naming the mlx_lm flag: `generate` `--extra-eos-token
456
+ --prefill-response --use-default-chat-template --chat-template-config
457
+ --max-kv-size --prompt-cache-file --quantize-activations --draft-model
458
+ --num-draft-tokens`, `--kv-bits` other than 4/8, `--kv-group-size` other than 64;
459
+ `server` `--allowed-origins --log-level --chat-template --use-default-chat-template
460
+ --chat-template-args --min-p --prompt-concurrency --prefill-step-size
461
+ --prompt-cache-size --prompt-cache-bytes --pipeline`; `convert` `--quant-predicate`
462
+ and `--q-mode` other than affine, `--q-bits` other than 4/8, `--q-group-size` other
463
+ than 32/64; `fuse` `--export-gguf --gguf-path`; `lora` `--test --test-batches`,
464
+ `--fine-tune-type dora|full`, optimizers other than adam/adamw, `--clear-cache-threshold
465
+ --report-to --project-name`, and the YAML keys `lr_schedule` and `lora_parameters.keys`.
466
+ argparse's unique-prefix abbreviations (`--max-tok`) are not accepted. Two behaviors differ without an error: `mlx-bun.lora` always masks the prompt in the
467
+ loss (mlx-lm does so only with `--mask-prompt`), and `--trust-remote-code` is
468
+ accepted because mlx-bun never runs code from a model repository.
469
+
470
+ mlx-lm's other console scripts have no counterpart and get no alias: `chat` (no
471
+ terminal chat; use the web app), `benchmark`, `cache_prompt`, `evaluate`,
472
+ `perplexity` (use `mlx-bun perplexity` with a local data file), `manage`, `share`, and the quantizers `awq`, `dwq`, `dynamic_quant`
473
+ and `gptq` (`convert --target-bpw` is the mixed-precision path).
40
474
 
41
- Every channel builds from the same bundle. Details, signing, and the native
42
- runtime resolution order: [docs/reference/distribution.md](./docs/reference/distribution.md).
475
+ Delivery: the package's `bin` links `mlx-bun.<cmd>` to `bin/mlx-bun.<cmd>.mjs`, a
476
+ one-line file that runs the shared launcher (npm and Bun resolve a bin to its real
477
+ file, so the file's name is the invoked name); the standalone executable
478
+ dispatches on the name it was started as; `scripts/install.sh` links each alias
479
+ to the same executable beside `~/.local/bin/mlx-bun`, and `bun run link-cli`
480
+ links them into `~/.bun/bin`. [Alias tests](tests/mlx-lm-aliases.test.ts) run
481
+ mlx_lm's own argument forms through each alias and the launcher files;
482
+ [installer tests](tests/install.test.ts) cover the links.
43
483
 
44
- ## Quickstart
484
+ ## Engine
45
485
 
46
- Bare `mlx-bun` aliases to `mlx-bun serve`; with no model named it downloads a
47
- sub-GB starter and opens a chat UI. To pick a model explicitly:
486
+ `src/engine/` owns loaded model lifetimes, preparation admission and the shared
487
+ continuous scheduler. `createAppEngine` takes ownership of its model context;
488
+ closing drains execution before releasing compiled runners, adapters, drafts,
489
+ model constants and weights. Replacement models can provide their own binding.
490
+ HTTP parsing, response formatting and web policy belong above this boundary.
491
+
492
+ [Engine behavior tests](tests/engine/model-host.test.ts) verify injected model
493
+ loading and lifecycle without native MLX. The gateway and session tests beside
494
+ it cover cancellation, output delivery and the same scheduler path at capacity
495
+ one and greater. These are CPU checks, not real-weight numerical verification.
496
+ A request shape the loaded composition does not serve gets the typed capability
497
+ error; there is no other path to fall to. Diffusion models are served through the same
498
+ scheduler by the library's interleaved denoising method. An image request's pixels
499
+ ride its options, and the gateway releases them exactly once, whether the row
500
+ settles or fails before submission. Requests the method cannot apply (grammar,
501
+ draft, logprobs, logits processors, fill) receive the typed
502
+ capability error; its caches are the legos the composition's KV scheme and paging name.
503
+
504
+ `engine/cache-services` builds the loaded model's cache service (the library's
505
+ `state/cache-service`) from the serve flags: RAM prefixes, saved state, generation
506
+ checkpoints, `--kv-budget` admission and paging, behind one interface. Its
507
+ default is 8 GB of RAM with plain KV; a caller supplies `ssdCacheDir` for saved state.
508
+ `mlx-bun serve` supplies it by default (`MLX_BUN_HOME/kv`, `--ssd-cache <dir>` moves it,
509
+ `off` disables it): one directory per model identity under the root, and
510
+ `engine/kv-budget.ts` holds one byte budget across all of them (`--ssd-cache-max`,
511
+ default 20 GiB, 0 unlimited). A live store is lent what the other stores and idle
512
+ directories leave (never less than a quarter of the budget); a closed store's
513
+ directory is idle, and idle directories lose their oldest files first, at start and
514
+ after every close. A model that returns finds its directory, and its prefix, intact.
515
+ The hub's resolved model directory supplies the identity, so startup does not hash
516
+ weight files. Saved KV is disposable; after replacing weights in place, clear it
517
+ when you want fresh state.
518
+ Composition passes the returned `cache` and `adapterNamespace` as gateway options;
519
+ `/stats`, the cache routes, telemetry and the model host read the service's own
520
+ `report()`. The engine borrows the service; construction alone does not attach it.
521
+ Pass its `close` through the owned `beforeModelDispose` hook so shutdown
522
+ drains execution, flushes persistence, clears cache state, then frees the model.
523
+ `flush` also supports explicit durability checks while the app is running:
524
+ `server/cache-routes.ts` serves main's `POST /admin/cache/flush` (200 with the
525
+ durability counters plus `entries` and `longest_durable_prefix_tokens`, 503
526
+ with the same counters when anything remains pending or failed) and
527
+ `POST /admin/cache/session/close` (`closed` reports whether a string
528
+ `session_id` was supplied; closing drops the session's cache association and
529
+ never deletes checkpoints, and it never waits for a flush in progress).
530
+ Cache policy tests inject storage and allocator ports; real SSD/numerical runs
531
+ remain separate verification.
532
+
533
+ ## Local decisions
534
+
535
+ `POST /v1/decisions` accepts the OpenAI Decisions request and returns its ordered
536
+ answer array (`predicate`, `choice`, `score`). `POST /v1/systemone` accepts Jev's
537
+ `state` and questions map and returns its keyed answers (`noul`, `choice`, `score`).
538
+ Both endpoints normalize into one handler and one numerical operation. Question
539
+ names identify responses and are excluded from the model input. Jev's structured
540
+ instructions/criteria, gzip/zstd requests and multipart/base64 image assets are
541
+ accepted; OpenAI accepts text and `input_image` parts through the existing media
542
+ preparation. Images require a graph with the corresponding media operations.
543
+ Box questions and denoising/pixel-input decision graphs are unsupported.
544
+
545
+ Point OpenAI's `baseURL` at `http://localhost:8080/v1`, or TypeSafe's `baseURL` at
546
+ `http://localhost:8080`, and set `model` to the exact local ID from `GET /v1/models`,
547
+ for example `mlx-community/gemma-4-e4b-it-OptiQ-4bit`. The residency host leases
548
+ that model, loading it when needed and sharing its resident weights with generation.
549
+ The existing current-model fallback also accepts cloud names for compatibility;
550
+ the local examples use the actual model ID. Responses identify the model that actually ran. The same loaded
551
+ model serves generation and decisions: each candidate is a scheduler task between
552
+ generation iterations, with private caches and its own adapter scope.
553
+
554
+ Each supplied answer is encoded as its complete JSON value (score levels use
555
+ their label), followed by the chat template's assistant-turn ending through its
556
+ first stop marker, including a next-role marker for templates that use that
557
+ boundary (the tokenizer's end token when no template ending is available). A teacher-forced forward
558
+ reads the prompt and supplied answer tokens. The response mask counts only
559
+ answer-token predictions, including the terminator. DPO's existing summed log
560
+ likelihood is divided by the answer token count, then normalized as
561
+ `softmax(-NLL / answer_token_count)` across the supplied candidates. No token is
562
+ sampled or generated. Scores use average token log likelihood, so longer answers
563
+ do not receive a multiplicative length penalty from summing token losses.
564
+ Scores come directly from the loaded model's answer likelihoods; there
565
+ is no tuning or calibration stage. Choice/score confidence is a statistic of
566
+ that distribution, computed with Jev's documented formulas.
567
+ Usage counts the tokens processed across candidate forwards and reports zero
568
+ output tokens. These operations currently keep no reusable prompt cache;
569
+ OpenAI cache counters are zero and cache hints do not change scoring.
570
+ Decision requests use bounded engine admission, returning 429 when its wait queue
571
+ is full. Image preparations hold the same media reservation used by chat until
572
+ the question finishes scoring or fails.
573
+
574
+ With a cached vision model, for example:
48
575
 
49
576
  ```sh
50
- mlx-bun serve e4b --port 8080
577
+ mlx-bun serve mlx-community/gemma-4-e4b-it-OptiQ-4bit
578
+ bun apps/mlx-bun/scripts/decision-example.ts http://localhost:8080 mlx-community/gemma-4-e4b-it-OptiQ-4bit
579
+ node apps/mlx-bun/scripts/decision-website-examples.mjs http://localhost:8080 mlx-community/gemma-4-e4b-it-OptiQ-4bit /path/to/lane-scene.png
51
580
  ```
52
581
 
53
- ```sh
54
- curl http://localhost:8080/v1/chat/completions \
55
- -H 'Content-Type: application/json' \
56
- -d '{"messages": [{"role": "user", "content": "Hello!"}], "max_tokens": 128}'
582
+ The [HTTP example](scripts/decision-example.ts) covers all three question types
583
+ through both endpoints and generation through the same model. The
584
+ [website examples](scripts/decision-website-examples.mjs) use the upstream OpenAI
585
+ and TypeSafe SDKs with local connection settings (Node 22+); a supplied image runs
586
+ the lane-choice scenario. OpenAI's lane widget is a scripted illustration, so use
587
+ a scene-only capture without the widget's displayed probabilities. The opt-in
588
+ [acceptance test](tests/engine/decisions-native.test.ts) runs these examples with
589
+ `MLX_BUN_TEST_NATIVE=1 MLX_BUN_APP_TEST_MODEL=<cached snapshot>` and optional
590
+ `MLX_BUN_DECISION_IMAGE=<lane capture.png>`; it also compares concurrent generation
591
+ against a solo greedy control. Tests without weights check SDK wire compatibility,
592
+ validation, compression, complete answer preservation, cancellation and disposal.
593
+
594
+ ## Server seams
595
+
596
+ `server/routes.ts` composes chat/text completion, Anthropic Messages, Responses, embedding, and discovery
597
+ handlers over an injected engine. Its `handle(Request)` returns a response or
598
+ `null` for the next application surface; it never opens a socket or closes the
599
+ borrowed engine. Application startup owns those lifetimes.
600
+
601
+ Tool-approval settings are the chat module's. Confirmed cache cleanup (`/api/gc/*`) is the models module's,
602
+ over the hub library and the model host's resident models.
603
+
604
+ Inside `server/`, request parsing and prompt preparation precede the single-use
605
+ admission plan. The completion executor consumes the engine contract; the sink
606
+ and OpenAI wire modules own reasoning/tool/content events, JSON, and SSE.
607
+ `prompt-contracts.ts` describes owned media inputs; `media-prompt.ts` owns the
608
+ HTTP media policy (which content parts are present, the video guard, and the 400
609
+ text for each refusal) and delegates to the loaded model's media preparation.
610
+ `engine/media-preparation.ts` binds one route per family when the model loads;
611
+ each route borrows the context's tokenizer, template, token ids and lazily loaded
612
+ towers and calls the library's numerical input builders. The opt-in
613
+ [native media test](tests/engine/media-native.test.ts) (`MLX_BUN_TEST_NATIVE=1` plus
614
+ `MLX_BUN_APP_TEST_GEMMA4_AUDIO_MODEL`, `MLX_BUN_APP_TEST_GEMMA4_UNIFIED_MODEL`,
615
+ `MLX_BUN_APP_TEST_QWEN_VISION_MODEL` or `MLX_BUN_APP_TEST_DIFFUSION_MODEL`, each a
616
+ cached snapshot directory) prepares image, audio, mixed and video prompts through a
617
+ loaded model with synthesized media and a warm encoder cache; the opt-in
618
+ [media generation test](tests/engine/media-generation.test.ts) serves the same
619
+ Gemma4 checkpoint and generates from image, audio and mixed image+audio prompts
620
+ (determinism, streaming, rows released together, disconnect recovery). Grammar and media
621
+ work enter the engine's preparation domain before allocating native resources.
622
+ Text-only protocol work loads no MLX library. `anthropic.ts` translates Messages
623
+ requests and semantic completion events, including tools, thinking, and usage.
624
+ Its JSON and SSE paths use the same preparation, capability admission, scheduler,
625
+ and cancellation as chat completions; no second service is created.
626
+ `responses.ts` owns Responses translation and its process-local, one-hour,
627
+ 32 MiB history. Successful JSON and SSE requests retain input, output, and
628
+ instructions for `previous_response_id`; failed or cancelled requests do not.
629
+ A generation error ends SSE with `response.failed`, without a misleading completion event. Composition
630
+ may supply `responseHistory` to replace the store; no isolated-worker forwarding
631
+ or duplicate completion service is involved.
632
+
633
+ The [request pipeline](tests/server/pipeline.test.ts) and
634
+ [HTTP examples](tests/server/routes.test.ts) execute with an injected engine,
635
+ including cancellation and ownership cleanup. Real-weight media and generation
636
+ verification remains separate; these tests prove the HTTP/engine boundary.
637
+ `server/start.ts` mounts HTTP, browser, and the modules' WebSocket handlers. Shutdown
638
+ stops the modules (the chat's sockets and turns), then closes connections, drains
639
+ execution, flushes caches, and releases the model. Its [listener tests](tests/server-start.test.ts) exercise real loopback
640
+ sockets without native MLX. The opt-in [model test](tests/engine/http-generation.test.ts)
641
+ uses `MLX_BUN_APP_TEST_MODEL=<cached-directory>` to exercise actual app startup,
642
+ lone/concurrent deterministic completion, live cache counters, stream cancellation,
643
+ Messages JSON, Responses JSON with a chained follow-up, and a real Pi WebSocket
644
+ turn with session persistence and replay. All chat, vault, and skill paths are
645
+ temporary; HTTP and WebSocket waits share a bounded network budget before shutdown.
646
+ The cached checkpoint used for this smoke is `mlx-community/MiniCPM5-1B-OptiQ-4bit`,
647
+ revision `664aabaed233c653f82716d8dc822234d0091f78`.
648
+ It never downloads weights; missing native libraries or an invalid supplied
649
+ checkpoint fail. This checks HTTP/Pi behavior, not quantize jobs, a compiled-binary
650
+ lifecycle, numerical parity, or performance.
651
+
652
+ The opt-in [paged KV test](tests/engine/paged-kv.test.ts) also covers Gemma4
653
+ HTTP cancellation for each paged reader: the gathered reader and the direct
654
+ reader that `--paged-kv-direct` composes (the composition's `pagedKv.direct`,
655
+ which `/stats` prints). A client leaves a greedy
656
+ `/v1/completions` stream mid-generation; the server observes the disconnect, the
657
+ row stops storing tokens before its natural end, and the scheduler drains. The
658
+ same server then answers another prompt and the abandoned one exactly as a fresh
659
+ paged server with the same reader does, and the frames the client received equal
660
+ a control stopped (`max_tokens`) at the token that published the last of them.
661
+ Every paged cache in the case uses the selected reader; both servers run without
662
+ the RAM prompt cache so every compared request prefills cold. Run with
663
+ `cd apps/mlx-bun && MLX_BUN_COMPILED_GEMMA_E4B=<cached Gemma4 snapshot directory> bun test tests/engine/paged-kv.test.ts -t "reader"`.
664
+
665
+ `server/status-routes.ts` borrows live cache, scheduler, model diagnostic and
666
+ Responses-history counters for `GET /stats`; `GET /fit` uses the public inference
667
+ fit functions (`@mlx-bun/inference/execution/fit`) and the served artifact metadata. Predictions remain advisory and do
668
+ not impose an admission limit. A runtime that plans its own memory reports that
669
+ plan (`/fit`'s `plan` object lists its streamed tiers and generation allowance;
670
+ `/stats.runtime` carries the runtime's own telemetry) instead of the generic
671
+ resident-weight estimate. `/stats.batch` reports the continuous scheduler
672
+ (`mode: "batch"`, `batched: true`) at every capacity, one included. Pending SSD counters include the generation
673
+ checkpoint queue as well as prompt-cache persistence work.
674
+ Historical EvalDB measurements have no migrated owner, so measurement fields
675
+ remain null; old machine-specific throughput constants are not reported as
676
+ measurements for the current server or CLI fit output. The library retains those
677
+ historical constants. The dashboard shows an unavailable marker
678
+ when no estimate exists. [Status tests](tests/server/status-routes.test.ts) use
679
+ synthetic counters and CPU fit inputs without a model or native MLX.
680
+
681
+ ## Client entry (`mlx-bun/client`)
682
+
683
+ `mlx-bun/client` (`src/server/client.ts`) is main's embedding client. It
684
+ imports no other module, so it loads without native MLX. `mlx-bun/engine`
685
+ (below) re-exports it.
686
+
687
+ - `createCompletionClient({ baseUrl, headers?, host? })` posts to
688
+ `<baseUrl>/chat/completions` (or `route: "completions"`) with the caller's
689
+ `headers` merged with `content-type: application/json`, the body with
690
+ `stream: false` forced, and the call's `signal`. The transport is the supplied
691
+ host's `forward`, otherwise the global `fetch`; the host stays the caller's.
692
+ A POST is never retried: a non-2xx status rejects with the status and response
693
+ text, and a result without a `choices` array rejects.
694
+ - `createDirectHost(handler, shutdown?)` gives an in-process
695
+ `(Request) => Promise<Response>` handler the same host shape. It is an adapter,
696
+ not a model loader. `close()` refuses new work, waits for handlers in flight,
697
+ then calls `shutdown` once; a handler owns any response stream it returns, and
698
+ streams pass through unchanged.
699
+ - Types: `EngineHost`, `CompletionCall`, `CompletionResponse`, and main's
700
+ completion contracts `CompletionClient`, `BatchCompletionClient` (ordered
701
+ `completeBatch`), and `TaskClient` (`run` with progress and an optional
702
+ `Cancellation`).
703
+
704
+ The [client tests](tests/client.test.ts) import the entry through the export
705
+ map with native MLX blocked; `verify-packages` repeats them from the installed package.
706
+
707
+ ## Engine entry (`mlx-bun/engine`)
708
+
709
+ `mlx-bun/engine` (`src/cli/engine-entry.ts`) is main's `src/library.ts`
710
+ without `initializeMlx`. It loads without native MLX and starts nothing on
711
+ import. It re-exports the client above with its contract types,
712
+ `createInferenceEngine` and `CancellationSource` with main's generation
713
+ contract types from `@mlx-bun/inference`, and adds `openIsolatedHost`. Main's
714
+ `initializeMlx` and its native bootstrap were removed deliberately: the native
715
+ runtime ships inside the packages and loads on first use, so there is no
716
+ acquisition step to call and no root (`.`) namespace to return. Import each
717
+ member from its owner instead (the migration table below). The root's selection helpers are
718
+ `mlx-bun/selection` (below), and main's in-process server is
719
+ [`mlx-bun/server`](#server-entry-mlx-bunserver).
720
+
721
+ ```ts
722
+ import { createCompletionClient, openIsolatedHost } from "mlx-bun/engine";
723
+
724
+ const host = await openIsolatedHost("/path/to/hf-snapshot", { arguments: ["--max-tokens", "256"] });
725
+ try {
726
+ const client = createCompletionClient({ baseUrl: "http://engine/v1", host });
727
+ const result = await client.complete({ body: { messages: [{ role: "user", content: "Hello" }] } });
728
+ console.log(result.choices[0]?.message?.content);
729
+ } finally {
730
+ await host.close();
731
+ }
57
732
  ```
58
733
 
734
+ The caller owns the host: `openIsolatedHost` starts one worker process in the
735
+ app launch form above, and `close()` stops it and removes its private socket
736
+ directory after it exits. The function's JSDoc states the argument, command,
737
+ retry, and close rules.
738
+
739
+ - `arguments` are `mlx-bun serve` arguments. `--host`, `--port`, and
740
+ `--no-open` only concern a TCP listener and have no effect on the socket;
741
+ `--isolate` is ignored, as the app form is always in-process.
742
+ - `command` must run the mlx-bun CLI of the same package version: the
743
+ installed binary, `[bun, <package>/bin/mlx-bun.mjs]`, or
744
+ `[bun, <package>/src/cli/main.ts]`. A compiled consumer must supply it,
745
+ because its own executable is not the mlx-bun CLI. The version handshake
746
+ refuses another package version but cannot tell apart two builds with the
747
+ same version.
748
+ - The host forwards to the whole app, so the app form's limitations above
749
+ apply: the user stores under HOME are shared with other mlx-bun processes
750
+ without locks, and `/ws/chat` cannot pass through `forward`. These predate this entry.
751
+
752
+ The [engine tests](tests/engine-entry.test.ts) import the entry through the
753
+ export map with native MLX blocked and drive the host over a fake worker;
754
+ `verify-packages` repeats them from the installed package. The worker's own
755
+ refusal and startup behaviors are covered by the
756
+ [worker-entry](tests/worker-entry.test.ts) and
757
+ [worker-process](tests/jobs/worker-process.test.ts) tests. The opt-in
758
+ [native test](tests/engine/library-host.test.ts) (`MLX_BUN_TEST_NATIVE=1` with
759
+ `MLX_BUN_APP_TEST_MODEL` and/or `MLX_BUN_APP_TEST_WHISPER_MODEL`) runs real
760
+ consumers, including a standalone CLI as `command`, under a temporary HOME;
761
+ its real-HOME check is a names-only guard.
762
+
763
+ ## Selection entry (`mlx-bun/selection`)
764
+
765
+ `mlx-bun/selection` (`src/cli/model-choice.ts`) is main's pure model-selection
766
+ helpers from `src/fit.ts`. It imports no module, so it loads without native MLX.
767
+ It exports `DEFAULT_REPO_ID`, `STARTER_REPO_ID`, `COEXIST_FRACTION`,
768
+ `chooseAutoModel(candidates, preferredRepo, fitsFull, fitsCoexist)` (the
769
+ automatic rule `model-selection.ts` applies), and
770
+ `largestRecommendedRepoId(ramBytes)` (the 26B at 48 GiB or more, the 12B at
771
+ 24 GiB or more, otherwise `DEFAULT_REPO_ID`; an explicit opt-in that automatic
772
+ selection never calls). Migrating from main's root: `recommendedRepoId()` is
773
+ `DEFAULT_REPO_ID`; `largestRecommendedRepoId()` is
774
+ `largestRecommendedRepoId(totalmem())` (RAM is now required); `chooseAutoModel`,
775
+ `COEXIST_FRACTION` and `DEFAULT_REPO_ID` import from `mlx-bun/selection`.
776
+
777
+ The [selection tests](tests/selection-entry.test.ts) import the entry through the
778
+ export map with native MLX blocked; `verify-packages` repeats them from the
779
+ installed package.
780
+
781
+ ## Server entry (`mlx-bun/server`)
782
+
783
+ `mlx-bun/server` (`src/cli/server-entry.ts`) restores main's in-process
784
+ `createServer` and `loadContext`. It loads without native MLX and starts nothing
785
+ on import. `createServer` composes the app `mlx-bun serve` composes (`startApp`,
786
+ then `startContextHost`) around a context the caller loaded, so it serves the
787
+ full app: every HTTP route, the browser app, Pi web chat on `/ws/chat`, jobs,
788
+ hub, memory, sessions, and publishing. It parses no CLI arguments, installs no
789
+ signal handler, opens no browser, and downloads nothing. The function's JSDoc
790
+ states the option, ownership, and close rules.
791
+
59
792
  ```ts
60
- import OpenAI from "openai";
61
- const client = new OpenAI({ baseURL: "http://localhost:8080/v1", apiKey: "local" });
62
- const res = await client.chat.completions.create({
63
- model: "local",
64
- messages: [{ role: "user", content: "Hello!" }],
65
- });
793
+ import { createServer, loadContext } from "mlx-bun/server";
794
+
795
+ const context = await loadContext("/path/to/hf-snapshot", "my-model");
796
+ const server = await createServer(context, 8080, { hostname: "127.0.0.1", memoryBudgetBytes: 12e9 });
797
+ try {
798
+ console.log(`API http://127.0.0.1:${server.port}/v1`);
799
+ } finally {
800
+ const { stopped, durability } = await server.close();
801
+ if (stopped) context.dispose();
802
+ if (!durability.durable) console.warn("cache persistence incomplete", durability);
803
+ }
66
804
  ```
67
805
 
68
- Longer walkthroughs on the site:
69
- [Installation](https://mlx-bun.dev/getting-started/installation/) and
70
- [Quickstart](https://mlx-bun.dev/getting-started/quickstart/).
71
-
72
- ## Reference docs
73
-
74
- Each fact about the project has exactly one home:
75
-
76
- | Topic | Doc |
77
- |---|---|
78
- | Current state, what's next | [STATUS.md](./STATUS.md) |
79
- | Benchmark numbers (parity / performance / quality) | [docs/reference/benchmarks.md](./docs/reference/benchmarks.md) |
80
- | Supported models roster | [docs/reference/models.md](./docs/reference/models.md) |
81
- | Server start flags, `MLX_BUN_*` env, defaults | [docs/reference/server-config.md](./docs/reference/server-config.md) |
82
- | HTTP API routes and request/response schemas | [docs/reference/server-api.md](./docs/reference/server-api.md) |
83
- | CLI verbs | [docs/reference/cli.md](./docs/reference/cli.md) |
84
- | TypeScript library API | [docs/reference/library-api.md](./docs/reference/library-api.md) |
85
- | LoRA fine-tuning (SFT / DPO / ORPO) | [docs/reference/training.md](./docs/reference/training.md) |
86
- | Build, sign, notarize, publish | [docs/reference/distribution.md](./docs/reference/distribution.md) |
87
- | Troubleshooting | [docs/reference/troubleshooting.md](./docs/reference/troubleshooting.md) |
88
- | Personal memory (local wiki) | [docs/reference/memory.md](./docs/reference/memory.md) |
89
- | Reference environment / oracle setup | [docs/reference/environment.md](./docs/reference/environment.md) |
90
- | Active engineering design docs | [docs/design/](./docs/README.md) |
91
- | Contributing / repo rules | [CONTRIBUTING.md](./CONTRIBUTING.md) |
92
-
93
- Correctness is verified bit-exact against the Python mlx-lm reference as the
94
- project's oracle: [benchmarks.md § Parity](./docs/reference/benchmarks.md#1-parity-porting-correctness--bit-exact-vs-the-oracle).
95
-
96
- ## Why
97
-
98
- MLX is Apple's ML framework — hand-tuned Metal kernels with official
99
- bindings for Python, C++, Swift, and C, but no JavaScript story. The
100
- performance-critical work (every matmul, every attention pass) lives in
101
- MLX's C++/Metal core, exposed through `mlx-c`; the layer on top — model
102
- loading, tokenization, sampling, serving — is pure orchestration and
103
- performance-neutral, so it can move to a better runtime without losing
104
- speed. Bun fits: `bun:ffi` binds `mlx-c` directly with no node-gyp step,
105
- `Bun.Image` gives native OS image codecs for vision input, and the result
106
- ships as one binary with no Python venv.
107
-
108
- ## License
109
-
110
- MIT. Third-party attributions: [THIRD_PARTY_LICENSES.md](./THIRD_PARTY_LICENSES.md).
806
+ - Options are serve's settings under their programmatic names (`capacity`,
807
+ `contextLimit`, `defaultGeneratedTokens`, `kvBudgetBytes`,
808
+ `memoryBudgetBytes`, `cache`, `request`, `whisper`, `readOnly`, and the
809
+ `memoryPaths`, `chatPaths`, and `storagePaths` store overrides). Defaults
810
+ follow main's library: port 0 binds an ephemeral port, an omitted `hostname`
811
+ binds every interface (tests pass `127.0.0.1`), capacity 8, bf16 KV, an 8 GB
812
+ RAM prompt cache, and `MLX_BUN_RD_CONTEXT_LIMIT` as the context cap when set.
813
+ User stores default to HOME and are shared with other mlx-bun processes
814
+ without locks, as for the CLI.
815
+ - A supplied graph: `binding` (`ModelBinding`) replaces the built-in numerics
816
+ and `buildPrompt` (`ModelPromptBuilder` returning `BuiltPrompt`) replaces
817
+ chat prompt construction. Both reach the engine and routes as given, with no
818
+ registry lookup, model class, or reload. A context without a chat template is
819
+ refused (`model <id> has no chat template`) unless `buildPrompt` builds its
820
+ prompts; such a builder returns `probeStableLen: false`. `artifact` (`path`,
821
+ `sizeBytes`, `expertsBytes`) only describes the weights for `/fit`, `/stats`,
822
+ and hub GC protection.
823
+ - Ownership: the context is borrowed by default, as in main. Close and every
824
+ failed start release what the server created and leave the context usable,
825
+ including for another server; the caller disposes it after close resolves
826
+ with `stopped: true`. `ownership: "owned"` makes the server dispose it exactly
827
+ once, after execution drains or when startup fails. The rule is
828
+ `createAppEngine`'s `ownership` option (`releaseContext`,
829
+ `engine/model-host.ts`), applied by the host and the entry until the engine
830
+ takes the context.
831
+ - `close({ timeoutMs = 120_000 })` resolves `{ stopped, timedOut, durability }`
832
+ with the final cache flush's result, `durable: false` included. One drain runs
833
+ however often it is called. At the deadline it resolves `stopped: false,
834
+ timedOut: true` with the pending counters and `durable: false` while the
835
+ drain continues; nothing is released under running execution. `flush()`
836
+ flushes cache persistence while serving.
837
+ - Process-wide state: the server applies no runtime switches and activates no
838
+ expert offload; those stay with the process's runtime configuration and the
839
+ CLI's loader. It sets the MLX allocator limit from a runtime memory plan or
840
+ `memoryBudgetBytes` and restores the value it found on close and on a failed
841
+ start. The limit is one per process: servers running at the same time share
842
+ it and the last applied wins. Sequential servers over one context are
843
+ supported. The Pi SDK's `proper-lockfile` registers `signal-exit`'s listeners
844
+ on SIGINT, SIGTERM, and other signals when the app's modules first load
845
+ (under the CLI too); they re-raise the signal when no other listener exists.
846
+
847
+ Migration from main (`02d723a`):
848
+
849
+ | main | `mlx-bun/server` |
850
+ | --- | --- |
851
+ | `createServer(ctx, port = 0, serverOptions)` returned Bun's `Server` | `await createServer(context, port = 0, options)` returns `{ port, flush, close }` |
852
+ | `shutdownServer(server, timeoutMs = 120_000)` → `{ stopped, timedOut, durability }` | `server.close({ timeoutMs })`, the same result; `server.stop()` becomes `await server.close()` |
853
+ | `flushServerCacheDurability(server)` | `server.flush()` |
854
+ | `loadContext(dir, id?, { implementations, profiles, … })` | the same, from `mlx-bun/server` |
855
+ | `ServerContext`, `ServingContext` | `ModelContext`, `LoadedModelContext` |
856
+ | `ServerContext.serving?: ModelServingBinding` | `options.binding` (`ModelBinding`) and `options.buildPrompt` for `serving.buildPrompt`; `createSerial` has no counterpart, since every request runs on the shared scheduler through `binding.gateway` |
857
+ | `ModelPromptBuilder`, `BuiltPrompt` | the same names |
858
+ | `batch` (1 pinned main's one-request executor) | `capacity` (1 is the continuous scheduler at capacity one) |
859
+ | `promptCacheBytes`, `kvQuant`, `quantizedKvStart`, `turboQuant`, `ssdCacheDir`, `ssdCacheMaxBytes`, `ssdDemoteIdleSec`, `ssdCacheVerify`, `generationCheckpointTokens` | `cache.*` |
860
+ | `pagedKv`, `defaultThinking`, `defaultTemperature`, `defaultTopP`, `defaultTopK`, `hlg` | `request.*` |
861
+ | `defaultMaxTokens` | `defaultGeneratedTokens` |
862
+ | `whisperModelDir`, `whisperModelId`, `whisperIdleUnloadSec`, `whisperResident` | `whisper.modelDir`, `.modelId`, `.idleUnloadSec`, `.resident` |
863
+ | `kvBudgetBytes`, `memoryBudgetBytes`, `hostname`, `defaultAdapter` | the same |
864
+ | `owner` | not an option; `/stats` reports `embedded` |
865
+ | `unixSocket` | not provided; the private worker launch forms own Unix sockets |
866
+ | `CompletionClient`, `BatchCompletionClient`, `TaskClient` (`mlx-bun/engine`) | the same, from `mlx-bun/engine` or `mlx-bun/client` |
867
+ | `mlx-bun/package.json` | the same subpath |
868
+
869
+ The [server entry tests](tests/server-entry.test.ts) import the entry through
870
+ the export map with native MLX blocked, then compose the real app over a
871
+ supplied binding in child processes with a temporary HOME: main's model
872
+ replacement shape (one context, five sequential servers), the full app and
873
+ `/ws/chat`, the template-less builder path, startup failures (options, state,
874
+ caches, bind) under both ownerships, and close's deadline, drain, and
875
+ durability evidence.
876
+ `verify-packages` repeats them from the installed package. The opt-in
877
+ [native test](tests/engine/in-process-server.test.ts) (`MLX_BUN_TEST_NATIVE=1`
878
+ with `MLX_BUN_APP_TEST_MODEL`) serves a real loaded context through a caller
879
+ binding under a temporary HOME, aborts a live stream during close, and reuses
880
+ the borrowed context for a second server.
881
+
882
+ ## Web chat
883
+
884
+ The chat is [`@mlx-bun/module-chat`](../../packages/module-chat/README.md): the Pi backend, policy,
885
+ transcripts, tool approvals, `/ws/chat`, the session routes, and the browser panel. This app installs it
886
+ (`src/modules.ts`) and runs it in the persistent state (`cli/serve-state.ts`), which serves its socket on the
887
+ listener (`createModuleSockets`) and stops it, closing every chat, before the listener drains. The app decides
888
+ three things for it: `readOnly` (`ServeOptions.readOnly`), the working directory (`chatPaths.cwd`), and, through
889
+ `chatPaths` (`sessionDir`, `agentDir`, `toolApprovalsFile`), where an embedder put its stores instead of the
890
+ module's storage entries; these are composition options, not CLI switches. Memory reaches the chat through the
891
+ registry: `@mlx-bun/module-memory/chat` registers the vault's read-only tools (`chat.tool`) and their hint and skill
892
+ (`chat.guidance`), each offered only while the vault exists.
893
+
894
+ The [existing-user data test](tests/existing-user-data.test.ts) opens data a
895
+ prior version left under HOME (sessions, Pi settings, approvals, the saved
896
+ token, the vault and its Reference links, adapter stores, the nightly schedule)
897
+ through the app with HOME set to a clone and native MLX blocked, and requires
898
+ nothing lost: the supplied directory and its link targets unchanged, no
899
+ deletion, a byte-identical vault, and only the app's own databases
900
+ (`.mlx-bun/db`), its bundled skill, and append-only or message-preserving
901
+ session rewrites. The earlier `~/.cache/mlx-bun` jobs, memory and registry
902
+ databases are not carried over and must stay byte-identical; its adapter stores
903
+ must appear in the picker's catalog. Its default case builds main's formats
904
+ in-test; `MLX_BUN_APP_TEST_USER_DATA=<isolated copy laid out as a HOME>` runs it
905
+ on real data and prints what to preserve before an old checkout is deleted. It
906
+ refuses a copy holding, at any depth, a link that resolves into a live store
907
+ (`~/.mlx-bun`, `MLX_BUN_HOME`, `~/.cache`, `~/.pi`, `~/Library`) or a directory
908
+ link leaving the copy.
909
+
910
+ ## Browser app
911
+
912
+ `src/web/browser/` preserves the existing model, training, quantization,
913
+ dataset and status UI as pages of the web shell; memory's module owns its persistent sidebar overlay alongside chat
914
+ ([`@mlx-bun/web-shell`](../../packages/web-shell/README.md)), which owns navigation, routing, theme,
915
+ the command palette's chrome and the mounting of module panels. `main.ts` composes the shell, the panels of the
916
+ installed modules (`installed-panels.ts`, generated by `web/build.ts` from the host's installed modules, so the bundle
917
+ carries every panel the host installs and nothing else; the chat is the first, a workspace panel) and the pages that
918
+ have not moved into modules yet; `chat-host.ts` is the app's side of the chat panel (memory's provenance chips and
919
+ sidebar entry, the agent-tools settings) and `shell.ts` the app's glue around the shell (its routes, the Hugging Face
920
+ and agent-tools settings, the hamburger, overlays and key bindings that reach the chat panel, the connection pill).
921
+ Browser code imports only local browser modules, the shell package and the data-only job protocol. Unported backend features may return 501;
922
+ preserving their UI does not claim their backend is ready.
923
+
924
+ `src/web/assets.ts` provides `createWebHandler()`, which loads the static payloads
925
+ and returns a `Request → Response | null` handler for application composition.
926
+ It opens no listener. `bun run --filter mlx-bun build:web` creates ignored
927
+ `dist/web/app.js`; prepack generates and includes that file, so installed apps
928
+ need no build step. A source checkout with no generated bundle compiles the
929
+ browser entry in memory on startup, without writing installation files. The
930
+ build script and runtime fallback share `web/build.ts`. Static HTML, theme, manifest, worker, and icon live in
931
+ `src/web/public/`. The vendored highlight.js bundle retains its BSD license in
932
+ `public/vendor/hljs-LICENSE`; its existing header records upstream provenance.
933
+
934
+ [Browser behavior tests](tests/web/browser.test.ts) cover streaming rendering,
935
+ escaping, attachments, panels, and interactions without a live server.
936
+ [Boot tests](tests/web/boot.test.ts) run the built bundle against `app.html` (the shell's tabs and pages, a
937
+ module panel mounted through the shell). [Static tests](tests/web/assets.test.ts) exercise the built bundle and asset
938
+ headers. The packed consumer check verifies the same assets after installation.
939
+
940
+ Adapters are the models module's (`/v1/adapters*`, [module README](../../packages/module-models/README.md)): the routes
941
+ reach the served model through its `adapters` operation, which `engine/adapter-operation.ts` builds over the loaded context
942
+ (mount, unmount and merge run under the engine execution lock in the process that holds the model); no HTTP handler owns
943
+ tensors. Serving with an adapter uses the shared scheduler. Adapter discovery reports the graph's actual mount
944
+ targets; the loader binds each request's projection deltas before its forward runs.
945
+
946
+ ## Memory vault
947
+
948
+ `@mlx-bun/module-memory/article` owns Markdown article structure; `vault.ts` owns vault
949
+ initialization, filesystem reads, search, links, and Git history. The default
950
+ vault is `~/.mlx-bun/wiki`, with `MLX_BUN_WIKI` as its override.
951
+ `@mlx-bun/module-memory/routes` exposes the read/init HTTP surface through
952
+ `createMemoryRoutes({ root })`; The app installs the memory module in persistent state and mounts its declared routes.
953
+ Initialization is explicit and idempotent, and its path stays confined to the
954
+ vault or temporary trees. Existing article/Talk directory links remain usable;
955
+ initialization confines its actual write targets. Reference seeding defaults to
956
+ none; composition can pass explicit `referenceSources` without inferring old
957
+ repository documentation paths. Merely starting the app does not create a vault.
958
+
959
+ [Route tests](tests/server/memory-routes.test.ts) use injected temporary vaults
960
+ and real local Git history; [article tests](../../packages/module-memory/tests/article.test.ts) cover
961
+ parsing and round trips. `query.ts` owns deterministic article navigation;
962
+ `tools.ts` owns the read-only Pi definitions and prompt hint. CLI composition
963
+ passes the same vault root to REST and to chat (through `@mlx-bun/module-memory/chat`) and supplies a skill directory
964
+ (default `~/.mlx-bun/skills`). `ServeOptions.memoryPaths` permits isolated app
965
+ composition without adding CLI flags. Missing vaults expose no memory tools and
966
+ create no skill files. Bundled skills are module package assets and are embedded in the standalone binary.
967
+
968
+ [Tool tests](../../packages/module-memory/tests/tools.test.ts) exercise temporary vaults, and the
969
+ [chat tools test](../../packages/module-memory/tests/chat-tools.test.ts) executes a memory tool through a real
970
+ read-only Pi session, contributed to the chat module through the registry, with a synthetic loopback model.
971
+ No read tool starts a synthesis run.
972
+
973
+ ### Memory synthesis
974
+
975
+ Main's nightly pipeline lives under `packages/module-memory/src/` unchanged in prompts, stage
976
+ order, database schema (`db.ts`, `~/.mlx-bun/db/memory.sqlite`), vault
977
+ layout, Git usage, and the dedup/normalize/reconcile rules: `pipeline.ts` drives
978
+ the four resumable, chronological stage workers in `stages.ts` (SEGMENT via
979
+ `chunk.ts`, ENTITY-EXTRACT via `entity.ts` + `resolve.ts`, ROUTE via `route.ts`,
980
+ SYNTHESIZE via `synthesize.ts`/`cluster.ts`/`reconcile.ts`), then the
981
+ deterministic `crosslink.ts` pass and the `wikify.ts` editorial sweep. `events.ts`
982
+ holds the shared event contract so no stage imports the orchestrator.
983
+
984
+ The engine is reached only through `model.ts`'s `MemoryCompletionClient` seam.
985
+ The memory domain defines the interface; composition injects an
986
+ implementation. `cli/memory-engine.ts` is main's memory task model (Gemma-4
987
+ e4b and its `memory-chunk` adapter), loaded in-process by the first completion
988
+ over the app engine's continuous gateway: the `memory` verb and its nightly job
989
+ use it, and so does `serve`'s own synthesis, which keeps it resident until
990
+ shutdown, as main did. Under `serve` each task-model completion (or batch, once
991
+ all its rows join) holds the served engine's execution lease, taken before the
992
+ weights load, so memory work never overlaps a managed job; chat waits while a
993
+ memory stage call runs (main's in-server client ran beside chat under its own
994
+ locks). `server/memory-completion-client.ts` holds the private worker client; the module owns its loopback client. The
995
+ loopback client posts each stage call to a serving mlx-bun's own
996
+ `/v1/chat/completions` (raw greedy sampling, neutral logit processors and the
997
+ model template's thinking defaults, the stage's system/user turns,
998
+ `adapter: "memory-chunk"` for the chunk stage when
999
+ `~/.mlx-bun/adapters/memory-chunk` exists and `"none"` otherwise):
1000
+ `memory --host`/`--port` use it.
1001
+
1002
+ In the default isolated `serve` the parent, which loads no model, keeps the pipeline,
1003
+ vault, and SSE; the current model's worker owns the task model (the same
1004
+ in-process client, loaded by its first call and kept until that worker stops). The parent's worker client (also in
1005
+ `server/memory-completion-client.ts`) sends each `complete` or
1006
+ `completeBatch` as one `POST /admin/memory/complete` over the worker's
1007
+ socket (`{ call, snapshot, requests: [{ stage, input, maxTokens }] }`, answered
1008
+ `{ outputs }` in input order). The worker runs the call under its own execution
1009
+ lease, taken before the lazy load and released after every row joined, so it
1010
+ waits for a managed job's lease and a job waits for it; the parent takes no
1011
+ lease of its own for it. The parent selects the task model snapshot once per call
1012
+ (`locateTaskModel`), the call carries it, and the call that loads the task model
1013
+ loads exactly that directory; the parent keeps the selected snapshot out of hub GC
1014
+ for that worker's lifetime (until it is respawned or the server stops). A cancelled run, a client
1015
+ disconnect, or the parent's shutdown aborts the request, and the worker aborts
1016
+ and joins every row. A call is never retried: a worker that stops mid-call fails
1017
+ that call with an error, and a restart drops the task model with
1018
+ its worker. Every path rides a continuous-batching scheduler, where one call is a
1019
+ batch of one. `MLX_BUN_MEMORY_BATCH`
1020
+ (default 1) bounds the calls in flight per batched stage. Nothing in the memory
1021
+ domain loads a model. Each run receives its client
1022
+ and vault root explicitly; Meta policy reads use that same vault. A failed batch
1023
+ stops admission, cancels siblings, and joins them before returning its error.
1024
+ The memory module's `createMemorySynthesis` (`packages/module-memory/src/synthesis.ts`) owns active server runs: request/body cancellation
1025
+ aborts the run and its in-flight completions (or its wait for the lease), and
1026
+ server shutdown cancels and joins all synthesis alongside managed jobs and
1027
+ downloads, then closes the task model, before engine drain. Completed stage writes are resumable; cancellation does not roll them back.
1028
+
1029
+ `GET /v1/memory/synthesize[?dry=1]` streams the run as SSE (`stage`/`log`/`done`
1030
+ events, a `summary`, then `[DONE]`; a failure ends with an `error` event); it is
1031
+ mounted only when composition supplies the pipeline. `mlx-bun memory` exposes
1032
+ main's subcommands: `init`/`setup` (the wizard below), `status` (default),
1033
+ `open`, `list`, `search`, `toc`, `section`, `links`, `read`, `synthesize`
1034
+ (`--dry-run`; `--since`/`--model` parsed but not consumed, as in main), the
1035
+ stage workers `segment`, `extract`, `route`, `synthesize-stage` (`--limit`,
1036
+ `--convs`), `link`, and `schedule`/`unschedule`. Model-driven subcommands load
1037
+ the memory task model in-process on first use (`cli/memory-engine.ts`), as main
1038
+ did, or talk to the serving mlx-bun named by `--host`/`--port`. The entity gold main read from
1039
+ `goldens/dreaming-entities-gold.json` is a published dataset: without that
1040
+ file the resolver runs unseeded (store aliases still fold).
1041
+
1042
+ [Pipeline tests](../../packages/module-memory/tests/) port main's model-free suites with fake stage
1043
+ calls, in-test vaults, and an in-test entity gold; the
1044
+ [client test](tests/server/memory-completion-client.test.ts) and
1045
+ [verb test](tests/memory-cli.test.ts) use a fake fetch and a temporary HOME.
1046
+
1047
+ ### Memory setup and nightly schedule
1048
+
1049
+ `mlx-bun memory init` (also `memory setup`) runs main's onboarding wizard.
1050
+ `mlx-bun setup` is main's true alias of `mlx-bun memory`: `mlx-bun setup init`
1051
+ is the same wizard and a bare `mlx-bun setup` reports status. The wizard
1052
+ initializes the vault through the same `setupVault` as `POST /api/memory/init`
1053
+ (idempotent; no reference seeding), offers to import an existing wiki's
1054
+ `articles/` (main's prompt; default yes once a path is given; the import is
1055
+ committed), and offers to install the nightly synthesis job (main's prompt;
1056
+ default no; then asks for the time, default 03:00). A non-TTY stdin answers
1057
+ every prompt with its default, so a scripted run initializes the vault and
1058
+ installs nothing.
1059
+
1060
+ `memory schedule [--at HH:MM]` writes main's launchd agent,
1061
+ `~/Library/LaunchAgents/com.mlx-bun.memory.plist` (label `com.mlx-bun.memory`,
1062
+ `StartCalendarInterval` at the local time, default 03:00, `RunAtLoad` off,
1063
+ `ProcessType` Background, logs in `~/.mlx-bun/logs/memory-synthesis.{out,err}.log`),
1064
+ then `launchctl unload` and `launchctl load -w` it; a failed load is reported,
1065
+ not thrown. `memory unschedule` runs `launchctl unload -w` and deletes the
1066
+ plist. The job runs `/bin/zsh -lc "exec <program> memory synthesize"`, where
1067
+ the program is the executable identity captured at startup
1068
+ (`jobs/executable.ts`) plus the CLI entry when running from source; it is
1069
+ never a PATH lookup and never `process.execPath` read later. The job's
1070
+ `memory synthesize` loads the memory task model itself, so it needs no running
1071
+ server; every status surface says so.
1072
+
1073
+ `scheduleStatus` reports `installed` (plist present), `loaded` (`launchctl
1074
+ list com.mlx-bun.memory` succeeds), `at` (read back from the plist; `null`
1075
+ when absent or hand-edited), and `note` (what the job runs and needs).
1076
+ `memory status` prints it as the `nightly` line, `GET /api/memory/status`
1077
+ returns it as `schedule` beside `status` (absent when no vault exists), and
1078
+ the `memory_status` tool prints its `nightly` line; the memory skill tells the
1079
+ assistant not to claim a scheduled run happened unless the last synthesis
1080
+ commit shows it.
1081
+
1082
+ `@mlx-bun/module-memory/schedule` owns the plist and takes the home directory and the
1083
+ `launchctl` runner as seams; `cli/memory.ts` adds the vault root, the job's
1084
+ program, and the prompt; the tool and route factories take a `schedule` probe.
1085
+ [Schedule tests](../../packages/module-memory/tests/schedule.test.ts) and the
1086
+ [setup verb test](tests/setup-cli.test.ts) drive every path under a temporary
1087
+ home with a recording `launchctl`, an injected vault root, and scripted
1088
+ answers; spawned runs use a temporary HOME and a non-TTY stdin. No test
1089
+ reaches the real launchd, `~/Library/LaunchAgents`, or `~/.mlx-bun`.
1090
+
1091
+ ## Jobs, quantization, and fine-tuning
1092
+
1093
+ `storage/paths.ts` owns every default location the app writes, all under
1094
+ `MLX_BUN_HOME` (default `~/.mlx-bun`), read from the environment at call time:
1095
+ `models/` (convert, web quantize and fuse outputs, plain model directories),
1096
+ `adapters/` (train, web fine-tune, merge, memory stages), `exports/`,
1097
+ `datasets/`, `db/` (jobs, model index, memory), `jobs/` (job logs), and the
1098
+ wiki, skill, log and credential files, and the chat module's `sessions/`, `pi-sessions/` and `tool-approvals.json`. The Hugging Face cache
1099
+ holds downloads only; the model index (`openRegistry()`) scans it and
1100
+ `models/`, so `ls`, `/library`, `serve <name>` and the folder picker find both.
1101
+ Explicit paths (`--mlx-path`, `--save-path`, `--adapter`, request fields) win.
1102
+ Earlier versions' job history and model index under `~/.cache/mlx-bun` are not
1103
+ carried over (the index rebuilds by scan); their adapter stores stay listed.
1104
+
1105
+ `jobs/` owns the lazily opened SQLite store, durable NDJSON events, SSE tails,
1106
+ and managed subprocess lifetimes. `cli/job-entry.ts` resolves the producer in the child process: the installed
1107
+ module that registered the job kind. HTTP parsing and job wire responses stay in `server/job-routes.ts`.
1108
+
1109
+ Fine-tuning is the train module's ([`@mlx-bun/module-train`](../../packages/module-train/README.md): the
1110
+ `finetune` job, `/api/finetune/inspect-dataset` and `/submit`, the `train`, `train-watch`, `fuse` and `draft` verbs,
1111
+ `mlx-bun.lora` and `mlx-bun.fuse`, and the `adapters/` storage entry, with `models/` and `datasets/` shared with
1112
+ quantize and datasets). This app installs it (`src/modules.ts`) and supplies its core services in the
1113
+ persistent state the same way as quantize's. The verbs run through the verb table (`cli/module-verbs.ts`); a verb
1114
+ that names no model runs on the app's automatic choice (`resolveModelAuto`, the one `serve` makes), which the
1115
+ verb host supplies to the catalog. Adapter merge and export are the models module's, on the served model's `adapters`
1116
+ operation (below).
1117
+
1118
+ Quantization and `convert` are the quantize module's ([`@mlx-bun/module-quantize`](../../packages/module-quantize/README.md):
1119
+ the `quantize` job, `/api/quantize/*`, the `convert` verb and `mlx-bun.convert`). This app installs it
1120
+ (`src/modules.ts`) and supplies its core services in the persistent state (`cli/serve-state.ts`): its
1121
+ `process` runner is a child of the job host (`jobs/service.ts`, `jobs/runner.ts`), which holds the execution
1122
+ lease until the child's process group is gone; the child (`cli/job-entry.ts`) activates the module that
1123
+ registered the job kind and runs its runner. A one-shot `convert` (`cli/module-verbs.ts`) runs over a job
1124
+ store of its own, created on first use and removed when it ends, so its runs never appear in the job history.
1125
+ `convert`'s and `fuse`'s `--upload-repo` push through the modules' `catalog` service (`publishing/catalog-hub.ts`).
1126
+ `/api/model/resolve-folder`, the fine-tune wizard's folder picker, is the models module's over the same catalog.
1127
+
1128
+ Composition injects the engine execution lease. A job drains active inference
1129
+ and holds that lease until its child's process group is gone and output streams
1130
+ finish; inference then resumes. The child leads its own process group, so
1131
+ descendants that outlive it are stopped (SIGTERM, then SIGKILL after 3 s) before
1132
+ the job counts as joined. The wait is bounded: a process still alive 3 s after
1133
+ SIGKILL, or one outside the group still holding the output after another 3 s, is
1134
+ logged and left behind, and the lease is released. Terminal signals no longer reach a group leader, so the
1135
+ parent holds a pipe on the child's stdin and the child stops its group when that
1136
+ pipe ends (`MLX_BUN_JOB_PARENT_PIPE`, set by the runner). A row the runner cannot
1137
+ read after admission fails the job before anything is spawned. As in main's
1138
+ direct-process server, resident model weights and caches remain allocated while
1139
+ the child runs. Shutdown stops queued jobs, aborts admission waits, terminates
1140
+ the active child's process group, and awaits all of it (with the same bound) before closing the store
1141
+ and engine. Every job row records main's `ended_at` format
1142
+ (`YYYY-MM-DD HH:MM:SS`, UTC) and `Name: message` errors, in-process dataset jobs included. Opening the app does not create the job database until a job
1143
+ route is used. A fine-tuning job selects its own model path;
1144
+ the resident inference model's adapter/training capabilities do not gate it.
1145
+
1146
+ The [training CLI tests](tests/train-cli.test.ts) spawn the CLI with native MLX blocked and drive the module's verbs through the
1147
+ verb table: help (including `draft`'s subcommand paragraph), usage errors, refusals and a dry-run plan. The
1148
+ module's own tests (`packages/module-train/tests`) use injected dependencies. An opt-in native check saves a tiny
1149
+ drafter and resolves it like a model path ([draft CLI test](tests/draft-cli.test.ts)).
1150
+
1151
+ [Job lifecycle tests](tests/jobs/lifecycle.test.ts) exercise leases, crash/error
1152
+ paths, shutdown, HTTP/SSE, and a real CPU-only child with temporary storage;
1153
+ [process-group tests](tests/jobs/process-group.test.ts) use real child processes
1154
+ with descendants holding the job's output.
1155
+ The [module's tests](../../packages/module-quantize/tests) verify option forwarding and output
1156
+ naming with an injected numerical operation; they do not run or establish parity for actual
1157
+ checkpoint quantization.
1158
+ The [train module's policy tests](../../packages/module-train/tests/policy.test.ts) cover the app recipe,
1159
+ explicit overrides, dataset inspection, HTTP submission, progress, and resource
1160
+ cleanup with a fake native runtime. These CPU checks do not extend the numerical
1161
+ claims in the [training evidence](../../packages/training/README.md). The opt-in
1162
+ [fine-tune preservation test](tests/engine/finetune-preservation.test.ts)
1163
+ (`MLX_BUN_TEST_NATIVE=1 MLX_BUN_APP_TEST_FINETUNE_REFERENCES=<ref.json>[:…]`) runs
1164
+ this producer on each submit record main's producer ran, for any family or
1165
+ training path, and requires main's metrics, config files, adapter and checkpoint
1166
+ tensors, fresh-reload logits, and optionally its `fuse` output exactly; its
1167
+ header gives the commands that produce the references outside the repository.
1168
+ On 2026-09-28 it passed on the M1 Max against main `02d723a` run from source with
1169
+ this tree's staged native library, for mlx-community MiniCPM5-1B-OptiQ-4bit
1170
+ (`664aabae`). The records were SFT (8 iterations, batch 2, accumulation 2, dropout,
1171
+ rsLoRA, checkpoints, fused output), ORPO (default L3 heads, grad checkpointing)
1172
+ and DPO (LoRA+). A reference with a changed seed failed as expected.
1173
+
1174
+ Composition takes `storagePaths` (job store, saved token file, and an artifact
1175
+ root replacing `MLX_BUN_HOME` for models, adapters, exports and datasets) like
1176
+ `chatPaths` and `memoryPaths`, so embedded and test servers never touch the
1177
+ user's jobs, credentials or artifacts. The opt-in
1178
+ [managed-jobs acceptance](tests/engine/managed-jobs.test.ts) runs a real
1179
+ `mlx-bun serve` process under a temporary HOME and `HF_HUB_CACHE` with the
1180
+ cached model named by `MLX_BUN_APP_TEST_MODEL`: a quantize job whose artifact
1181
+ appears in the library and reloads for a short generation, inference after its
1182
+ lease, a three-step SFT job with a
1183
+ periodic checkpoint, a long job cancelled by shutdown (child terminated, row
1184
+ terminal, complete checkpoint metadata kept, no final adapter, bounded progress),
1185
+ and a restart on the same storage that mounts the preserved checkpoint; then a spawned `train` interrupted by SIGINT at a step
1186
+ boundary, and, with a cached bf16 snapshot named by
1187
+ `MLX_BUN_APP_TEST_BF16_MODEL`, a `convert` interrupted after its durable job log
1188
+ reaches the Probing/Sensitivity stage and a complete uniform conversion whose
1189
+ output reloads and generates. It downloads nothing.
1190
+
1191
+ A further case in the same file consumes finished fine-tune outputs as
1192
+ artifacts, with the RAM prompt cache off so every compared request prefills
1193
+ cold. Two SFT jobs train on sentences written at run time. Each finished
1194
+ adapter (not a checkpoint), and their merge through `POST /api/finetune/merge`,
1195
+ is mounted with `POST /v1/adapters` and selected per request. A selected
1196
+ adapter changes the greedy `/v1/completions` text; requests without one, and
1197
+ requests after `DELETE /v1/adapters/<id>`, return the base choice exactly, text
1198
+ and per-token logprobs, and an unmounted id is refused. A fresh process mounting
1199
+ the same two directories reproduces both adapters' choices exactly.
1200
+ `mlx-bun fuse` then folds one adapter into the base. The output keeps
1201
+ `config.json`, the tokenizer files, the tensor inventory (names, dtypes, shapes)
1202
+ and every tensor outside the folded modules byte-identical, changes only folded
1203
+ modules, and loads with `serve` and generates. Its output is not compared with
1204
+ the mounted adapter: `fuse` re-quantizes folded modules with their source spec,
1205
+ so the fused model and the adapter are not bit-exact by contract. Run with
1206
+ `cd apps/mlx-bun && MLX_BUN_APP_TEST_MODEL=<cached snapshot directory> bun test tests/engine/managed-jobs.test.ts -t "finished fine-tune outputs"`.
1207
+
1208
+ ## Dataset jobs
1209
+
1210
+ Dataset generation is the datasets module's ([`@mlx-bun/module-datasets`](../../packages/module-datasets/README.md):
1211
+ the thirteen templates, `/api/dataset/templates` and `/api/dataset/submit`, the `dataset`
1212
+ job kind, the Docker Python verifier). This app installs it (`src/modules.ts`) and supplies
1213
+ its core services in the persistent state (`cli/serve-state.ts`), because a module that
1214
+ requires `jobs` runs beside the job store: `jobs/service.ts` runs its task runners on the
1215
+ job host, so its rows, logs and `/api/jobs` streams are the same as any job's;
1216
+ `storage` resolves the module's `datasets/` entry under `MLX_BUN_HOME` (the
1217
+ `artifactRoot` override replaces it, as for other produced artifacts); and
1218
+ `cli/served-model-host.ts` is the `modelHost` that leases the attached serving host's
1219
+ model for `generate`, sending the request to its own HTTP listener (its Unix socket in
1220
+ the worker app form; through the proxy in the default isolated mode), so jobs call the same app.
1221
+
1222
+ Adapter merge/export requests are the models module's. Merge is the served model's `adapters` operation: it runs the
1223
+ public training library under that model's engine execution lock, in the process that holds the model. The default isolated
1224
+ parent loads no MLX, so `cli/worker-unit.ts` reaches the worker through `cli/worker-adapters.ts` (one JSON call on the
1225
+ worker's private `POST /admin/adapters`, served by `server/worker-routes.ts` on the worker's own operation), and
1226
+ `residency/leased-adapters.ts` holds the model resident for each call; `--in-process` calls the unit directly. Export writes
1227
+ a CPU-only manifest without taking any lease. Merges land in
1228
+ `~/.mlx-bun/adapters/merged-…` and exports in `~/.mlx-bun/exports/export-…`,
1229
+ with unique suffixes so simultaneous requests cannot overwrite each other's
1230
+ artifacts.
1231
+
1232
+ `GET /v1/adapters/available` lists the catalog's adapters (`adapterStores()` in
1233
+ `storage/paths.ts`): `~/.mlx-bun/adapters`, then, read-only, the stores
1234
+ earlier versions wrote (`~/.cache/mlx-bun/adapters`,
1235
+ `~/.cache/mlx-bun/mlx-bun-finetunes`, `~/.cache/mlx-bun-finetunes`), each
1236
+ directory once. Nothing is moved. The [storage layout tests](tests/storage-layout.test.ts)
1237
+ drive each producer's real default (web fine-tune, merge, a `train` dry run,
1238
+ the memory stage reader) and require the app's catalog to list it; they
1239
+ check that a quantize job's and a default `convert` run's directories under
1240
+ `models/` are what the index, `serve <path|name>` and the folder picker read,
1241
+ that nothing lands in the hub cache, and that earlier `models--local--…`
1242
+ quantize snapshots there still resolve. Their tensor files are zeroed
1243
+ stand-ins; loading and generating from real artifacts is the opt-in
1244
+ managed-jobs and startup-adapter acceptance.
1245
+
1246
+ `publishing/credentials.ts` owns the app's `~/.mlx-bun/hf.json` token file (mode
1247
+ 0600). Resolution prefers the saved token, then `HF_TOKEN`, then the shared HF
1248
+ cache through the hub resolver. Explicit paths and environment isolate tests
1249
+ and embedded consumers. Settings responses expose presence only.
1250
+ `publishing/upload.ts` selects an explicit source or the supplied job's output
1251
+ and passes resolved credentials to the public hub uploader. HTTP shapes and
1252
+ errors belong to `server/publishing-routes.ts`; CLI composition supplies the
1253
+ read-only job lookup. Quantized models and adapters publish as model repos;
1254
+ datasets publish as dataset repos. Uploads need no model execution lease.
1255
+ [Publishing tests](tests/server/publishing-routes.test.ts) use temporary token
1256
+ storage and an injected uploader; they never read installed credentials or
1257
+ publish to Hugging Face.
1258
+
1259
+ The `upload` verb, main's `mlx_lm.upload` counterpart, is the models module's:
1260
+ `mlx-bun upload --path <dir> --upload-repo <org/repo> [--private]` (`--path`
1261
+ is required; there is no working-directory default). It publishes through the catalog
1262
+ (`publishing/catalog-hub.ts`, which resolves the token through
1263
+ `publishing/credentials.ts`), fails before any request when the repo id,
1264
+ directory, or write token is missing, and pushes a model repo through the same
1265
+ public hub uploader with the commit message "Upload with mlx-bun". SIGINT or
1266
+ SIGTERM aborts the transfer; nothing is committed after an abort. The
1267
+ [upload CLI tests](tests/upload-cli.test.ts) spawn the real CLI against a local mock Hub with an isolated
1268
+ home directory and an invented token; the module's own tests inject the catalog.
1269
+
1270
+ ## Web hub
1271
+
1272
+ The hub routes, the library and the Models panel are the models module's ([`@mlx-bun/module-models`](../../packages/module-models/README.md)),
1273
+ which this app installs in the persistent state (`placement: "app"`). `hub/downloads.ts` stays here: it owns web-started
1274
+ transfers, and `catalogTransfers` gives the catalog's `startDownload` and `downloads()` that owner. Admission
1275
+ is synchronous before the metadata request, so a duplicate submit answers 409;
1276
+ `GET /downloads` serves the owner's rows, one per transfer from admission (the
1277
+ panel shows "preparing…" until the listing and preflight finish) through the
1278
+ hub tracker's live row to done, or to an error when the listing fails or
1279
+ shutdown cancels it; completion re-indexes the catalog, which announces `catalog.changed` (the module's library rows and the
1280
+ host's model listing follow it); shutdown aborts and joins every transfer before the engine
1281
+ closes, leaving resumable partials and publishing nothing. Selecting a model
1282
+ (`POST /api/hub/serve`, the panel's Serve button) asks the model host to serve it (`ModelHost.serve`, which the serving host
1283
+ lends through its link): it loads beside the running models when it fits,
1284
+ otherwise the least recently used one is drained first (see the model host), and the
1285
+ answer is `{ ok: true, model }` (404 for an id that is not a local model, 502 when it
1286
+ cannot load). A host that serves one model answers `restart_required` with the
1287
+ `mlx-bun serve <id>` command.
1288
+
1289
+ ## Audio transcription
1290
+
1291
+ Speech-to-text is the transcription module's ([`@mlx-bun/module-transcription`](../../packages/module-transcription/README.md):
1292
+ the service, `/v1/audio/*`, `/admin/transcription/unload`, `transcribe`, `dictate`).
1293
+ This app installs it (`src/modules.ts`) and supplies its core services:
1294
+ The in-process composition shares the state's `modelHost` with persistent and
1295
+ model-scoped modules. It forwards generation to the serving host and borrows
1296
+ Whisper's transcription operations and residency; the Whisper host still owns
1297
+ its weights and shutdown. Model-scoped modules activate after this link is attached.
1298
+ `serve-host.ts` builds the Whisper model host from `--whisper-model`,
1299
+ `--whisper-idle-unload` and `--whisper-resident`, gives it the generation
1300
+ gateway's exclusive lock so decoding never overlaps chat, mounts the module's
1301
+ routes where the audio routes were, and stops the module and then releases the
1302
+ weights ahead of the chat model. `--preload` and serving a Whisper checkpoint as
1303
+ the main model start the transcription-only server (the audio routes plus
1304
+ `/v1`, `/v1/models`, `/health` and `/stats`). `GET /v1/models` (which the web chat's
1305
+ `ready.transcription` flag, for the hold-to-talk mic in the chat composer, reads) reports the model host's default Whisper. The [serve
1306
+ tests](tests/serve-cli.test.ts) cover the flags, both `runServe` branches and
1307
+ both compositions over a fake Whisper backend; the [voice
1308
+ test](tests/engine/voice.test.ts) and the opt-in [transcription
1309
+ test](tests/engine/transcription.test.ts) run real weights through this app, and the
1310
+ opt-in [transcription parity test](tests/engine/transcription-parity.test.ts) checks the
1311
+ served transcripts against the oracle.
1312
+
1313
+ ## Metrics and performance
1314
+
1315
+ The live view is the metrics module's ([`@mlx-bun/module-metrics`](../../packages/module-metrics/README.md):
1316
+ `/api/metrics/snapshot`, `/stream`, bench-serve runs and history). This app installs it and supplies what it
1317
+ subscribes to. `AppState.events` is the `events` bus; `startModelHost` measures the model load and
1318
+ `startContextHost` publishes it once the modules are active; `src/engine/telemetry.ts` wraps the completion engine
1319
+ (`engine.completion` is replaced by the observed one, so every route runs through it) to publish each request's
1320
+ queue wait, time to first token, prefill and decode rates and total time from the run's own stats, and samples the
1321
+ gateway and prompt cache every second (batch rows of capacity, queue depth, tokens per second, projected KV bytes
1322
+ against `--kv-budget`, prompt-cache bytes, hits and misses, per-model memory), quiet while idle and unchanged.
1323
+ Publishing only appends to bounded queues, so a slow subscriber never delays generation; nothing under the engine
1324
+ imports the bus. The module requires `jobs`, so it activates in the persistent state (`serve-state.ts`) with the state's bus;
1325
+ `jobs/service.ts` runs its `bench-serve` runner as a managed task (rows and logs in the job store, listed by
1326
+ `/api/jobs`), under the engine's execution lease because the runner declares `gpu: "exclusive"`. In the default isolated mode
1327
+ each worker's events are relayed to the parent's bus, so the view there covers every resident model. `/stats`, `/health` and `/fit` are unchanged. The status page loads the
1328
+ module's panel element while it is visible (`web/browser/status.ts`). The opt-in [real-weights
1329
+ test](tests/engine/metrics-native.test.ts) (`MLX_BUN_APP_TEST_MODEL=<snapshot directory>`) serves a model and requires
1330
+ the module's numbers to equal each response's `usage` and `/stats`.
1331
+
1332
+ ## Benchmarks
1333
+
1334
+ Answer-quality evaluation is the benchmarks module's ([`@mlx-bun/module-benchmarks`](../../packages/module-benchmarks/README.md):
1335
+ `/api/benchmarks/tasks`, `/runs`, `/compare`, the `eval-serve` job kind). It orchestrates `scripts/eval-serve.ts`
1336
+ (the capability evaluation runner) and scores nothing itself. This app installs it and supplies what it requires in the
1337
+ persistent state (`cli/serve-state.ts`): the `eval-serve` runner runs on the job host under the engine's execution lease
1338
+ (`gpu: "exclusive"`), `storage` resolves its `benchmarks/` entries under `MLX_BUN_HOME` (datasets in `benchmarks/data`,
1339
+ placed by the user, never downloaded), `modelHost` names the served model, and `catalog` (the registry-backed one
1340
+ over the model index, native-free like the rest of the state) resolves it to its directory.
1341
+ The runner then starts its own server from `bin/mlx-bun.mjs serve --model <dir> --port <free>` in a sandboxed `HOME`;
1342
+ the app's served model stays resident and idle. The web shell mounts the module's panel (a Benchmarks tab, from its manifest). The opt-in [real-weights test](tests/engine/benchmarks-native.test.ts)
1343
+ (`MLX_BUN_APP_TEST_MODEL=<snapshot directory> MLX_BUN_EVAL_DATA=<pinned datasets>`) runs `gsm8k-50` and `mmlu` through
1344
+ the module and requires its history to equal a direct `eval-serve.ts run` of the same plan.
1345
+
1346
+ ## Standalone bundle
1347
+
1348
+ After staging the root native setup and the microphone helper with
1349
+ `bun run --filter @mlx-bun/module-transcription build:native`, run `bun run build:binary` from the root.
1350
+ `dist/bundle/` contains the executable, native libraries/helpers, Pi's Photon
1351
+ WASM sidecar, the project license, and combined third-party notices: the MLX and
1352
+ inference package notices, Photon's installed Apache-2.0 license, the app's own
1353
+ [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md), and one section per npm package
1354
+ the build retained. [bundle-notices.ts](../../scripts/bundle-notices.ts) reads the
1355
+ metafiles of the executable and embedded browser builds: every input that
1356
+ contributed bytes maps to its nearest `package.json` `name@version`; workspace
1357
+ packages are excluded. A section copies the installed LICENSE, LICENCE, NOTICE and
1358
+ COPYING files verbatim. Packages that install none (Pi, XGrammar) must be named at
1359
+ that exact `name@version` in the app or package notices, which carry the upstream
1360
+ text with its source revision; otherwise the build fails. Reviewed vendored license
1361
+ headers are copied from the installed files. Curated sections of the app and
1362
+ inference notices carry the notices of code that Jiti's prebundle, XGrammar's WASM
1363
+ build and json-bigint's `lib/parse.js` embed; those packages' retained files are
1364
+ pinned by sha256, and each curated section by the sha256 of its text. Another
1365
+ version, a changed file, or a changed or missing section fails the build until
1366
+ reviewed. The notices still missing are listed under Release acceptance in PLAN.md.
1367
+ Move the whole directory together. Web assets and the memory skill
1368
+ are embedded; source checkouts retain their existing asset readers and browser
1369
+ build fallback. No terminal Pi assets are included.
1370
+
1371
+ `bun run verify:binary` builds into temporary storage, archives the bundle with
1372
+ the release preparation's tar command, and removes the build. The shared
1373
+ acceptance then extracts the archive to another directory, compiles a consumer
1374
+ into that copy, checks every notice section of a fresh build, and installs the
1375
+ unchanged archive through the curl installer using a local curl stub and
1376
+ temporary home, then upgrades once. It checks the actual CLI and managed child plus the consumer for
1377
+ web, memory, synthetic registry/fit, native path resolution, microphone helper
1378
+ help, and Photon initialization in the relocated, installed, and retained
1379
+ previous bundles.
1380
+ The default runs no model and makes no remote download. Each install ends with
1381
+ the installed executable's Neural Engine install step (`mlx-bun compile-ane`),
1382
+ which the check requires to run: it compiles this Mac's programs or reports none
1383
+ to compile, and where the Neural Engine cannot compile (a virtual machine) the
1384
+ installer warns and keeps the installation. Mac CI runs this check.
1385
+ With exclusive GPU access, `bun run verify:binary --model /path/to/cached-model`
1386
+ also starts the actual relocated executable, checks its web assets, `/stats`,
1387
+ `/fit`, and a chat completion, then requires a clean SIGTERM exit. It discovers
1388
+ the supplied weights through a temporary HF snapshot symlink and isolates the
1389
+ child's home, caches, and chat storage; it never copies or downloads weights.
1390
+ This is a local build artifact, not signing/notarization or release publishing.
1391
+
1392
+ [`scripts/install.sh`](../../scripts/install.sh) installs a complete release
1393
+ bundle under `${MLX_BUN_INSTALL_DIR:-$HOME/.mlx-bun}/app-install/` and links
1394
+ `~/.local/bin/mlx-bun` and, beside it, `mlx-bun.<cmd>` for each
1395
+ [mlx-lm alias](#mlx-lm-compatibility-mlx-buncmd) (each a link to the same
1396
+ executable; all are refused where a directory stands and switch with the app).
1397
+ `MLX_BUN_VERSION` selects `latest` or a pinned tag.
1398
+ The installer validates the files and version before switching its `current`
1399
+ symlink and retains the old app on failure. After switching it runs the
1400
+ installed `mlx-bun compile-ane` (the Neural Engine install step; a failure warns
1401
+ and keeps the installation). Successful updates keep the current
1402
+ and immediate previous bundles, plus any older bundle used by a running app.
1403
+ Other older owned bundles and stale stages are removed on the next install;
1404
+ vnode inspection keeps bundles used by apps launched through PATH or symlinks,
1405
+ and an inconclusive inspection retains the affected bundle. The CLI captures
1406
+ its executable identity at startup, so managed jobs launched after an upgrade
1407
+ still use that running app's original build. A later install prunes old bundles
1408
+ after their processes exit.
1409
+ Sessions, wiki, credentials, and legacy flat installation files
1410
+ outside `app-install/` stay intact. A custom `MLX_BUN_INSTALL_DIR` relocates only
1411
+ the installed bundle; application data still lives under `~/.mlx-bun`.
1412
+ A concurrent install is blocked by `app-install/lock`; after an interrupted
1413
+ installer, remove the reported absolute lock directory only after confirming
1414
+ its recorded PID is no longer an installer. Run the script with `--help` for usage.
1415
+ The public installer must not deploy before a compatible release bundle exists;
1416
+ older release archives lack the newly required license and notice files.
1417
+
1418
+ `bun scripts/prepare-homebrew.ts /path/to/mlx-bun-v<version>-arm64.tar.gz`
1419
+ prepares a local `mlx-bun.rb` beside the archive, with its version, release URL,
1420
+ and SHA256. The formula installs the entire bundle in `libexec` and symlinks its
1421
+ command into `bin`. Preparation checks archive members; release verification
1422
+ must additionally check that its binary version matches the archive filename.
1423
+ Preparation does not install, sign, notarize, publish, or
1424
+ update the tap. [Installer tests](tests/install.test.ts) use local archives and
1425
+ temporary homes, including reinstall, failure, and every refusal path (usage,
1426
+ tag, platform, archive layout, executable and version, command destination,
1427
+ app-root and `current` ownership), without network access.
1428
+
1429
+ ## Release preparation
1430
+
1431
+ `bun scripts/prepare-release.ts prepare /new/output/directory` builds from staged
1432
+ natives, checks the executable against the app manifest version, packs workspace
1433
+ packages, and writes unsigned local archives, checksums, and a Homebrew formula.
1434
+ It then runs the model-free `verify:binary` acceptance on the unsigned archive
1435
+ and requires unchanged bundle hashes afterwards; a failure removes `unsigned/`
1436
+ and writes no `preparation.json`.
1437
+ `preparation.json` records bundle hashes, dependency-first package publication
1438
+ order, package archive hashes, source revision, and pending private/version decisions. Private
1439
+ example apps are excluded; an unpublished private main app still requires a decision. Nothing changes those decisions or
1440
+ publishes. The `unsigned/` output is for local verification only.
1441
+
1442
+ The same script has explicit `sign <directory> <Developer-ID-identity>`,
1443
+ `notarize <directory> <keychain-profile>`, and `package <directory>` stages.
1444
+ Signing handles every nested Mach-O before the executable, applies the preserved
1445
+ Bun JIT/library-validation entitlements, verifies each signature, and checks
1446
+ launch/version. It signs a temporary bundle copy and only replaces the original
1447
+ after every check succeeds, so a partial failure can be retried. Notarization submits to Apple and requires JSON status `Accepted`;
1448
+ an exit code of zero alone is insufficient. Packaging rejects changed bundle
1449
+ bytes and missing accepted evidence, then writes the versioned and stable arm64
1450
+ tarballs, matching checksum sidecars, and formula under `release/`.
1451
+
1452
+ Run `--help` for usage. Signing/notarization and publishing require Josh's release
1453
+ instruction; preparation and tests do not access identities or Apple services.
1454
+ The release entrypoints are ported from main: `scripts/release-binary.sh` builds,
1455
+ signs, notarizes and packages; `scripts/publish-release.sh` publishes GitHub assets,
1456
+ updates the tap, then publishes npm. `PUBLISH=1 ./scripts/release-binary.sh` runs
1457
+ that publication flow. After it completes, deploy the website and installer through
1458
+ the [manual Pages workflow](../website/README.md); its release tag must be GitHub's
1459
+ latest release and match the published application version.
1460
+ `BUILD_DIR` names the preparation directory, `OUT_DIR` holds
1461
+ the distributable archives, and `RELEASE_NOTES` names the reviewed release body.
1462
+ The publisher requires a clean checkout at `origin/main` matching the prepared
1463
+ source. Its npm step extends main's idempotent publication to all public workspace
1464
+ packages in dependency order, requiring matching archive integrity for an existing
1465
+ version. The formula is generated from the accepted archive, so no checked-in
1466
+ version/hash mirror needs another commit. `bun scripts/release-inputs.ts inspect
1467
+ <directory> <release-notes>` inspects artifacts without publishing. Parity and
1468
+ performance qualification belong to refactor acceptance; this flow does not run
1469
+ them. No preparation stage invokes publication. [Release tests](tests/release.test.ts)
1470
+ use captured mock signing/notary commands and a recording acceptance stand-in;
1471
+ they do not prove a real signature or Apple acceptance.
1472
+
1473
+ The Quantize (`/quantize`), Fine-tune (`/finetune`) and Build Dataset (`/dataset`) pages are the installed
1474
+ modules' self-contained panels. They preserve their wizard steps, job streams and publishing controls;
1475
+ leaving a page closes its stream, while returning to an unfinished job replays its stored progress.