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/src/server.ts DELETED
@@ -1,1314 +0,0 @@
1
- import { ContinuationPersistence } from "./backends/mlx/continuation-persistence";
2
- import { modelServingBinding } from "./backends/mlx/model-serving";
3
- import type { ServingContext } from "./serve/model-host";
4
- export { generationCheckpointKey } from "./serve/checkpoint-identity";
5
- // OpenAI-compatible HTTP server: /v1/chat/completions (+ SSE streaming)
6
- // and /v1/models. Phase 4 core — tool calling, vision, and the
7
- // byte-capped prompt cache land on top of this.
8
- //
9
- // Original code; behavioral reference: mlx-lm server.py (MIT) for the
10
- // protocol surface and stop/finish semantics. No code ported — Bun.serve
11
- // and the generation queue are structurally different.
12
- //
13
- // Generation is serialized through a single queue (one GPU, batch=1).
14
-
15
- import type { Server } from "bun";
16
- import { STATIC_ROUTE_ASSETS } from "./web-assets";
17
- import { readFileSync } from "node:fs";
18
- import { type TurboQuantScheme } from "./config";
19
- import {
20
- GLM52_G5_ASPIRATIONAL_DECODE_TPS,
21
- GLM52_G5_DIRECT_ORACLE_WARM_DECODE_TPS,
22
- GLM52_G5_MEASURED_AT,
23
- GLM52_G5_MEASURED_WARM_DECODE_TPS,
24
- } from "./model/glm52-memory";
25
- import {
26
- type GenerateOptions,
27
- } from "./generate";
28
- import { createPreparationExecutor } from "./serve/preparation";
29
- import { cloneKvCaches, legacyCacheCodecs, SpillQueue } from "./kv-store";
30
- import type { Cache } from "./model/gemma4";
31
- import { resolveKvScheme } from "./kv-scheme";
32
- import { runtimeValue } from "./runtime-config";
33
- import { TURBOQUANT_HEAD_DIMS } from "./mlx/turboquant-tables";
34
- import type { HlgConfig } from "./sampler";
35
- import { isMonotone, CURVE_UMIN, type CurveParams } from "./lab/curve/curve-sampler";
36
- import { GenerationGateway, disposeUnstartedRequest } from "./serve/generation-gateway";
37
- import { createSessionCompletionEngine } from "./serve/session-completion-engine";
38
- import {
39
- CompletionExecutor,
40
- } from "./serve/completion-executor";
41
- import {
42
- createPromptResponseTrace,
43
- type PromptResponseTrace,
44
- } from "./serve/prompt-response-trace";
45
- import { handleAdminRoute } from "./serve/admin-routes";
46
- import { handleAuxiliaryRoute } from "./serve/aux-routes";
47
- import { createDiscoveryRoutes } from "./serve/discovery-routes";
48
- import { handleLabRoute } from "./serve/lab-routes";
49
- import { handleModelAdminRoute } from "./serve/model-admin-routes";
50
- import { handleStaticRoute } from "./serve/static-routes";
51
-
52
- import { PromptCache, cacheBytes } from "./prompt-cache";
53
- import { SsdCacheStore } from "./ssd-cache";
54
- import {
55
- SsdDurabilityCoordinator,
56
- type DurabilityFlushResult,
57
- type DurabilitySnapshotStats,
58
- } from "./ssd-durability";
59
- import { configFingerprint } from "./model/fingerprint";
60
- import {
61
- anthropicToChatBody, chatJsonToAnthropic, createAnthropicStreamProtocol,
62
- type AnthropicRequest,
63
- } from "./anthropic";
64
- import {
65
- ResponseStore, chatJsonToResponses, resolveResponsesConversation,
66
- responsesToChatBody, createResponsesStreamProtocol,
67
- type ResponsesRequest,
68
- } from "./responses";
69
- import { fit } from "./fit";
70
- import { setMemoryLimit } from "./mlx/ffi";
71
- import { makePiWsHandler, type PiWsData } from "./pi-web";
72
- import { ChatStage } from "./serve/chat-stage";
73
- import { TextCompletionStage } from "./serve/text-completion-stage";
74
- import { InferenceStage } from "./serve/inference-request";
75
- import { admit, respondJson, respondStream, type ErrorFormatter } from "./serve/http";
76
- import {
77
- chatCompletionJson, chatCompletionStream, textCompletionJson, textCompletionStream,
78
- } from "./serve/openai-wire";
79
- import { createRequestPrep } from "./serve/request-prep";
80
- import {
81
- detectDraftKind,
82
- loadContext,
83
- type DraftKind,
84
- type GenSamplingDefaults,
85
- type LoadContextOptions,
86
- type ServerContext,
87
- } from "./serve/model-host";
88
- import {
89
- ChatRequest,
90
- TextCompletionRequest,
91
- type ChatRequestParams,
92
- type TextCompletionParams,
93
- } from "./serve/chat-request";
94
-
95
- // The serving facade: createServer's companion loaders stay importable from
96
- // here (public library API via src/index.ts; bench scripts; parity tests).
97
- export {
98
- detectDraftKind,
99
- loadContext,
100
- type DraftKind,
101
- type GenSamplingDefaults,
102
- type LoadContextOptions,
103
- type ServerContext,
104
- };
105
-
106
- export interface ServerOptions {
107
- /** Byte cap for the prompt (KV) cache. Default 8 GB. */
108
- promptCacheBytes?: number;
109
- /** Aggregate KV-byte budget across concurrently-admitted batch rows
110
- * (`--kv-budget`, batching-perf-path P3). Joiners whose projected KV
111
- * (prompt + max_tokens, window-capped) would exceed it QUEUE until rows
112
- * evict; a request over the budget alone is rejected. Unset = no
113
- * aggregate cap (an explicit memoryBudget also limits each request). */
114
- kvBudgetBytes?: number;
115
- /** KV quantization override. Unset/"off" is bf16. "config" applies the
116
- * model's declared mixed-precision kv_config; supported per-layer schemes
117
- * compose with continuous scheduling. A number forces uniform bits
118
- * (group size 64, start 0) and uses the preserved serial executor. */
119
- kvQuant?: "off" | "config" | number;
120
- /** Optional absolute row offset for KV conversion; omitted preserves start-zero server policy. */
121
- quantizedKvStart?: number;
122
- /** TurboQuant scheme (docs/design/turboquant.md): a separate axis from
123
- * kvQuant above, mutually exclusive with it (`--kv-quant turbo[:k<bits>v
124
- * <bits>]` sets this instead of kvQuant). Solo-only in v1 — see
125
- * GenerationGateway's explicit refusal. */
126
- turboQuant?: TurboQuantScheme;
127
- /** OPTIONAL paged KV cache (`--paged-kv`, docs/design/kv-cache.md):
128
- * vLLM-style block-pool storage for full-attention layers,
129
- * gather-to-contiguous before the stock SDPA. Default off (unset = the
130
- * plain KVCache path, byte-identical). Gemma4-family, bf16, B1/B>1;
131
- * startup refuses `--kv-quant`,
132
- * turbo, and `--draft-model` combinations; paged requests bypass the
133
- * prompt cache and run uncompiled. Gated bit-exact vs the plain path. */
134
- pagedKv?: { blockSize?: number };
135
- /** Explicit opt-in process budget. Enforces the fit estimate's context
136
- * limit and sets the MLX allocator limit. Unset keeps estimates advisory. */
137
- memoryBudgetBytes?: number;
138
- /** Who owns this server's lifetime: "serve" (persistent, mlx-bun
139
- * serve) or "pi-session" (dies with the pi session that started
140
- * it). Exposed at /stats.server so other mlx-bun processes can
141
- * warn before attaching to a server that may vanish. */
142
- owner?: "serve" | "pi-session" | "embedded";
143
- /** Listen on a UNIX DOMAIN SOCKET instead of a TCP port — the engine-
144
- * child mode of the isolation architecture (docs/reference/server-config.md): the
145
- * parent process reverse-proxies HTTP to this socket. Stale socket
146
- * files are unlinked before bind. When set, `hostname`/port are ignored. */
147
- unixSocket?: string;
148
- /** Interface to bind (Bun.serve hostname). Unset ⇒ Bun's default
149
- * (all interfaces) — embedded/library use. The CLI always passes one:
150
- * "127.0.0.1" (loopback, mlx_lm.server parity) unless --host says
151
- * otherwise. */
152
- hostname?: string;
153
- /** Server-wide default for the chat template's `enable_thinking`
154
- * variable (MiniCPM5/CPM and other hybrid-reasoning models). A
155
- * request's `chat_template_kwargs.enable_thinking` overrides it per
156
- * call; undefined ⇒ fall back to the model's own default (false for
157
- * MiniCPM5). Set via `--thinking true|false`. */
158
- defaultThinking?: boolean;
159
- /** Server-wide sampling defaults (set via --temperature/--top-p/--top-k).
160
- * Precedence: an explicit per-request field wins, then these, then the
161
- * model's generation_config.json, then the built-in fallback. Lets the
162
- * browser chat (which sends no sampling fields) be steered from the CLI. */
163
- defaultTemperature?: number;
164
- defaultTopP?: number;
165
- defaultTopK?: number;
166
- /** Completion cap when the request omits max_tokens (`--max-tokens`).
167
- * mlx_lm.server's flag; its default there is 512 — ours stays 65,536 so
168
- * thinking traces never truncate. `--max-tokens 512` = mlx-lm behavior. */
169
- defaultMaxTokens?: number;
170
- /** Continuous-scheduler concurrency cap (`--batch N`). Default 8; the
171
- * scheduler specializes active B=1 and B=N with mlx-lm parity at the same
172
- * composition. `--batch 1` pins the preserved strict serial executor. */
173
- batch?: number;
174
- /** HLG tone-curve sampling default (set via --hlg-sampling on + sub-knobs).
175
- * A per-request `hlg` object overrides it field-by-field. Off when unset. */
176
- hlg?: HlgConfig;
177
- /** Adapter id mounted at startup via `serve --adapter <dir>` (mlx-lm's
178
- * `--adapter-path`). Used when a request sends no `adapter` field; a
179
- * request's explicit `adapter` (including "none") always wins. Hot-swap
180
- * via /v1/adapters is unchanged. */
181
- defaultAdapter?: string;
182
- /** SSD cold tier for the prompt/KV cache (`--ssd-cache <dir>`): prefix
183
- * KV survives RAM eviction AND server restarts — the coding-agent
184
- * long-context TTFT win (docs/design/kv-cache.md). Off unless
185
- * a directory is given. Serial lane only (where the prompt cache lives). */
186
- ssdCacheDir?: string;
187
- /** Optional byte cap for the SSD tier (default unbounded). */
188
- ssdCacheMaxBytes?: number;
189
- /** Idle-demotion threshold in seconds (`--ssd-demote-idle`): prompt-cache
190
- * entries unused this long spill to the SSD tier and free their GPU
191
- * memory (Layer 0 — RAM drains between bursts, prefixes stay reachable
192
- * via zero-copy restore). Default 300 when the tier is on; 0 disables. */
193
- ssdDemoteIdleSec?: number;
194
- /** Verify every tensor hash on restore (`--ssd-cache-verify`) — reads all
195
- * bytes eagerly, defeating lazy fault-in; integrity paranoia only. */
196
- ssdCacheVerify?: boolean;
197
- /** Periodically persist an in-flight serial generation every N emitted
198
- * tokens. An identical request after restart replays the saved assistant
199
- * prefix and continues from the already-sampled next token. Requires the
200
- * SSD cache and --batch 1. */
201
- generationCheckpointTokens?: number;
202
- }
203
-
204
- interface ServerLifecycle {
205
- stopJobs: () => Promise<void>;
206
- close: () => Promise<void>;
207
- flush: () => Promise<DurabilityFlushResult>;
208
- stats: () => DurabilitySnapshotStats;
209
- stopTimers: () => void;
210
- }
211
-
212
- export interface ServerShutdownResult {
213
- stopped: boolean;
214
- timedOut: boolean;
215
- durability: DurabilityFlushResult;
216
- }
217
-
218
- const serverLifecycles = new WeakMap<Server<unknown>, ServerLifecycle>();
219
-
220
- function emptyDurabilityResult(): DurabilityFlushResult {
221
- return {
222
- durable: true,
223
- flushedSnapshots: 0,
224
- missingSnapshots: 0,
225
- pendingSnapshots: 0,
226
- pendingSpills: 0,
227
- pendingSpillBytes: 0,
228
- droppedSpills: 0,
229
- failedSpills: 0,
230
- elapsedMs: 0,
231
- };
232
- }
233
-
234
- /** Flush prompt-cache snapshots without stopping the HTTP server. */
235
- export function flushServerCacheDurability(
236
- server: Server<unknown>,
237
- ): Promise<DurabilityFlushResult> {
238
- return serverLifecycles.get(server)?.flush() ?? Promise.resolve(emptyDurabilityResult());
239
- }
240
-
241
- /** Stop admission, flush cache durability, then finish the Bun server. */
242
- export async function shutdownServer(
243
- server: Server<unknown>, timeoutMs = 120_000,
244
- ): Promise<ServerShutdownResult> {
245
- const lifecycle = serverLifecycles.get(server);
246
- lifecycle?.stopTimers();
247
- const stopped = Promise.resolve(server.stop(false));
248
- const work = (async (): Promise<ServerShutdownResult> => {
249
- await lifecycle?.stopJobs();
250
- await stopped;
251
- const durability = lifecycle ? await lifecycle.flush() : emptyDurabilityResult();
252
- await lifecycle?.close();
253
- return { stopped: true, timedOut: false, durability };
254
- })();
255
- let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
256
- const timeout = new Promise<ServerShutdownResult>((resolve) => {
257
- timeoutHandle = setTimeout(() => {
258
- const stats = lifecycle?.stats();
259
- resolve({
260
- stopped: false,
261
- timedOut: true,
262
- durability: {
263
- ...emptyDurabilityResult(),
264
- durable: false,
265
- ...(stats ?? {}),
266
- elapsedMs: timeoutMs,
267
- },
268
- });
269
- }, timeoutMs);
270
- timeoutHandle.unref?.();
271
- });
272
- try {
273
- return await Promise.race([work, timeout]);
274
- } finally {
275
- if (timeoutHandle) clearTimeout(timeoutHandle);
276
- }
277
- }
278
-
279
-
280
- export interface ContextAdmissionDecision {
281
- /** Effective completion ceiling after admission. */
282
- maxTokens: number;
283
- /** True when admission reduced the client's upper bound to fit. */
284
- clamped: boolean;
285
- }
286
-
287
- /** Resolve a completion cap against an explicitly enforced context limit.
288
- * Serving keeps fit estimates advisory unless the operator chooses a budget. */
289
- export function admitRequestContext(
290
- promptTokens: number,
291
- requestedMaxTokens: number,
292
- maxSafeContext: number,
293
- ): ContextAdmissionDecision | null {
294
- const available = maxSafeContext - promptTokens;
295
- if (available < 1) return null;
296
- if (requestedMaxTokens <= available)
297
- return { maxTokens: requestedMaxTokens, clamped: false };
298
- return { maxTokens: available, clamped: true };
299
- }
300
-
301
-
302
-
303
- // v2 curve designer: CORS-open so a file:// editor can call a localhost engine.
304
- const CURVE_CORS: Record<string, string> = {
305
- "access-control-allow-origin": "*",
306
- "access-control-allow-methods": "POST, OPTIONS",
307
- "access-control-allow-headers": "content-type",
308
- };
309
- /** coherence flag: non-Latin-letter ratio / replacement char / reserved token. */
310
- function curveJunk(s: string): boolean {
311
- const L = s.match(/\p{L}/gu) ?? [];
312
- const nonLatin = L.length ? L.filter((c) => !/\p{Script=Latin}/u.test(c)).length / L.length : 0;
313
- return nonLatin >= 0.02 || /�/.test(s) || /<unused\d+>/.test(s);
314
- }
315
-
316
-
317
- export function createServer(
318
- ctx: ServingContext, port = 0, serverOptions: ServerOptions = {},
319
- ): Server<unknown> {
320
- // --batch N is a concurrency cap: N===1 pins the strict serial executor;
321
- // N>1 admits supported execution compositions to the continuous scheduler.
322
- // The scheduler chooses its B=1 fast path or B=N step from active rows.
323
- // Both full-attention (CPM) and
324
- // sliding-window (Gemma) models batch — the scheduler assembles each layer's
325
- // cache by attention type. Non-batchable requests (vision / adapters /
326
- // unsupported explicit kv-quant) drain to the serial executor
327
- // (see GenerationGateway.place). No inference setting is rewritten.
328
- // DEFAULT 8 (flipped 2026-07-05, Josh's call, after GATE-B1-SPEED): a
329
- // lone request through the batch lane IS the serial engine (adopted
330
- // serial-class caches, compiled decode, prompt cache + SSD restore;
331
- // 0.992-0.996 paired decode ratios, byte-identical output), so the cap
332
- // only changes behavior when concurrent requests actually arrive — the
333
- // agentic sub-agent workload. --batch 1 pins strict serial for
334
- // arrival-independent numerics. 8 = optiq's Mac-safe concurrency.
335
- const serving = modelServingBinding(ctx);
336
- ctx = { ...ctx, serving };
337
- const batch = Math.max(1, Math.floor(serverOptions.batch ?? 8));
338
- if (serverOptions.generationCheckpointTokens !== undefined) {
339
- if (!Number.isInteger(serverOptions.generationCheckpointTokens) ||
340
- serverOptions.generationCheckpointTokens < 1)
341
- throw new Error("--generation-checkpoint expects a positive integer token interval");
342
- if (!serverOptions.ssdCacheDir)
343
- throw new Error("--generation-checkpoint requires --ssd-cache <dir>");
344
-
345
- }
346
- const defaultGeneratedTokens =
347
- serverOptions.defaultMaxTokens ?? ctx.glmMemoryPlan?.maxGenerationTokens;
348
-
349
- // KV-quant scheme, resolved once. UNSET now means bf16 (flipped
350
- // 2026-07-05 with the naked-=-L1 default): quantized KV measured 5–20%
351
- // SLOWER decode than bf16 at ≤16k on every model — on mlx-lm too (its
352
- // kv8 oracle trails its own bf16) — so it pays only in memory headroom
353
- // and must be an explicit opt-in (--kv-quant config|4|8, or --l2
354
- // whose presets pass it explicitly). The CLI always passes kvQuant now;
355
- // this fallback is the library-user default and matches the CLI's.
356
- // Mutually exclusive by contract (GenerateOptions.turboQuant doc): a
357
- // programmatic caller setting both gets turboQuant — say so, like the
358
- // other risky-combination warnings below.
359
- if (serverOptions.turboQuant && serverOptions.kvQuant && serverOptions.kvQuant !== "off")
360
- console.warn(
361
- `[kv-quant] both turboQuant and --kv-quant ${serverOptions.kvQuant} are set; ` +
362
- `turboQuant wins (they are mutually exclusive).`,
363
- );
364
- const resolvedKvScheme = resolveKvScheme({
365
- override: serverOptions.kvQuant,
366
- turboQuant: serverOptions.turboQuant,
367
- quantizedKvStart: serverOptions.quantizedKvStart,
368
- config: ctx.kvConfig,
369
- });
370
- const kvScheme = resolvedKvScheme.generationOptions;
371
- // The model execution plan qualifies the method while retaining KV policy.
372
- if (ctx.draft && (kvScheme.turboQuant || kvScheme.kvBits || kvScheme.kvConfig?.length))
373
- console.warn(
374
- `[spec] --draft-model with quantized KV (--kv-quant ${serverOptions.turboQuant ? "turbo" : String(serverOptions.kvQuant)}): ` +
375
- `requests keep the KV scheme; the model execution plan enables speculation ` +
376
- `only for a qualified combination. Check response usage for the active method. ` +
377
- `(docs/design/speculative-decoding.md Phase 4)`,
378
- );
379
- // TurboQuant head-dim fail-fast (2026-07-07 review): the cache class only
380
- // supports {64,128,256,512} (sign-vector + Lloyd-Max table coverage) and
381
- // used to validate LAZILY on the first append — an unsupported model
382
- // (e.g. a 72/80/96 head dim) accepted --kv-quant turbo at startup and
383
- // then 500'd EVERY request from inside prefill. The config knows the
384
- // full-attention head dim (the only kind that converts; sliding layers
385
- // stay bf16) — refuse at createServer instead.
386
- if (kvScheme.turboQuant) {
387
- const dim = ctx.model.config.text.globalHeadDim;
388
- if (!(TURBOQUANT_HEAD_DIMS as readonly number[]).includes(dim))
389
- throw new Error(
390
- `--kv-quant turbo: this model's full-attention head_dim is ${dim}; ` +
391
- `TurboQuant supports {${TURBOQUANT_HEAD_DIMS.join(",")}} ` +
392
- `(docs/design/turboquant.md) — use --kv-quant config|4|8 or omit it`,
393
- );
394
- }
395
- // Paged KV v1 (docs/design/kv-cache.md): explicit refusals, not
396
- // silent downgrades — the incompatible combos would otherwise degrade
397
- // quietly (kv-quant: the swap would drop the scheme; speculative
398
- // provider composition with paged storage is not implemented).
399
- if (serverOptions.pagedKv) {
400
- if (kvScheme.kvBits || kvScheme.kvConfig?.length || kvScheme.turboQuant)
401
- throw new Error(
402
- `--paged-kv is bf16-only in v1 — omit --kv-quant (quantized paged ` +
403
- `blocks are a documented follow-up, docs/design/kv-cache.md).`,
404
- );
405
- if (ctx.draft)
406
- throw new Error(
407
- `--paged-kv cannot combine with --draft-model in v1 ` +
408
- `(docs/design/kv-cache.md non-goals).`,
409
- );
410
- if (!ctx.model.config.modelType.startsWith("gemma4"))
411
- throw new Error(
412
- `--paged-kv v1 supports Gemma4-family models only ` +
413
- `(this model: ${ctx.model.config.modelType}) — docs/design/kv-cache.md.`,
414
- );
415
- const bs = serverOptions.pagedKv.blockSize;
416
- if (bs !== undefined && (!Number.isInteger(bs) || bs <= 0))
417
- throw new Error(`--paged-kv-block-size must be a positive integer (got ${bs})`);
418
- if (serverOptions.ssdCacheDir)
419
- console.warn(
420
- "[paged-kv] --ssd-cache has no effect: paged requests bypass the prompt " +
421
- "cache (v1 non-goal), so nothing reaches the SSD tier.",
422
- );
423
- }
424
-
425
- // SSD cold tier (docs/design/kv-cache.md): prefix KV survives RAM
426
- // eviction and restarts. Compatibility key = configFingerprint (graph
427
- // shape) + the EFFECTIVE kv scheme (flags pick bf16 vs config vs uniform
428
- // on the same model — restored caches must match what serving produces) +
429
- // tokenizer hash (ids must keep meaning the same text). The model binding
430
- // also identifies the runtime numerics that produced the persisted state.
431
- // RAM prompt-cache cap: 8 GB default (Josh's call, 2026-07-06). The old
432
- // flat 2e9 was an anti-OOM reflex sized for a 1B model in 24 GB — a
433
- // single full-context 12B entry is ~0.6 GB, so it silently shrank to
434
- // "three entries" on big models. 8 GB holds a dozen 12B contexts; idle
435
- // entries still demote to SSD and LRU eviction bounds pressure.
436
- // --prompt-cache <GB> overrides; 0 disables.
437
- const promptCacheCap = serverOptions.promptCacheBytes ?? 8e9;
438
- const stateCodecs = ctx.stateCodecs ?? legacyCacheCodecs;
439
- const cloneState = (caches: Cache[]) => cloneKvCaches(caches, stateCodecs);
440
- let ssdStore: SsdCacheStore | null = null;
441
- if (serverOptions.ssdCacheDir) {
442
- if (promptCacheCap <= 0)
443
- throw new Error("--ssd-cache requires the RAM prompt cache (--prompt-cache 0 disables it)");
444
- if (typeof serving.stateCompatibility !== "string" || !serving.stateCompatibility.length)
445
- throw new Error("--ssd-cache requires the model binding's stateCompatibility identity");
446
- const schemeKey = resolvedKvScheme.cacheKey;
447
- const stateKey = Bun.hash(serving.stateCompatibility).toString(16);
448
- const tokJson = readFileSync(`${ctx.model.config.modelDir}/tokenizer.json`);
449
- ssdStore = new SsdCacheStore({
450
- codecs: stateCodecs,
451
- dir: serverOptions.ssdCacheDir,
452
- maxBytes: serverOptions.ssdCacheMaxBytes ?? Number.POSITIVE_INFINITY,
453
- configFingerprint: `${configFingerprint(ctx.model.config)}-${schemeKey}-${stateKey}`,
454
- tokenizerHash: Bun.hash(tokJson).toString(16),
455
- modelId: ctx.modelId,
456
- verify: serverOptions.ssdCacheVerify,
457
- });
458
- const recovered = ssdStore.scan();
459
- const capacity = Number.isFinite(ssdStore.maxBytes)
460
- ? `${(ssdStore.maxBytes / 2 ** 30).toFixed(0)} GiB cap`
461
- : "unlimited";
462
- console.log(
463
- `[ssd-cache] ${serverOptions.ssdCacheDir} — ${recovered} entr${recovered === 1 ? "y" : "ies"} recovered, ` +
464
- `${(ssdStore.totalBytes / 2 ** 30).toFixed(2)} GiB, ${capacity}`,
465
- );
466
- }
467
-
468
- // Layer 0 (unified-engine plan): the cold tier is bound INTO the prompt
469
- // cache — take() itself tiers over it, so the batch scheduler and every
470
- // other consumer get SSD restores through the same API; eviction AND
471
- // idle demotion spill into it.
472
- const coldTier = ssdStore
473
- ? {
474
- find: (prompt: number[], ns: string) => {
475
- const h = ssdStore!.find(prompt, ns);
476
- return h ? { prefixLen: h.prefixLen, handle: h.entry } : null;
477
- },
478
- restore: (handle: unknown) => {
479
- const loaded = serving.restore(ssdStore!, handle as import("./ssd-cache").SsdIndexEntry);
480
- if (!loaded) return null;
481
- // Restore is a STREAMED COPY (2026-07-07 A7-restore): the caches
482
- // own their bytes and no mapping outlives loadKvCache — nothing
483
- // to pin, nothing to unmap. retain stays in the entry contract
484
- // as a no-op so callers' dispose ordering is unchanged.
485
- return { tokens: loaded.tokens, caches: loaded.caches, attachments: loaded.attachments, retain: () => {} };
486
- },
487
- store: (tokens: number[], caches: import("./model/gemma4").Cache[], ns: string,
488
- attachments?: import("./backends/mlx/checkpoint-state").CheckpointAttachment[]) => {
489
- if (ssdStore!.hasDurablePrefix(tokens, ns)) return true;
490
- return ssdStore!.store(tokens, caches, ns, attachments);
491
- },
492
- }
493
- : null;
494
- const promptCache = new PromptCache(
495
- promptCacheCap,
496
- // RAM eviction AND idle demotion spill to the cold tier NON-BLOCKING
497
- // (2026-07-06, same contract as the write-behind snapshot below): the
498
- // cache hands us OWNED zero-copy clones + copied tokens (made under
499
- // the generation lock — microseconds; entries are immutable so the
500
- // clones stay consistent), and the flush goes through the BOUNDED
501
- // SpillQueue -> storeAsync (generation-locked per tensor, see below) ->
502
- // clone disposal on every settle/drop path (that dispose is what
503
- // actually frees the demoted GPU memory; the queue's byte cap keeps
504
- // starved-gate retention bounded — 2026-07-07 review fix). spillQueue
505
- // is declared with the snapshot scheduler below; safe to close over
506
- // here because spills only fire at put()/demoteIdle time, long after
507
- // init.
508
- ssdStore
509
- ? {
510
- spillOwned: (entry) =>
511
- spillQueue!.enqueue({ tokens: entry.tokens, caches: entry.caches, ns: entry.ns, attachments: entry.attachments }),
512
- }
513
- : null,
514
- coldTier,
515
- cloneState,
516
- );
517
- // Responses-API store for previous_response_id resumption (Phase 11):
518
- // TTL + byte-capped LRU, port of optiq/response_store.py. Pairs with
519
- // the prompt cache: a resumed conversation re-renders the same prefix,
520
- // so its KV prefill is already cached.
521
- const responseStore = new ResponseStore();
522
-
523
- // Memory estimates remain available in diagnostics. Only an explicit
524
- // operator budget or context cap constrains request planning.
525
- const memoryAdmission = ctx.glmMemoryPlan ?? fit(
526
- ctx.model.config, ctx.model.weightsBytes, 1,
527
- undefined, undefined, 0, serverOptions.memoryBudgetBytes,
528
- resolvedKvScheme.fitOptions,
529
- );
530
- const profileContext = runtimeValue("MLX_BUN_RD_CONTEXT_LIMIT");
531
- const profileLimit = profileContext === undefined ? null : Number(profileContext);
532
- if (profileLimit !== null && (!Number.isSafeInteger(profileLimit) || profileLimit < 1))
533
- throw new Error("MLX_BUN_RD_CONTEXT_LIMIT must be a positive integer");
534
- const budgetLimit = serverOptions.memoryBudgetBytes !== undefined
535
- ? memoryAdmission.maxSafeContext : ctx.glmMemoryPlan?.contextTokens ?? null;
536
- const contextLimit = profileLimit === null ? budgetLimit
537
- : Math.min(profileLimit, budgetLimit ?? Infinity);
538
- const admission = memoryAdmission;
539
- const allocatorLimit =
540
- admission.allocatorLimitBytes ?? serverOptions.memoryBudgetBytes;
541
- if (allocatorLimit) setMemoryLimit(allocatorLimit);
542
-
543
- let checkpointPersistence: ContinuationPersistence | undefined;
544
- const executionServices: import("./backends/mlx/serial-executor").MlxSerialServices = {
545
- get checkpointPersistence() { return checkpointPersistence; },
546
- promptCache, checkpoints: ssdStore,
547
- checkpointEveryTokens: serverOptions.generationCheckpointTokens,
548
- identity: { artifact: ctx.profile.artifact, implementation: ctx.profile.profile.execution,
549
- stateAbi: "legacy-cache-array-v1", codecs: stateCodecs.id },
550
- adapterNamespace: (adapters) => ctx.adapters.cacheNamespace(adapters),
551
- cloneState,
552
- };
553
- serving.gateway.configureContinuation?.(executionServices);
554
- const runGeneration = serving.createSerial(executionServices);
555
-
556
- // Write-behind persistence (restart survival — the oMLX boundary-snapshot
557
- // idea at whole-entry granularity, spill-on-evict alone can't survive a
558
- // clean exit): after a request completes, snapshot its still-RAM-resident
559
- // entry to SSD off the request path. Debounced + coalesced per namespace —
560
- // an agent hammering one conversation persists the settled state once
561
- // things go quiet, not every turn. Runs under the gateway lock (byte
562
- // extraction must not race a generation mutating the entry); if the entry
563
- // was evicted (already spilled) or extended (a newer schedule pending)
564
- // meanwhile, findExact misses and nothing is written.
565
- // Keyed by namespace and exact tokens: a sub-second final [prompt+gen] put
566
- // must not cancel the pending boundary snapshot, and unrelated prompts of
567
- // equal length must not cancel each other. Exact reschedules still
568
- // coalesce; stale keys self-clean when findExact misses.
569
- //
570
- // NON-BLOCKING (2026-07-06, the write-behind persistence contract): the
571
- // gateway lock is held only for a zero-copy SNAPSHOT (findExact +
572
- // cloneKvCaches — microseconds; entries are immutable so the clones are
573
- // consistent forever). The flush itself runs OFF the lock via
574
- // storeAsync, and writes chain serially so two multi-hundred-MB flushes
575
- // never overlap.
576
- //
577
- // IDLE-GATED (2026-07-07, the decode@ctx fix): "off the lock" was not
578
- // enough — every per-tensor flush step is a blocking GPU sync on the
579
- // SAME stream decode uses (ops.contiguous enqueues a kernel, rawBytesView
580
- // evals) plus a synchronous multi-MB writeSync, and the setImmediate
581
- // pacing interleaved those slices exactly between decode tokens. A ~16k
582
- // entry's flush overlapping the bench's ctx repeats depressed decode@ctx
583
- // ~9% on e4b (mlx-lm runs no equivalent background work). Now every step
584
- // — including the first — runs through gateway.runWhenIdle(), so the
585
- // flush only progresses while NOTHING is generating and pauses when a
586
- // request arrives mid-flush. Tradeoffs, accepted: durability waits for a
587
- // quiet moment (single-user serving quiesces constantly; sustained
588
- // hammering defers the flush AND the spill clones' GPU-memory release —
589
- // bounded by the chain). MLX_BUN_SSD_WRITEBEHIND=0
590
- // disables write-behind snapshots entirely — the paired-A/B lever + kill
591
- // switch (restart survival then degrades to spill-on-evict only).
592
- const writeBehindOn = runtimeValue("MLX_BUN_SSD_WRITEBEHIND") !== "0";
593
- const ssdFlushStep = <T>(step: () => T): Promise<T> =>
594
- gateway.runWhenIdle(async () => step());
595
- // Bounded write-behind queue (2026-07-07 review fix — see SpillQueue in
596
- // kv-store.ts): pending clones pin their entries' GPU buffers while the
597
- // idle gate starves under sustained traffic, so QUEUED bytes are capped —
598
- // over cap the oldest queued spill drops (clone disposed immediately, a
599
- // future cache miss, never a wrong result) instead of accumulating past
600
- // the prompt-cache cap. Default 2 GB (a quarter of the 8 GB RAM-cache
601
- // default); MLX_BUN_SSD_SPILL_QUEUE_GB overrides. The durability
602
- // coordinator retains a dirty record until the queue confirms the atomic
603
- // store. Explicit flush and graceful shutdown can therefore retry a drop.
604
- // `|| 2` would coerce an explicit "0" back to 2 GB — parse so 0 works
605
- // (cap 0 = keep only the newest + in-flight clone pinned; the soft cap
606
- // never drops the item just enqueued).
607
- // The gateway exists before the spill queue because the SSD writer uses
608
- // its idle boundary to avoid competing with generation.
609
- const gateway = new GenerationGateway(serving.gateway, batch, runGeneration, {
610
- kvBudgetBytes: serverOptions.kvBudgetBytes,
611
- checkpoints: !!(serverOptions.generationCheckpointTokens && ssdStore),
612
- stateCodecs,
613
- kvScheme: resolvedKvScheme,
614
- promptCache,
615
- adapterNamespace: (adapters) => ctx.adapters.cacheNamespace(adapters),
616
- });
617
- const spillQueueGbRaw = Number(runtimeValue("MLX_BUN_SSD_SPILL_QUEUE_GB"));
618
- const spillQueueCapBytes =
619
- (Number.isFinite(spillQueueGbRaw) && spillQueueGbRaw >= 0 ? spillQueueGbRaw : 2) * 1024 ** 3;
620
- // Both queues share the configured total: reserve half for checkpoints
621
- // only when that service exists. Each retains the existing soft-cap rule.
622
- const checkpointQueueBytes = ssdStore && serverOptions.generationCheckpointTokens
623
- ? Math.floor(spillQueueCapBytes / 2) : 0;
624
- if (ssdStore && serverOptions.generationCheckpointTokens)
625
- checkpointPersistence = new ContinuationPersistence(ssdStore, { maxBytes: checkpointQueueBytes, runStep: ssdFlushStep });
626
- const spillQueue = ssdStore
627
- ? new SpillQueue(
628
- spillQueueCapBytes - checkpointQueueBytes,
629
- cacheBytes,
630
- (item) => ssdStore!.storeAsync(item.tokens, item.caches, item.ns, ssdFlushStep, item.attachments),
631
- (caches) => { for (const c of caches) c.dispose(); },
632
- )
633
- : null;
634
- const durability = ssdStore && spillQueue && writeBehindOn
635
- ? new SsdDurabilityCoordinator(
636
- gateway,
637
- promptCache,
638
- spillQueue,
639
- cloneState,
640
- (tokens, ns) => ssdStore.hasDurablePrefix(tokens, ns),
641
- )
642
- : null;
643
- const durabilityStats = (): DurabilitySnapshotStats => {
644
- const stats = durability?.stats ?? {
645
- pendingSnapshots: 0,
646
- pendingSpills: spillQueue?.pendingCount ?? 0,
647
- pendingSpillBytes: spillQueue?.pendingBytes ?? 0,
648
- droppedSpills: spillQueue?.droppedCount ?? 0,
649
- failedSpills: spillQueue?.failedCount ?? 0,
650
- };
651
- const checkpoints = checkpointPersistence?.stats;
652
- return { ...stats, pendingSpills: stats.pendingSpills + (checkpoints?.pendingCount ?? 0),
653
- pendingSpillBytes: stats.pendingSpillBytes + (checkpoints?.pendingBytes ?? 0),
654
- droppedSpills: stats.droppedSpills + (checkpoints?.dropped ?? 0),
655
- failedSpills: stats.failedSpills + (checkpoints?.failed ?? 0) };
656
- };
657
- const flushDurability = async (): Promise<DurabilityFlushResult> => {
658
- const started = performance.now();
659
- const checkpoints = await checkpointPersistence?.flush();
660
- const result = durability ? await durability.flush() : null;
661
- if (!result) await spillQueue?.drain();
662
- const stats = durabilityStats();
663
- return {
664
- ...stats,
665
- durable: (result?.durable ?? stats.pendingSpills === 0) && (checkpoints?.durable ?? true) && stats.pendingSpills === 0,
666
- flushedSnapshots: result?.flushedSnapshots ?? 0,
667
- missingSnapshots: result?.missingSnapshots ?? 0,
668
- elapsedMs: performance.now() - started,
669
- };
670
- };
671
- // Every put() — serial lane AND batch scheduler — schedules the snapshot
672
- // (Layer 0: batch-lane entries survive restarts too, not just evictions).
673
- promptCache.onPut = durability ? (tokens, ns) => durability.schedule(tokens, ns) : null;
674
-
675
- // Idle demotion (Layer 0): entries unused for --ssd-demote-idle seconds
676
- // spill to SSD and free their GPU memory — the RAM tier drains between
677
- // bursts while every prefix stays reachable (take() restores via
678
- // zero-copy mmap). The exclusive section below covers only the sweep's
679
- // zero-copy clone + dispose (spillOwned above); the SSD write itself
680
- // runs off-lock on ssdWriteChain. Swept only when the engine is TRULY idle: the
681
- // runExclusive below registers as a serial waiter, which would DRAIN a
682
- // running batch, so the activity check guards it (a momentary exclusive
683
- // grab of an idle engine is free). 0 disables.
684
- const demoteIdleMs = (serverOptions.ssdDemoteIdleSec ?? (ssdStore ? 300 : 0)) * 1000;
685
- let demoteTimer: ReturnType<typeof setInterval> | null = null;
686
- if (ssdStore && demoteIdleMs > 0) {
687
- demoteTimer = setInterval(() => {
688
- if (gateway.busy) return;
689
- void gateway.runExclusive(async () => {
690
- const n = promptCache.demoteIdle(demoteIdleMs);
691
- if (n > 0) console.log(`[ssd-cache] demoted ${n} idle entr${n === 1 ? "y" : "ies"} to disk`);
692
- }).catch(() => {});
693
- }, Math.max(30_000, demoteIdleMs / 4));
694
- demoteTimer.unref?.();
695
- }
696
-
697
- const sessionEngine = createSessionCompletionEngine(gateway, disposeUnstartedRequest);
698
- const completionExecutor = new CompletionExecutor(sessionEngine);
699
-
700
- // /library response cache (30 s) — registry + config reads only.
701
- const startedAt = Date.now();
702
- const discoveryRoutes = createDiscoveryRoutes(ctx, gateway, startedAt);
703
-
704
- // Captured so the WebSocket handler can resolve the bound (possibly
705
- // ephemeral) port lazily for the loopback pi provider.
706
- let serverRef!: Server<unknown>;
707
-
708
- // Lab job system (quantize / finetune / dataset), lazily opened so a
709
- // plain serve with no Lab activity pays nothing. markZombies() recovers
710
- // rows orphaned by a crashed prior process. The dataset runner is
711
- // in-process (pure JS + loopback /v1); quantize and finetune run as
712
- // GPU-leased subprocesses via src/jobs/job-entry.ts.
713
- let jobStore: import("./jobs").JobStore | null = null;
714
- const ensureJobs = async () => {
715
- if (!jobStore) {
716
- const jobs = await import("./jobs");
717
- const store = new jobs.JobStore();
718
- store.markZombies();
719
- try { (await import("./dataset")).registerDatasetRunner(); } catch {}
720
- jobStore = store;
721
- }
722
- return jobStore;
723
- };
724
-
725
- // Per-request preparation (options/template/prompt-ids/grammar/router)
726
- // and the chat core, each built once with their collaborators injected.
727
- // The request pipeline, composed once:
728
- // new ChatRequest(body) → chatStage.run → InferenceRequest
729
- // → inferenceStage.admit → inferenceStage.run → InferenceResult → wire
730
- // (/v1/completions substitutes textStage; Anthropic and Responses reuse
731
- // chatStage with their own wire formats.)
732
- const prep = createRequestPrep({ ctx, serverOptions, kvScheme, defaultGeneratedTokens });
733
- const { templateOptionsFor } = prep;
734
- const preparation = createPreparationExecutor((work, signal) => gateway.runExclusive(work, undefined, signal), batch);
735
- const chatStage = new ChatStage(
736
- ctx, prep, promptCache, contextLimit, serverOptions.defaultAdapter,
737
- preparation, serving.buildPrompt);
738
- const textStage = new TextCompletionStage(
739
- ctx, prep, contextLimit, defaultGeneratedTokens, serverOptions.defaultAdapter);
740
- const inferenceStage = new InferenceStage(completionExecutor);
741
- const openAiMeta = (id: string) => ({ id, created: Math.floor(Date.now() / 1000), model: ctx.modelId });
742
- /** Parse the JSON body under the request's trace; a bad body is a 400. */
743
- const parseBody = async <T,>(request: Request, trace: PromptResponseTrace | undefined): Promise<T | Response> => {
744
- const closeBodyParse = trace?.begin("request.body_parse");
745
- try {
746
- return (await request.json()) as T;
747
- } catch {
748
- trace?.finish("error", { stage: "body_parse" });
749
- return Response.json({ error: { message: "invalid JSON body" } }, { status: 400 });
750
- } finally {
751
- closeBodyParse?.();
752
- }
753
- };
754
-
755
-
756
-
757
- if (serverOptions.unixSocket) {
758
- try { require("node:fs").unlinkSync(serverOptions.unixSocket); } catch {}
759
- }
760
- // Bun's types make unix/port mutually exclusive variants that a spread
761
- // union can't prove — runtime accepts either; cast once at the call.
762
- serverRef = Bun.serve({
763
- ...(serverOptions.unixSocket
764
- ? ({ unix: serverOptions.unixSocket } as unknown as Record<string, never>)
765
- : {
766
- port,
767
- ...(serverOptions.hostname ? { hostname: serverOptions.hostname } : {}),
768
- }),
769
- idleTimeout: 0,
770
- // Web chat rides pi's AgentSession events over a WebSocket; the embedded
771
- // pi provider points back at THIS server's own loopback /v1 (port
772
- // resolved lazily — it may be ephemeral until serve() binds).
773
- websocket: makePiWsHandler({
774
- port: () => serverRef.port ?? port,
775
- modelId: ctx.modelId,
776
- contextWindow: ctx.model.config.text.maxPositionEmbeddings,
777
- // capability flags — true if a tower is loaded or loadable (lazy)
778
- vision: !!(ctx.vision || ctx.loadVision),
779
- audio: !!(ctx.audio || ctx.loadAudio),
780
- thinking: ctx.template.supportsThinking,
781
- // Model-author sampling defaults (generation_config.json, server-CLI
782
- // overrides applied) for the sampling popover's per-model recommended
783
- // values (web-ui-pass-plan.md #14 groundwork) — same resolution
784
- // /v1/models' gen_defaults uses, so the two surfaces agree. The
785
- // think-mode-vs-not distinction lives in toOptions (per-request); this
786
- // is the single "model's own defaults" snapshot sent once at ready.
787
- genDefaults: {
788
- temperature: serverOptions.defaultTemperature ?? ctx.genDefaults.temperature ?? null,
789
- topP: serverOptions.defaultTopP ?? ctx.genDefaults.topP ?? null,
790
- topK: serverOptions.defaultTopK ?? ctx.genDefaults.topK ?? null,
791
- },
792
- }),
793
- async fetch(request, server) {
794
- const url = new URL(request.url);
795
-
796
- if (url.pathname === "/admin/cache/flush" && request.method === "POST") {
797
- const result = await flushDurability();
798
- return Response.json(
799
- {
800
- ...result,
801
- entries: ssdStore?.entries ?? 0,
802
- longest_durable_prefix_tokens:
803
- ssdStore?.longestDurablePrefixTokens ?? 0,
804
- },
805
- { status: result.durable ? 200 : 503 },
806
- );
807
- }
808
-
809
- if (url.pathname === "/ws/chat") {
810
- if (server.upgrade(request, { data: { sessionId: crypto.randomUUID() } as PiWsData }))
811
- return undefined;
812
- return new Response("expected websocket", { status: 426 });
813
- }
814
-
815
- const auxiliaryResponse = await handleAuxiliaryRoute(url, request);
816
- if (auxiliaryResponse) return auxiliaryResponse;
817
-
818
- const staticResponse = handleStaticRoute(url, request, STATIC_ROUTE_ASSETS);
819
- if (staticResponse) return staticResponse;
820
-
821
- const discoveryResponse = await discoveryRoutes.handle(url, request);
822
- if (discoveryResponse) return discoveryResponse;
823
-
824
- const modelAdminResponse = await handleModelAdminRoute(url, request, ctx, gateway);
825
- if (modelAdminResponse) return modelAdminResponse;
826
-
827
- if (url.pathname === "/fit" && request.method === "GET") {
828
- // Fit assessment for the status page: this-machine report at the
829
- // admission ceiling + the Apple SKU matrix at a fixed 32k.
830
- // expertsBytes comes from the registry so MoE models predict on
831
- // active bytes — same numbers as `mlx-bun fit` and the serve
832
- // banner (the three surfaces used to disagree). When the eval DB
833
- // has a real measurement for this snapshot, it rides along:
834
- // measured beats predicted.
835
- const { skuMatrix, thisMachine, detectChip } = await import("./fit");
836
- const machine = thisMachine();
837
- const chip = detectChip();
838
- let expertsBytes = 0;
839
- let artifactDiskBytes: number | null = null;
840
- let measured: { decodeTps: number; ts: number } | null = null;
841
- try {
842
- const { Registry } = await import("./registry");
843
- const rec = new Registry().list().find((r) => r.repoId === ctx.modelId);
844
- if (rec) {
845
- expertsBytes = rec.expertsBytes;
846
- artifactDiskBytes = rec.sizeBytes;
847
- const { EvalDB } = await import("./evaldb");
848
- measured = new EvalDB().latestFor(rec.path);
849
- }
850
- } catch {}
851
- if (ctx.glmMemoryPlan) {
852
- const plan = ctx.glmMemoryPlan;
853
- const li = plan.lineItems;
854
- const kvBytes = li.targetKvBytes + li.mtpKvBytes;
855
- const transientBytes = plan.plannedProcessBytes -
856
- li.residentWeightsBytes - li.mainExpertSlabBytes -
857
- li.mtpExpertSlabBytes - kvBytes;
858
- return Response.json({
859
- machine: {
860
- chip: chip.name,
861
- ram_bytes: machine.ramBytes,
862
- bandwidth_gbs: machine.bandwidthGBs,
863
- },
864
- context_tokens: plan.contextTokens,
865
- typical_context_tokens: plan.contextTokens,
866
- typical_decode_tps: GLM52_G5_MEASURED_WARM_DECODE_TPS,
867
- measured_decode_tps: GLM52_G5_MEASURED_WARM_DECODE_TPS,
868
- measured_at: GLM52_G5_MEASURED_AT,
869
- report: {
870
- fits: true,
871
- weights_bytes: li.residentWeightsBytes,
872
- kv_bytes: kvBytes,
873
- transient_bytes: transientBytes,
874
- total_bytes: plan.plannedProcessBytes,
875
- usable_bytes: plan.processLimitBytes,
876
- max_safe_context: plan.contextTokens,
877
- predicted_decode_tps: null,
878
- },
879
- glm52: {
880
- artifact_disk_bytes: artifactDiskBytes,
881
- main_expert_slab_bytes: li.mainExpertSlabBytes,
882
- mtp_expert_slab_bytes: li.mtpExpertSlabBytes,
883
- max_generation_tokens: plan.maxGenerationTokens,
884
- direct_oracle_warm_decode_tps:
885
- GLM52_G5_DIRECT_ORACLE_WARM_DECODE_TPS,
886
- aspirational_decode_tps: GLM52_G5_ASPIRATIONAL_DECODE_TPS,
887
- },
888
- sku_matrix_ctx: plan.contextTokens,
889
- sku_matrix: [{
890
- sku: chip.name,
891
- ram_gb: Math.round(machine.ramBytes / 2 ** 30),
892
- fits: true,
893
- max_context: plan.contextTokens,
894
- decode_tps: GLM52_G5_MEASURED_WARM_DECODE_TPS,
895
- }],
896
- });
897
- }
898
- const report = fit(
899
- ctx.model.config, ctx.model.weightsBytes, admission.maxSafeContext,
900
- machine, undefined, expertsBytes, serverOptions.memoryBudgetBytes,
901
- resolvedKvScheme.fitOptions,
902
- );
903
- return Response.json({
904
- machine: { chip: chip.name, ram_bytes: machine.ramBytes, bandwidth_gbs: machine.bandwidthGBs },
905
- context_tokens: admission.maxSafeContext,
906
- // Headline number: prediction at a TYPICAL context (8k) — the
907
- // max-context report below is the bandwidth worst case (every
908
- // decode step re-reads the full KV), not the everyday speed.
909
- typical_context_tokens: Math.min(8192, admission.maxSafeContext),
910
- typical_decode_tps: fit(
911
- ctx.model.config, ctx.model.weightsBytes,
912
- Math.min(8192, admission.maxSafeContext),
913
- machine, undefined, expertsBytes, serverOptions.memoryBudgetBytes,
914
- resolvedKvScheme.fitOptions,
915
- ).predictedDecodeTps,
916
- measured_decode_tps: measured?.decodeTps ?? null,
917
- measured_at: measured?.ts ?? null,
918
- report: {
919
- fits: report.fits,
920
- weights_bytes: report.weightsBytes,
921
- kv_bytes: report.kvBytes,
922
- transient_bytes: report.transientBytes,
923
- total_bytes: report.totalBytes,
924
- usable_bytes: report.usableBytes,
925
- max_safe_context: report.maxSafeContext,
926
- predicted_decode_tps: report.predictedDecodeTps,
927
- },
928
- sku_matrix_ctx: 32768,
929
- sku_matrix: skuMatrix(ctx.model.config, ctx.model.weightsBytes, 32768, expertsBytes).map((r) => ({
930
- sku: r.sku, ram_gb: r.ramGB, fits: r.fits,
931
- max_context: r.maxContext, decode_tps: r.decodeTps,
932
- })),
933
- });
934
- }
935
-
936
- if (url.pathname === "/stats" && request.method === "GET") {
937
- // Active KV scheme across ALL layers. Since Phase 9 rotating
938
- // (sliding-window) caches quantize too, so every layer the
939
- // scheme names counts — the old display filtered to
940
- // full_attention and silently undercounted (e.g. 26B showed
941
- // 5/30 quantized when its kv_config.json covers all 30).
942
- const layerTypes = ctx.model.config.text.layerTypes;
943
- const kvLayers: Record<string, number> = {};
944
- let kvMode = "bf16";
945
- if (kvScheme.turboQuant) {
946
- // v1: full-attention layers only (sliding-window stays bf16 —
947
- // docs/design/turboquant.md non-goal).
948
- const fullAttn = layerTypes.filter((l) => l !== "sliding_attention").length;
949
- kvMode = `turbo k${kvScheme.turboQuant.kBits}v${kvScheme.turboQuant.vBits}`;
950
- kvLayers[`turbo-k${kvScheme.turboQuant.kBits}v${kvScheme.turboQuant.vBits}`] = fullAttn;
951
- } else if (kvScheme.kvBits) {
952
- kvMode = `uniform-kv${kvScheme.kvBits}`;
953
- kvLayers[`kv${kvScheme.kvBits}`] = layerTypes.length;
954
- } else if (kvScheme.kvConfig) {
955
- kvMode = "mixed (kv_config.json)";
956
- for (const e of kvScheme.kvConfig)
957
- kvLayers[`kv${e.bits}`] = (kvLayers[`kv${e.bits}`] ?? 0) + 1;
958
- }
959
- const bf16Layers = layerTypes.length - Object.values(kvLayers).reduce((a, b) => a + b, 0);
960
- const slidingLayers = layerTypes.filter((l) => l === "sliding_attention").length;
961
- return Response.json({
962
- server: {
963
- owner: serverOptions.owner ?? "embedded",
964
- model: ctx.modelId,
965
- started_at: startedAt,
966
- },
967
- prompt_cache: {
968
- entries: promptCache.size,
969
- bytes: promptCache.totalBytes,
970
- max_bytes: promptCache.maxBytes,
971
- hits: promptCache.hits,
972
- misses: promptCache.misses,
973
- },
974
- ...(ssdStore ? {
975
- ssd_cache: {
976
- dir: serverOptions.ssdCacheDir,
977
- entries: ssdStore.entries,
978
- bytes: ssdStore.totalBytes,
979
- max_bytes: Number.isFinite(ssdStore.maxBytes) ? ssdStore.maxBytes : null,
980
- restores: ssdStore.stats.restores,
981
- spills: ssdStore.stats.spills,
982
- restore_ms_last: Math.round(ssdStore.stats.restoreMsLast),
983
- demotions: promptCache.demotions,
984
- pending_snapshots: durability?.stats.pendingSnapshots ?? 0,
985
- pending_spills: spillQueue?.pendingCount ?? 0,
986
- pending_spill_bytes: spillQueue?.pendingBytes ?? 0,
987
- dropped_spills: spillQueue?.droppedCount ?? 0,
988
- failed_spills: spillQueue?.failedCount ?? 0,
989
- longest_durable_prefix_tokens: ssdStore.longestDurablePrefixTokens,
990
- },
991
- } : {}),
992
- response_store: {
993
- entries: responseStore.size,
994
- bytes: responseStore.totalBytes,
995
- max_bytes: responseStore.maxBytes,
996
- ttl_ms: responseStore.ttlMs,
997
- },
998
- kv_quant: {
999
- mode: kvMode,
1000
- layers: { ...kvLayers, ...(bf16Layers > 0 ? { bf16: bf16Layers } : {}) },
1001
- attention: {
1002
- global: layerTypes.length - slidingLayers,
1003
- sliding_window: slidingLayers,
1004
- },
1005
- },
1006
- admission: {
1007
- max_safe_context: admission.maxSafeContext,
1008
- enforced_context_tokens: contextLimit,
1009
- memory_budget_bytes:
1010
- ctx.glmMemoryPlan?.processLimitBytes ??
1011
- serverOptions.memoryBudgetBytes ?? null,
1012
- usable_bytes: admission.usableBytes,
1013
- weights_bytes: ctx.model.weightsBytes,
1014
- },
1015
- ...serving.diagnostics(),
1016
- // --batch: configured cap, whether batching is live for this model,
1017
- // and rows currently decoding in the batch.
1018
- batch: {
1019
- configured: batch,
1020
- mode: gateway.batchMode,
1021
- batched: gateway.batchingEnabled,
1022
- active_rows: gateway.activeRows,
1023
- pending_rows: gateway.pendingRows,
1024
- submitted_rows: gateway.submittedRows,
1025
- kv_bytes: gateway.kvBytes.projected,
1026
- kv_budget_bytes: gateway.kvBytes.budget,
1027
- },
1028
- });
1029
- }
1030
-
1031
- // A parent-managed GPU job holds this response open. The connection owns
1032
- // the native lease, so parent death cannot strand the worker lock.
1033
- if (serverOptions.unixSocket && url.pathname === "/admin/lease" && request.method === "POST") {
1034
- const lease = await gateway.acquireExecutionLease(request.signal);
1035
- let released = false;
1036
- const release = () => {
1037
- if (released) return;
1038
- released = true;
1039
- request.signal.removeEventListener("abort", release);
1040
- lease.dispose();
1041
- };
1042
- request.signal.addEventListener("abort", release, { once: true });
1043
- if (request.signal.aborted) release();
1044
- return new Response(new ReadableStream({
1045
- start(controller) { controller.enqueue(new TextEncoder().encode("leased\n")); },
1046
- cancel() { release(); },
1047
- }), { headers: { "content-type": "application/octet-stream" } });
1048
- }
1049
-
1050
- // Engine-mode admin (unix-socket children only — never exposed on
1051
- // TCP): drain = quiesce the gateway + demote the whole prompt cache
1052
- // to the SSD tier. The pool calls this before evicting a model
1053
- // child, so its state survives the eviction losslessly.
1054
- if (
1055
- serverOptions.unixSocket &&
1056
- url.pathname === "/admin/drain" &&
1057
- request.method === "POST"
1058
- ) {
1059
- await gateway.runExclusive(async () => {
1060
- promptCache.demoteIdle(0);
1061
- });
1062
- return Response.json({ drained: true, demotions: promptCache.demotions });
1063
- }
1064
-
1065
- // ---- Curve Designer: POST /signal {prompt} → next-token histogram over the curve's x-axis ----
1066
- // One prefill forward; bins the real log-prob distribution so the editor can draw the
1067
- // signal under the curve (you shape where the tokens actually are).
1068
- if (url.pathname === "/signal" && request.method === "OPTIONS")
1069
- return new Response(null, { headers: CURVE_CORS });
1070
- if (url.pathname === "/signal" && request.method === "POST") {
1071
- let sbody: { prompt?: string };
1072
- try { sbody = (await request.json()) as typeof sbody; }
1073
- catch { return Response.json({ error: "invalid JSON" }, { status: 400, headers: CURVE_CORS }); }
1074
- let sids = ctx.tokenizer.encode(ctx.template.render([{ role: "user", content: typeof sbody.prompt === "string" ? sbody.prompt : "" }], templateOptionsFor({} as ChatRequestParams, null)));
1075
- if (sids[0] === sids[1] && sids[0] === ctx.tokenizer.bosTokenId) sids = sids.slice(1);
1076
- try {
1077
- const NB = 80;
1078
- // gateway.runExclusive: this raw forward must not run concurrently
1079
- // with batched decode steps or a serial generation (GPU + shared
1080
- // model state are single-owner — D3, one lock).
1081
- const result = await gateway.runExclusive(() => serving.signal(sids, NB, CURVE_UMIN));
1082
- return Response.json(result, { headers: CURVE_CORS });
1083
- } catch (e) {
1084
- return Response.json({ error: `signal failed: ${(e as Error).message}` }, { status: 500, headers: CURVE_CORS });
1085
- }
1086
- }
1087
-
1088
- // ---- v2 HLG Curve Designer: POST /generate {prompt, curve, n, max_tokens, seed} ----
1089
- // The drawn log-prob transfer curve REPLACES temperature+softmax entirely
1090
- // (src/lab/curve/curve-sampler.ts). The browser editor calls this; same curve object the
1091
- // tool's "Copy values" emits is the one the sampler consumes — one contract.
1092
- if (url.pathname === "/generate" && request.method === "OPTIONS")
1093
- return new Response(null, { headers: CURVE_CORS });
1094
- if (url.pathname === "/generate" && request.method === "POST") {
1095
- let body: { prompt?: string; curve?: CurveParams; n?: number; max_tokens?: number; seed?: number; default?: boolean };
1096
- try { body = (await request.json()) as typeof body; }
1097
- catch { return Response.json({ error: "invalid JSON" }, { status: 400, headers: CURVE_CORS }); }
1098
- const curve = body.curve;
1099
- // Identity / no shaped curve → fall back to the model's DEFAULT chat recipe
1100
- // (temp + top-p + top-k) — the honest "what you'd get chatting" baseline, which
1101
- // a smooth curve can't replicate (top-p/top-k are hard truncations).
1102
- const useCurve = body.default !== true && Array.isArray(curve?.points) && curve.points.length >= 2;
1103
- if (useCurve && !isMonotone(curve!))
1104
- return Response.json({ error: "curve is not monotone — all segment slopes must be ≥ 0" }, { status: 400, headers: CURVE_CORS });
1105
- const recipe = {
1106
- temperature: serverOptions.defaultTemperature ?? ctx.genDefaults.temperature ?? 0.7,
1107
- topP: serverOptions.defaultTopP ?? ctx.genDefaults.topP ?? 0,
1108
- topK: serverOptions.defaultTopK ?? ctx.genDefaults.topK ?? 0,
1109
- };
1110
- const prompt = typeof body.prompt === "string" ? body.prompt : "";
1111
- const n = Math.max(1, Math.min(8, Math.floor(Number(body.n) || 3)));
1112
- const maxTokens = Math.max(1, Math.min(256, Math.floor(Number(body.max_tokens) || 80)));
1113
- const baseSeed = Number.isFinite(body.seed) ? Number(body.seed) >>> 0 : (Date.now() & 0xffffffff);
1114
- let ids = ctx.tokenizer.encode(ctx.template.render([{ role: "user", content: prompt }], templateOptionsFor({} as ChatRequestParams, null)));
1115
- if (ids[0] === ids[1] && ids[0] === ctx.tokenizer.bosTokenId) ids = ids.slice(1);
1116
- const samples: { text: string; junk: boolean }[] = [];
1117
- try {
1118
- for (let i = 0; i < n; i++) {
1119
- const toks: number[] = [];
1120
- const genOpts: GenerateOptions = useCurve
1121
- ? { curve, seed: baseSeed + i, maxTokens, ...kvScheme }
1122
- : { temperature: recipe.temperature, topP: recipe.topP, topK: recipe.topK, seed: baseSeed + i, maxTokens, ...kvScheme };
1123
- // The same plan and execution lease cover the Lab comparison endpoint.
1124
- const shape = { hasVision: false, hasAdapters: false, hasRepetitionPenalty: false,
1125
- hasLogitsExtras: false, wantsLogprobs: false, userSeed: true,
1126
- kvQuant: !!(genOpts.kvBits || genOpts.kvConfig?.length), turboQuant: !!genOpts.turboQuant,
1127
- hasGrammar: false, hasDraft: !!ctx.draft };
1128
- const placement = gateway.place(shape, genOpts);
1129
- await gateway.run(ids, genOpts, (t) => { toks.push(t); }, undefined,
1130
- shape, placement, request.signal);
1131
- const text = ctx.tokenizer.decode(toks, true).trim();
1132
- samples.push({ text, junk: curveJunk(text) });
1133
- }
1134
- } catch (e) {
1135
- return Response.json({ error: `generation failed: ${(e as Error).message}` }, { status: 500, headers: CURVE_CORS });
1136
- }
1137
- return Response.json({ mode: useCurve ? "curve" : "default", recipe: useCurve ? undefined : recipe, n, seed: baseSeed, samples }, { headers: CURVE_CORS });
1138
- }
1139
-
1140
- if (url.pathname === "/v1/chat/completions" && request.method === "POST") {
1141
- const id = `chatcmpl-${crypto.randomUUID()}`;
1142
- const trace = createPromptResponseTrace({
1143
- traceId: request.headers.get("x-mlx-bun-trace-id") ?? id,
1144
- requestId: id,
1145
- route: url.pathname,
1146
- });
1147
- const body = await parseBody<ChatRequestParams>(request, trace);
1148
- if (body instanceof Response) return body;
1149
- const meta = openAiMeta(id);
1150
- const a = await admit(
1151
- inferenceStage, () => chatStage.run(new ChatRequest(body), id, request.signal), trace, "chat request");
1152
- if ("response" in a) return a.response;
1153
- return body.stream
1154
- ? respondStream(inferenceStage, a.admitted, chatCompletionStream(meta), request.signal, trace)
1155
- : respondJson(inferenceStage, a.admitted, (r) => chatCompletionJson(r, meta), request.signal, trace);
1156
- }
1157
-
1158
- // Raw text completion (mlx_lm.server's /v1/completions): no chat
1159
- // template — TextCompletionStage tokenizes the prompt string directly.
1160
- if (url.pathname === "/v1/completions" && request.method === "POST") {
1161
- const id = `cmpl-${crypto.randomUUID()}`;
1162
- const trace = createPromptResponseTrace({
1163
- traceId: request.headers.get("x-mlx-bun-trace-id") ?? id,
1164
- requestId: id,
1165
- route: url.pathname,
1166
- });
1167
- const body = await parseBody<TextCompletionParams>(request, trace);
1168
- if (body instanceof Response) return body;
1169
- const meta = openAiMeta(id);
1170
- const a = await admit(
1171
- inferenceStage, () => textStage.run(new TextCompletionRequest(body), id), trace, "text completion");
1172
- if ("response" in a) return a.response;
1173
- return body.stream
1174
- ? respondStream(inferenceStage, a.admitted, textCompletionStream(meta), request.signal, trace)
1175
- : respondJson(inferenceStage, a.admitted, (r) => textCompletionJson(r, meta), request.signal, trace);
1176
- }
1177
-
1178
- // Anthropic Messages API (Phase 11) — on by default, mirroring
1179
- // optiq serve (--anthropic defaults True; the drop-in claim
1180
- // depends on it). Oracle: optiq/anthropic_shim.py, ported in
1181
- // src/anthropic.ts. Point Claude Code at this port via
1182
- // ANTHROPIC_BASE_URL for a fully local backend.
1183
- if (url.pathname === "/v1/messages" && request.method === "POST") {
1184
- const anthropicError: ErrorFormatter = (status, message) =>
1185
- Response.json(
1186
- { type: "error", error: { type: status >= 500 ? "api_error" : "invalid_request_error", message } },
1187
- { status },
1188
- );
1189
- let anthropicBody: AnthropicRequest;
1190
- try {
1191
- anthropicBody = (await request.json()) as AnthropicRequest;
1192
- } catch {
1193
- return anthropicError(400, "invalid JSON body", {});
1194
- }
1195
- let chatBody: ChatRequestParams;
1196
- try {
1197
- chatBody = anthropicToChatBody(anthropicBody) as unknown as ChatRequestParams;
1198
- } catch (e) {
1199
- return anthropicError(400, (e as Error).message, {});
1200
- }
1201
- const id = `chatcmpl-${crypto.randomUUID()}`;
1202
- const a = await admit(
1203
- inferenceStage, () => chatStage.run(new ChatRequest(chatBody), id, request.signal), undefined,
1204
- "anthropic request", anthropicError);
1205
- if ("response" in a) return a.response;
1206
- if (anthropicBody.stream)
1207
- return respondStream(
1208
- inferenceStage, a.admitted, createAnthropicStreamProtocol(ctx.modelId), request.signal);
1209
- return respondJson(
1210
- inferenceStage, a.admitted,
1211
- (r) => chatJsonToAnthropic(chatCompletionJson(r, openAiMeta(id)), ctx.modelId),
1212
- request.signal, undefined, anthropicError);
1213
- }
1214
-
1215
- // OpenAI Responses API (Phase 11) — Codex/Cursor/Continue speak
1216
- // this now. Oracle: optiq/responses_shim.py + responses_server.py,
1217
- // ported in src/responses.ts. previous_response_id resumes a prior
1218
- // conversation from the in-process store (TTL + byte-capped LRU).
1219
- if (url.pathname === "/v1/responses" && request.method === "POST") {
1220
- const responsesError: ErrorFormatter = (status, message) =>
1221
- Response.json(
1222
- {
1223
- error: {
1224
- message,
1225
- type: status >= 500 ? "server_error" : "invalid_request_error",
1226
- param: null, code: null,
1227
- },
1228
- },
1229
- { status },
1230
- );
1231
- let responsesBody: ResponsesRequest;
1232
- try {
1233
- responsesBody = (await request.json()) as ResponsesRequest;
1234
- } catch {
1235
- return responsesError(400, "invalid JSON body", {});
1236
- }
1237
-
1238
- let conversation: ReturnType<typeof resolveResponsesConversation>;
1239
- try { conversation = resolveResponsesConversation(responsesBody, responseStore); }
1240
- catch (error) { return responsesError(404, (error as Error).message, {}); }
1241
- responsesBody = conversation.body;
1242
- const prevId = conversation.previousId;
1243
- const capturedInput = conversation.input;
1244
- const capturedInstructions = conversation.instructions;
1245
- const storeHere = !(serverOptions.unixSocket && request.headers.get("x-mlx-bun-response-owner") === "parent");
1246
- const remember = (id: string, output: unknown[]) => {
1247
- if (storeHere) responseStore.put(id, { input: capturedInput, output, instructions: capturedInstructions });
1248
- };
1249
-
1250
- let chatBody: ChatRequestParams;
1251
- try {
1252
- chatBody = responsesToChatBody(responsesBody) as unknown as ChatRequestParams;
1253
- } catch (e) {
1254
- return responsesError(400, (e as Error).message, {});
1255
- }
1256
- const id = `chatcmpl-${crypto.randomUUID()}`;
1257
- const a = await admit(
1258
- inferenceStage, () => chatStage.run(new ChatRequest(chatBody), id, request.signal), undefined,
1259
- "responses request", responsesError);
1260
- if ("response" in a) return a.response;
1261
- if (responsesBody.stream)
1262
- return respondStream(
1263
- inferenceStage, a.admitted,
1264
- createResponsesStreamProtocol(
1265
- ctx.modelId,
1266
- prevId,
1267
- (final) =>
1268
- remember(final.id as string, final.output as unknown[]),
1269
- ),
1270
- request.signal);
1271
- return respondJson(
1272
- inferenceStage, a.admitted,
1273
- (r) => {
1274
- const responses = chatJsonToResponses(chatCompletionJson(r, openAiMeta(id)), ctx.modelId, prevId);
1275
- remember(responses.id as string, responses.output as unknown[]);
1276
- return responses;
1277
- },
1278
- request.signal, undefined, responsesError);
1279
- }
1280
-
1281
- const labResponse = await handleLabRoute(url, request, {
1282
- ensureJobs,
1283
- acquireGpu: (signal) => gateway.acquireExecutionLease(signal),
1284
- serverPort: () => server.port,
1285
- invalidateLibrary: () => discoveryRoutes.invalidateLibrary(),
1286
- });
1287
- if (labResponse) return labResponse;
1288
-
1289
- const adminResponse = await handleAdminRoute(url, request, {
1290
- ensureJobs,
1291
- invalidateLibrary: () => discoveryRoutes.invalidateLibrary(),
1292
- });
1293
- if (adminResponse) return adminResponse;
1294
-
1295
- return Response.json({ error: { message: "not found" } }, { status: 404 });
1296
- },
1297
- });
1298
- serverLifecycles.set(serverRef, {
1299
- stopJobs: async () => {
1300
- if (jobStore) {
1301
- const jobs = await import("./jobs");
1302
- await Promise.all([jobs.closeSubprocessJobs(jobStore), jobs.closeInProcessJobs(jobStore)]);
1303
- }
1304
- },
1305
- close: async () => { preparation.close(); await sessionEngine.close(); await gateway.close(); },
1306
- flush: flushDurability,
1307
- stats: durabilityStats,
1308
- stopTimers: () => {
1309
- if (demoteTimer) clearInterval(demoteTimer);
1310
- demoteTimer = null;
1311
- },
1312
- });
1313
- return serverRef;
1314
- }