mlx-bun 0.2.0 → 0.4.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 (433) hide show
  1. package/README.md +71 -588
  2. package/docs/README.md +94 -0
  3. package/docs/{design → archive/investigations}/audio-input-plan.md +4 -4
  4. package/docs/{design → archive/investigations}/diffusion-gemma-port.md +3 -3
  5. package/docs/{design → archive/investigations}/faithful-l1-consolidation.md +4 -4
  6. package/docs/{design → archive/investigations}/minicpm5-decode-megakernel.md +11 -11
  7. package/docs/archive/investigations/openjarvis-fit.md +198 -0
  8. package/docs/{design → archive/investigations}/optimization_plan.md +1 -1
  9. package/docs/{design → archive/investigations}/pre-colibri-stabilization.md +5 -5
  10. package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
  11. package/docs/{design → archive/investigations}/the-dreaming-handoff.md +2 -2
  12. package/docs/archive/planning/release-notes-v0.3.0.md +35 -0
  13. package/docs/design/batching.md +1841 -0
  14. package/docs/design/decode-speed-program.md +2346 -169
  15. package/docs/design/dreaming-nightly-pipeline.md +675 -282
  16. package/docs/design/generic-model-support.md +655 -334
  17. package/docs/design/kv-cache.md +824 -0
  18. package/docs/design/orpo-training.md +673 -1239
  19. package/docs/design/speculative-decoding.md +1990 -0
  20. package/docs/design/turboquant.md +2212 -0
  21. package/docs/design/unified-engine-frontier-plan.md +1833 -727
  22. package/docs/design/web-chat-redesign.md +666 -1083
  23. package/docs/planning/PRODUCT_ROADMAP.md +50 -1
  24. package/docs/planning/release-notes-v0.4.0.md +45 -0
  25. package/docs/planning/video-series.md +2 -2
  26. package/docs/reference/benchmarks.md +3613 -0
  27. package/docs/reference/cli.md +434 -160
  28. package/docs/reference/distribution.md +328 -182
  29. package/docs/reference/environment.md +398 -0
  30. package/docs/reference/glossary.md +39 -0
  31. package/docs/reference/library-api.md +566 -168
  32. package/docs/reference/memory.md +227 -64
  33. package/docs/reference/models.md +113 -6
  34. package/docs/reference/server-api.md +944 -429
  35. package/docs/reference/server-config.md +740 -338
  36. package/docs/reference/training.md +393 -254
  37. package/docs/reference/troubleshooting.md +158 -0
  38. package/package.json +9 -4
  39. package/src/audio/decode.ts +2 -2
  40. package/src/audio/features.ts +2 -2
  41. package/src/backends/mlx/assistant-target.ts +46 -0
  42. package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
  43. package/src/backends/mlx/autoregressive.ts +108 -0
  44. package/src/backends/mlx/batch-group.ts +1790 -0
  45. package/src/backends/mlx/cache-attachment.ts +67 -0
  46. package/src/backends/mlx/cache-layout.ts +31 -0
  47. package/src/backends/mlx/checkpoint-state.ts +53 -0
  48. package/src/backends/mlx/continuation-persistence.ts +106 -0
  49. package/src/backends/mlx/continuation-request.ts +43 -0
  50. package/src/backends/mlx/continuation.ts +77 -0
  51. package/src/backends/mlx/diffusion.ts +39 -0
  52. package/src/backends/mlx/draft-target.ts +32 -0
  53. package/src/backends/mlx/evaluation.ts +105 -0
  54. package/src/backends/mlx/fill-append.ts +45 -0
  55. package/src/backends/mlx/full-kv-row-append.ts +32 -0
  56. package/src/backends/mlx/full-kv-row-donor.ts +54 -0
  57. package/src/backends/mlx/full-transitioning-kv-rows.ts +65 -0
  58. package/src/backends/mlx/gateway-binding.ts +130 -0
  59. package/src/backends/mlx/graph.ts +38 -0
  60. package/src/backends/mlx/kv-codec.ts +9 -0
  61. package/src/backends/mlx/kv-maintenance.ts +133 -0
  62. package/src/backends/mlx/kv-tensor-rows.ts +212 -0
  63. package/src/backends/mlx/memory-client.ts +189 -0
  64. package/src/backends/mlx/methods.ts +90 -0
  65. package/src/backends/mlx/model-prompt.ts +164 -0
  66. package/src/backends/mlx/model-serving.ts +79 -0
  67. package/src/backends/mlx/prefill-cohort.ts +89 -0
  68. package/src/backends/mlx/prefill-rows.ts +200 -0
  69. package/src/backends/mlx/prefill.ts +33 -0
  70. package/src/backends/mlx/projected-context-rows.ts +72 -0
  71. package/src/backends/mlx/request-policy.ts +20 -0
  72. package/src/backends/mlx/request-state-policy.ts +41 -0
  73. package/src/backends/mlx/rollback.ts +89 -0
  74. package/src/backends/mlx/row-sampling.ts +49 -0
  75. package/src/backends/mlx/serial-executor.ts +242 -0
  76. package/src/backends/mlx/speculative-group.ts +359 -0
  77. package/src/backends/mlx/speculative-round.ts +161 -0
  78. package/src/backends/mlx/speculative.ts +114 -0
  79. package/src/backends/mlx/state-rows.ts +104 -0
  80. package/src/backends/mlx/state-views.ts +46 -0
  81. package/src/backends/mlx/target-layout-capability.ts +35 -0
  82. package/src/backends/mlx/text-engine.ts +73 -0
  83. package/src/backends/mlx/transitioning-kv-rows.ts +165 -0
  84. package/src/cli.ts +66 -58
  85. package/src/client.ts +66 -0
  86. package/src/config.ts +21 -4
  87. package/src/contracts/completion.ts +16 -0
  88. package/src/contracts/execution.ts +60 -0
  89. package/src/contracts/generation.ts +103 -0
  90. package/src/contracts/host.ts +7 -0
  91. package/src/contracts/jobs.ts +66 -0
  92. package/src/contracts/pi-provider.ts +30 -0
  93. package/src/contracts/pi.ts +259 -0
  94. package/src/contracts/prefix-cache.ts +23 -0
  95. package/src/contracts/resources.ts +25 -0
  96. package/src/contracts/sampling.ts +23 -0
  97. package/src/contracts/scheduling.ts +45 -0
  98. package/src/contracts/tsconfig.json +14 -0
  99. package/src/diffusion/diffusion-generate.ts +218 -153
  100. package/src/embed.ts +1 -1
  101. package/src/engine/admission.ts +59 -0
  102. package/src/engine/cancellation.ts +38 -0
  103. package/src/engine/engine.ts +56 -0
  104. package/src/engine/execution-coordinator.ts +70 -0
  105. package/src/engine/execution-plan.ts +53 -0
  106. package/src/engine/resources.ts +53 -0
  107. package/src/engine/scheduler.ts +72 -0
  108. package/src/engine/session.ts +247 -0
  109. package/src/eval/README.md +44 -1
  110. package/src/eval/kl.ts +236 -3
  111. package/src/eval/reference-logits.ts +207 -0
  112. package/src/eval/runner.ts +8 -96
  113. package/src/fill/echo-index.ts +188 -0
  114. package/src/fill/fill-session.ts +452 -0
  115. package/src/fill/proposal.ts +68 -0
  116. package/src/fill/schema-rows.ts +506 -0
  117. package/src/fill/tool-boundary.ts +103 -0
  118. package/src/fit.ts +30 -16
  119. package/src/generate.ts +709 -417
  120. package/src/grammar.ts +12 -7
  121. package/src/harness-pi.ts +14 -33
  122. package/src/hub-rest.ts +4 -4
  123. package/src/index.ts +7 -0
  124. package/src/inference/denoising.ts +17 -0
  125. package/src/inference/draft-acceptance.ts +41 -0
  126. package/src/inference/draft-output.ts +49 -0
  127. package/src/inference/graph.ts +33 -0
  128. package/src/inference/prefill.ts +39 -0
  129. package/src/inference/rollback.ts +7 -0
  130. package/src/jobs/db.ts +4 -0
  131. package/src/jobs/index.ts +2 -0
  132. package/src/jobs/job-entry.ts +12 -12
  133. package/src/jobs/runner.ts +98 -13
  134. package/src/jobs/task-client.ts +19 -0
  135. package/src/jobs/types.ts +2 -58
  136. package/src/kv-scheme.ts +70 -12
  137. package/src/kv-store.ts +177 -63
  138. package/src/lab/README.md +18 -0
  139. package/src/{assets → lab/curve}/curve-designer.html +2 -2
  140. package/src/{curve-sampler.ts → lab/curve/curve-sampler.ts} +2 -2
  141. package/src/{expert-trace.ts → lab/expert-trace/expert-trace.ts} +5 -5
  142. package/src/lab/paged-kv/paged-kv-rows.ts +83 -0
  143. package/src/{model → lab/paged-kv}/paged-kv.ts +32 -15
  144. package/src/library.ts +40 -0
  145. package/src/lora.ts +18 -0
  146. package/src/media-fetch.ts +1 -1
  147. package/src/memory/article.ts +1 -1
  148. package/src/memory/chunk.ts +1 -1
  149. package/src/memory/model.ts +30 -169
  150. package/src/memory/prompts.ts +1 -1
  151. package/src/memory/rest.ts +1 -1
  152. package/src/memory/schedule.ts +1 -1
  153. package/src/memory/stages.ts +4 -1
  154. package/src/memory/vault.ts +1 -3
  155. package/src/mlx/array.ts +11 -11
  156. package/src/mlx/compile.ts +12 -2
  157. package/src/mlx/ffi.ts +58 -11
  158. package/src/mlx/materialize.ts +23 -0
  159. package/src/mlx/metal-kernel.ts +10 -11
  160. package/src/mlx/ops.ts +33 -13
  161. package/src/mlx/quantized-rows.ts +36 -0
  162. package/src/mlx/token-bitmask.ts +52 -0
  163. package/src/mlx/turboquant-kv-decode.ts +82 -0
  164. package/src/mlx/turboquant-kv-inverse.ts +122 -0
  165. package/src/mlx/turboquant-ops.ts +33 -3
  166. package/src/mlx/turboquant-tables.ts +2 -2
  167. package/src/model/artifact-identity.ts +73 -0
  168. package/src/model/batched-kv.ts +235 -0
  169. package/src/model/batched-mask.ts +20 -6
  170. package/src/model/batched-quant.ts +9 -3
  171. package/src/model/batched-quantized-kv.ts +144 -0
  172. package/src/model/batched-rotating-quant.ts +78 -17
  173. package/src/model/batched-rotating-state.ts +76 -0
  174. package/src/model/batched-rotating.ts +65 -23
  175. package/src/model/batched-row-storage.ts +52 -0
  176. package/src/model/batched-ssm.ts +52 -0
  177. package/src/model/batched-turboquant-kv.ts +93 -0
  178. package/src/model/compiled-decode.ts +8 -2
  179. package/src/model/decoded-kv-donor.ts +12 -0
  180. package/src/model/delayed-quantized-kv.ts +72 -0
  181. package/src/model/delayed-rotating-quantized-kv.ts +93 -0
  182. package/src/model/delayed-turboquant-kv.ts +55 -0
  183. package/src/model/diffusion-gemma.ts +8 -8
  184. package/src/model/factory.ts +76 -33
  185. package/src/model/fingerprint.ts +1 -1
  186. package/src/model/flash-attention.ts +3 -3
  187. package/src/model/full-prefill-padding.ts +56 -0
  188. package/src/model/full-prefill-row.ts +105 -0
  189. package/src/model/gemma4-base.ts +179 -80
  190. package/src/model/gemma4.ts +40 -32
  191. package/src/model/generated/gemma4-12b.ts +6 -3
  192. package/src/model/generated/gemma4-26b.ts +6 -3
  193. package/src/model/generated/gemma4-e4b.ts +6 -3
  194. package/src/model/glm52-cache.ts +80 -4
  195. package/src/model/glm52-dsa.ts +13 -9
  196. package/src/model/glm52-memory.ts +1 -1
  197. package/src/model/glm52-mla.ts +12 -6
  198. package/src/model/glm52.ts +19 -40
  199. package/src/model/implementation.ts +69 -0
  200. package/src/model/kv-attention-view.ts +67 -0
  201. package/src/model/minicpm5.ts +17 -15
  202. package/src/model/profile.ts +12 -2
  203. package/src/model/qwen-append.ts +12 -0
  204. package/src/model/qwen-conv.ts +52 -0
  205. package/src/model/qwen3-delta-state.ts +74 -0
  206. package/src/model/qwen3-delta.ts +59 -36
  207. package/src/model/qwen3_5-checkpoint.ts +164 -0
  208. package/src/model/qwen3_5.ts +202 -78
  209. package/src/model/rotating-kv-donor.ts +35 -0
  210. package/src/model/rotating-kv-layout.ts +262 -0
  211. package/src/model/rotating-row-transaction.ts +35 -0
  212. package/src/model/speculative-rotating-kv.ts +101 -0
  213. package/src/model/ssm-prefill-padding.ts +39 -0
  214. package/src/model/support.ts +1 -1
  215. package/src/model/trellis-balanced-scatter.ts +51 -0
  216. package/src/model/trellis-linear.ts +672 -0
  217. package/src/model/trellis-shared-m.ts +151 -0
  218. package/src/model/trellis-shared-scatter.ts +144 -0
  219. package/src/model/trellis-splitk-prefill.ts +158 -0
  220. package/src/model/trellis-tiled-prefill.ts +136 -0
  221. package/src/model/trellis-vector-expand.ts +62 -0
  222. package/src/model/trellis-wide-prefill.ts +94 -0
  223. package/src/model/turboquant-codec.ts +83 -0
  224. package/src/model/universal/dense.ts +15 -14
  225. package/src/model/universal/modules.ts +8 -1
  226. package/src/native-pack.ts +6 -5
  227. package/src/pi-provider.ts +6 -64
  228. package/src/pi-web.ts +8 -315
  229. package/src/preflight.ts +31 -15
  230. package/src/prompt-cache.ts +110 -69
  231. package/src/quantize/allocator.ts +1 -1
  232. package/src/quantize/config-writer.ts +10 -3
  233. package/src/quantize/rotate.ts +4 -4
  234. package/src/quantize/trellis.ts +586 -0
  235. package/src/responses.ts +28 -1
  236. package/src/runtime-config.ts +18 -4
  237. package/src/sampler.ts +40 -29
  238. package/src/serve/batch-scheduler.ts +8 -1671
  239. package/src/serve/chat-request.ts +391 -0
  240. package/src/serve/chat-stage.ts +195 -0
  241. package/src/serve/checkpoint-identity.ts +64 -0
  242. package/src/serve/completion-executor.ts +15 -7
  243. package/src/serve/discovery-routes.ts +12 -13
  244. package/src/serve/generation-gateway.ts +183 -213
  245. package/src/serve/http.ts +159 -0
  246. package/src/serve/inference-request.ts +79 -0
  247. package/src/serve/isolate.ts +297 -105
  248. package/src/serve/lab-routes.ts +3 -0
  249. package/src/serve/lane-registry.ts +2 -1
  250. package/src/serve/model-admin-routes.ts +7 -5
  251. package/src/serve/model-binding.ts +43 -0
  252. package/src/serve/model-host.ts +511 -0
  253. package/src/serve/openai-wire.ts +103 -0
  254. package/src/serve/parent-application.ts +56 -0
  255. package/src/serve/pipeline.ts +33 -0
  256. package/src/serve/preparation.ts +33 -0
  257. package/src/serve/request-plan.ts +23 -18
  258. package/src/serve/request-prep.ts +344 -0
  259. package/src/serve/response-lease.ts +24 -0
  260. package/src/serve/responses-client.ts +69 -0
  261. package/src/serve/session-completion-engine.ts +110 -0
  262. package/src/serve/session-search.ts +1 -1
  263. package/src/serve/static-routes.ts +5 -5
  264. package/src/serve/text-completion-stage.ts +104 -0
  265. package/src/serve/token-streams.ts +519 -0
  266. package/src/server.ts +335 -2711
  267. package/src/spec/assistant-rows.ts +92 -0
  268. package/src/spec/assistant-source.ts +31 -55
  269. package/src/spec/cache-identity.ts +22 -0
  270. package/src/spec/deepspec-rows.ts +29 -0
  271. package/src/spec/deepspec-source.ts +16 -14
  272. package/src/spec/dflash-source.ts +36 -12
  273. package/src/spec/drafter.ts +153 -142
  274. package/src/spec/dspark/ab-stats.ts +3 -3
  275. package/src/spec/dspark/calibration.ts +2 -2
  276. package/src/spec/dspark/deepspec-module.ts +97 -122
  277. package/src/spec/dspark/generate-dflash.ts +2 -2
  278. package/src/spec/dspark/generate.ts +2 -2
  279. package/src/spec/dspark/loader.ts +1 -1
  280. package/src/spec/dspark/module-dflash.ts +129 -172
  281. package/src/spec/dspark/module.ts +3 -3
  282. package/src/spec/dspark/quantize-drafter.ts +1 -1
  283. package/src/spec/generate.ts +1 -1
  284. package/src/spec/glm52-mtp-graph.ts +29 -0
  285. package/src/spec/glm52-mtp-rows.ts +132 -0
  286. package/src/spec/glm52-mtp-source.ts +54 -137
  287. package/src/spec/glm52-mtp-state.ts +33 -0
  288. package/src/spec/ngram-source.ts +93 -21
  289. package/src/spec/projected-draft-rows.ts +73 -0
  290. package/src/spec/qwen-mtp-module.ts +171 -0
  291. package/src/spec/qwen-mtp-rows.ts +219 -0
  292. package/src/spec/qwen-mtp-source.ts +147 -195
  293. package/src/spec/qwen-mtp-state.ts +30 -0
  294. package/src/spec/serve-loop.ts +201 -201
  295. package/src/spec/source.ts +110 -13
  296. package/src/spec/two-model-rows.ts +61 -0
  297. package/src/spec/two-model.ts +44 -102
  298. package/src/ssd-cache.ts +99 -19
  299. package/src/ssd-durability.ts +36 -20
  300. package/src/tool-call.ts +51 -14
  301. package/src/train/flash-cce.ts +2 -2
  302. package/src/train/forward.ts +6 -0
  303. package/src/train/loss.ts +1 -1
  304. package/src/train/prefix-shared.ts +5 -1
  305. package/src/train/segmented.ts +5 -3
  306. package/src/train/trainer.ts +6 -6
  307. package/src/vision/prompt.ts +1 -1
  308. package/src/vision/qwen3vl-tower.ts +52 -10
  309. package/src/web/app.html +1 -1
  310. package/src/web/app.js +62 -60
  311. package/src/web/src/adapters-panel.ts +3 -3
  312. package/src/web/src/assistant.ts +2 -36
  313. package/src/web/src/chat.ts +4 -4
  314. package/src/web/src/composer.ts +4 -4
  315. package/src/web/src/hub.ts +1 -1
  316. package/src/web/src/markdown.ts +0 -0
  317. package/src/web/src/memory-panel.ts +1 -1
  318. package/src/web/src/model-picker.ts +1 -1
  319. package/src/web/src/palette.ts +1 -1
  320. package/src/web/src/protocol.ts +3 -39
  321. package/src/web/src/sessions.ts +1 -1
  322. package/src/web/src/ui-catalog.ts +4 -18
  323. package/src/web-assets.ts +47 -0
  324. package/src/weights.ts +55 -9
  325. package/docs/archive/investigations/curve-runs/bisector-route.json +0 -98
  326. package/docs/archive/investigations/curve-runs/bisector-trace.json +0 -1393
  327. package/docs/archive/investigations/curve-runs/curve-terrain.json +0 -1179
  328. package/docs/archive/investigations/curve-runs/curve-terrain.svg +0 -42
  329. package/docs/archive/investigations/hlg-ab.html +0 -34
  330. package/docs/archive/investigations/hlg-explorer.html +0 -238
  331. package/docs/archive/investigations/hlg-figs/cliff.svg +0 -1
  332. package/docs/archive/investigations/hlg-figs/cross.svg +0 -1
  333. package/docs/archive/investigations/hlg-figs/edges.svg +0 -1
  334. package/docs/archive/investigations/hlg-figs/k20-grids.svg +0 -1
  335. package/docs/archive/investigations/hlg-figs/k20-peaks.svg +0 -1
  336. package/docs/archive/investigations/hlg-figs/lw-curve.svg +0 -1
  337. package/docs/archive/investigations/hlg-figs/terrain.svg +0 -1
  338. package/docs/archive/investigations/hlg-report.html +0 -262
  339. package/docs/archive/investigations/hlg-runs/hlg-ab.json +0 -149
  340. package/docs/archive/investigations/hlg-runs/hlg-blind-prompts.md +0 -87
  341. package/docs/archive/investigations/hlg-runs/hlg-blind.json +0 -107
  342. package/docs/archive/investigations/hlg-runs/hlg-cliff.json +0 -197
  343. package/docs/archive/investigations/hlg-runs/hlg-cross.json +0 -505
  344. package/docs/archive/investigations/hlg-runs/hlg-edge.json +0 -388
  345. package/docs/archive/investigations/hlg-runs/hlg-eval1.json +0 -25
  346. package/docs/archive/investigations/hlg-runs/hlg-frontier.json +0 -86
  347. package/docs/archive/investigations/hlg-runs/hlg-grid.json +0 -242
  348. package/docs/archive/investigations/hlg-runs/hlg-grid2.json +0 -343
  349. package/docs/archive/investigations/hlg-runs/hlg-lw-coarse.json +0 -110
  350. package/docs/archive/investigations/hlg-runs/hlg-map.json +0 -383
  351. package/docs/archive/investigations/hlg-runs/run0-original.log +0 -159
  352. package/docs/archive/investigations/hlg-runs/runA-recommended.log +0 -131
  353. package/docs/archive/investigations/hlg-runs/runAA-cliff-cpm5.log +0 -26
  354. package/docs/archive/investigations/hlg-runs/runAB-grid2-e4b.log +0 -18
  355. package/docs/archive/investigations/hlg-runs/runAC-peak-e4b-partial.log +0 -25
  356. package/docs/archive/investigations/hlg-runs/runAD-ab-samples.log +0 -8
  357. package/docs/archive/investigations/hlg-runs/runAE-edge-e4b.log +0 -51
  358. package/docs/archive/investigations/hlg-runs/runAF-logit-calibration.log +0 -10
  359. package/docs/archive/investigations/hlg-runs/runAG-eval-config2.log +0 -16
  360. package/docs/archive/investigations/hlg-runs/runB-gain-variants.log +0 -192
  361. package/docs/archive/investigations/hlg-runs/runC-pivots.log +0 -162
  362. package/docs/archive/investigations/hlg-runs/runD-oetf.log +0 -132
  363. package/docs/archive/investigations/hlg-runs/runE-eotf.log +0 -162
  364. package/docs/archive/investigations/hlg-runs/runF-shaper-working.log +0 -86
  365. package/docs/archive/investigations/hlg-runs/runG-shaper-sweep.log +0 -101
  366. package/docs/archive/investigations/hlg-runs/runH-wsweep.log +0 -176
  367. package/docs/archive/investigations/hlg-runs/runI-asweep.log +0 -176
  368. package/docs/archive/investigations/hlg-runs/runJ-knobmap.log +0 -49
  369. package/docs/archive/investigations/hlg-runs/runK-frontier.log +0 -19
  370. package/docs/archive/investigations/hlg-runs/runL-widemap.log +0 -65
  371. package/docs/archive/investigations/hlg-runs/runM-frontier-emb.log +0 -15
  372. package/docs/archive/investigations/hlg-runs/runN-verify.log +0 -116
  373. package/docs/archive/investigations/hlg-runs/runO-frontier-seed5000.log +0 -15
  374. package/docs/archive/investigations/hlg-runs/runP-grid-AxSm.log +0 -15
  375. package/docs/archive/investigations/hlg-runs/runQ-grid-tgW.log +0 -15
  376. package/docs/archive/investigations/hlg-runs/runR-blind-transcript.log +0 -79
  377. package/docs/archive/investigations/hlg-runs/runS-cross-coherence.log +0 -33
  378. package/docs/archive/investigations/hlg-runs/runT-cross-cpm5.log +0 -33
  379. package/docs/archive/investigations/hlg-runs/runU-cross-12b.log +0 -33
  380. package/docs/archive/investigations/hlg-runs/runV-lw-e4b.log +0 -22
  381. package/docs/archive/investigations/hlg-runs/runW-lw-12b.log +0 -22
  382. package/docs/archive/investigations/hlg-runs/runX-lw-cpm5.log +0 -22
  383. package/docs/archive/investigations/hlg-runs/runY-cliff-e4b.log +0 -26
  384. package/docs/archive/investigations/hlg-runs/runZ-cliff-12b.log +0 -26
  385. package/docs/archive/mlx-bun-lab-report.html +0 -275
  386. package/docs/archive/wwdc-mlx-bun.html +0 -848
  387. package/docs/design/adapters-end-to-end.md +0 -205
  388. package/docs/design/batching-perf-path.md +0 -255
  389. package/docs/design/batching-v2-plan.md +0 -529
  390. package/docs/design/bucketing-stage.md +0 -197
  391. package/docs/design/compat-cli-surface-design.md +0 -230
  392. package/docs/design/docs-reorg-plan.md +0 -165
  393. package/docs/design/dspark-serving-program.md +0 -371
  394. package/docs/design/dspark-speculative-decoding.md +0 -318
  395. package/docs/design/grammar-spec-batching-integration.md +0 -301
  396. package/docs/design/memory-inference-path.md +0 -422
  397. package/docs/design/memory-synthesis.md +0 -287
  398. package/docs/design/memory-system.md +0 -237
  399. package/docs/design/mlx-lm-serving-execution-seam.md +0 -81
  400. package/docs/design/mlx-lm-tool-parity-plan.md +0 -545
  401. package/docs/design/omlx-adoption-map.md +0 -98
  402. package/docs/design/orpo-dynamic-lambda.md +0 -220
  403. package/docs/design/orpo-future-enhancements.md +0 -340
  404. package/docs/design/paged-kv-cache.md +0 -214
  405. package/docs/design/parallel-slots.md +0 -567
  406. package/docs/design/parity-tier-dag.md +0 -161
  407. package/docs/design/repo-cleanup-plan.md +0 -231
  408. package/docs/design/runtime-isolation.md +0 -263
  409. package/docs/design/segmented-backward-training.md +0 -543
  410. package/docs/design/spec-decode-larger-targets.md +0 -229
  411. package/docs/design/ssd-kv-cold-tier.md +0 -415
  412. package/docs/design/structured-output.md +0 -484
  413. package/docs/design/tauri-desktop-app.md +0 -127
  414. package/docs/design/the-dreaming-master-plan.md +0 -445
  415. package/docs/design/turboquant-kv.md +0 -235
  416. package/docs/design/turboquant-weights.md +0 -467
  417. package/docs/design/w4a16-compute-precision-spike.md +0 -232
  418. package/docs/design/web-chat-beat-matrix.md +0 -315
  419. package/docs/design/write-pipeline-entity.md +0 -468
  420. package/docs/planning/IDEAS.md +0 -69
  421. package/docs/reference/embedding.md +0 -197
  422. package/docs/reference/features-matrix.md +0 -101
  423. package/docs/reference/orpo-quickstart.md +0 -135
  424. package/src/flags.ts +0 -11
  425. /package/docs/{design → archive/investigations}/colibri-glm52-port.md +0 -0
  426. /package/docs/{planning → archive/planning}/chunk-task-roadmap.md +0 -0
  427. /package/docs/{planning → archive/planning}/curve-sampler-research-plan.md +0 -0
  428. /package/docs/{planning → archive/planning}/memory-docs-and-dag-plan.md +0 -0
  429. /package/docs/{planning → archive/planning}/pi-integration-review.md +0 -0
  430. /package/docs/{planning → archive/planning}/release-notes-v0.2.0.md +0 -0
  431. /package/src/{memory-plan.ts → admission-plan.ts} +0 -0
  432. /package/{docs/archive/investigations → src/serve/assets}/curve-terrain.html +0 -0
  433. /package/{docs/dag → src/serve/assets}/training-inference-map.html +0 -0
@@ -0,0 +1,391 @@
1
+ // Wire-level chat/completion request shapes and the pure per-request
2
+ // helpers: message normalization, sampling-field coercion, grammar-degrade
3
+ // prompt injection, and the mlx_lm.server-parity validators. No model, no
4
+ // server state. Extracted from src/server.ts (repo-taming Phase 4).
5
+ import type { ChatMessage, ToolDefinition } from "../chat-template";
6
+ import type { HlgConfig } from "../sampler";
7
+ import { RequestError } from "./pipeline";
8
+
9
+ export interface ChatRequestParams {
10
+ messages: ChatMessage[];
11
+ stream?: boolean;
12
+ max_tokens?: number;
13
+ max_completion_tokens?: number;
14
+ temperature?: number;
15
+ top_p?: number;
16
+ top_k?: number;
17
+ seed?: number;
18
+ repetition_penalty?: number;
19
+ /** mlx-lm extension: recent-token window for repetition_penalty
20
+ * (default 20; 0 = whole history, Python `[-0:]` semantics). */
21
+ repetition_context_size?: number;
22
+ /** min-p sampling (mlx_lm.server's `min_p`): keep tokens whose probability
23
+ * is ≥ min_p · p(top token). 0 = off. */
24
+ min_p?: number;
25
+ /** XTC sampling (mlx_lm.server names): with probability `xtc_probability`
26
+ * per step, remove every token above `xtc_threshold` except the least
27
+ * likely of them. EOS + the newline token are always exempt (the server
28
+ * injects them as xtc special tokens, matching mlx_lm.server). */
29
+ xtc_probability?: number;
30
+ xtc_threshold?: number;
31
+ /** OpenAI logit_bias: {tokenId: additive bias}. JSON object keys arrive as
32
+ * strings; coerced to int keys / float values like mlx-lm (400 on failure). */
33
+ logit_bias?: Record<string, number>;
34
+ /** OpenAI presence/frequency penalties + mlx-lm's context-size extensions
35
+ * (window of recent tokens the penalty looks at; default 20). */
36
+ presence_penalty?: number;
37
+ presence_context_size?: number;
38
+ frequency_penalty?: number;
39
+ frequency_context_size?: number;
40
+ /** mlx_lm.server logprobs: `logprobs` is a BOOL (even on /v1/completions —
41
+ * not OpenAI's legacy int), `top_logprobs` an int in [0, 11] or the -1
42
+ * "unset" sentinel (server.py validates exactly that; OpenAI's cap is 20,
43
+ * mlx-lm's is 11 — we copy the reference). Non-stream responses carry
44
+ * mlx-lm's logprobs block; stream chunks never do (reference behavior). */
45
+ logprobs?: boolean;
46
+ top_logprobs?: number;
47
+ /** OpenAI stop sequences: plain string or array (spec allows up to 4).
48
+ * Matched on DECODED text, not token ids — see StopMatcher. */
49
+ stop?: string | string[];
50
+ tools?: ToolDefinition[];
51
+ tool_choice?: "auto" | "none" | { type: string; function?: { name: string } };
52
+ /** Forwarded to HF chat templates, matching optiq serve. MiniCPM5 uses
53
+ * enable_thinking to select direct answers vs the <think> channel; Qwen3.8
54
+ * adds preserve_thinking (keep think blocks from history; template default
55
+ * true — better prompt-cache reuse in agent loops). */
56
+ chat_template_kwargs?: {
57
+ enable_thinking?: boolean;
58
+ preserve_thinking?: boolean;
59
+ [key: string]: unknown;
60
+ };
61
+ /** OpenAI reasoning control. For models with a switchable <think> channel
62
+ * (Qwen3.5/MiniCPM5) it gates enable_thinking: "none" → off, any level → on.
63
+ * This is what Pi sends when the provider advertises reasoning. For
64
+ * templates that read a reasoning-depth variable (Qwen3.8: xhigh/medium/
65
+ * low), the level ALSO maps into the template via qwenReasoningEffort —
66
+ * "xhigh" accepted as a first-class value (Qwen3.8's default/deepest). */
67
+ reasoning_effort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
68
+ /** Mounted LoRA adapter selection: "id", "a+b" (stacked), or "none". */
69
+ adapter?: string;
70
+ /** HLG tone-curve sampling override (per request). Snake_case wire fields,
71
+ * merged over the server's --hlg-sampling config. docs/archive/hlg-sampling.md. */
72
+ hlg?: {
73
+ enabled?: boolean;
74
+ width?: number;
75
+ shoulder?: number;
76
+ toe?: number;
77
+ pivot_offset?: number;
78
+ };
79
+ /** OpenAI structured output: {type:"json_object"} | {type:"json_schema",
80
+ * json_schema:{name,schema,strict?}} | {type:"text"}. Enforced at the
81
+ * sampler via xgrammar token-bitmasks (src/grammar.ts). L2-class (oMLX
82
+ * oracle). On compile failure, degrades to a system-prompt injection +
83
+ * Warning header (oMLX parity), never 500. */
84
+ response_format?: unknown;
85
+ /** vLLM/oMLX grammar aliases (all compiled via xgrammar): raw EBNF/LARK
86
+ * grammar, regex, enum choice, and a bare JSON-schema object. Precedence
87
+ * (guided_grammar > json_schema > json_object > structured_outputs >
88
+ * guided_regex > guided_choice) mirrors oMLX _effective_guided_grammar. */
89
+ guided_grammar?: string;
90
+ guided_regex?: string;
91
+ guided_choice?: string[];
92
+ structured_outputs?: unknown;
93
+ }
94
+
95
+ /** POST /v1/completions body (mlx_lm.server's raw text completion — no chat
96
+ * template). Sampling/penalty/stop fields are the same names as ChatRequestParams;
97
+ * `prompt` replaces `messages`. mlx_lm.server accepts only a STRING prompt
98
+ * (it calls `tokenizer.encode(request.prompt)` directly) — token-array
99
+ * prompts are rejected there too, so we match. No `echo` (mlx-lm has none). */
100
+ export type TextCompletionParams = Omit<ChatRequestParams, "messages" | "tools" | "tool_choice"> & {
101
+ prompt?: unknown;
102
+ };
103
+
104
+
105
+ /** Per-field default HLG knobs when enabling without specifying them. */
106
+ export const HLG_DEFAULTS = { width: 4, shoulder: 4, toe: 6, pivotOffset: 6 } as const;
107
+
108
+ /** Resolve the effective HLG config: a per-request `hlg` object overrides the
109
+ * server's --hlg-sampling default field-by-field. Returns undefined (HLG off)
110
+ * unless enabled by the request or the server. */
111
+ export function resolveHlg(
112
+ reqHlg: ChatRequestParams["hlg"],
113
+ serverHlg: HlgConfig | undefined,
114
+ ): HlgConfig | undefined {
115
+ const enabled = reqHlg?.enabled ?? serverHlg?.enabled ?? false;
116
+ if (!enabled) return undefined;
117
+ const base = serverHlg ?? HLG_DEFAULTS;
118
+ return {
119
+ enabled: true,
120
+ width: reqHlg?.width ?? base.width,
121
+ shoulder: reqHlg?.shoulder ?? base.shoulder,
122
+ toe: reqHlg?.toe ?? base.toe,
123
+ pivotOffset: reqHlg?.pivot_offset ?? base.pivotOffset,
124
+ pivot: "top",
125
+ };
126
+ }
127
+
128
+ /** True when a rendered prompt ends INSIDE an unclosed `<think>` block — the
129
+ * generation prompt primed reasoning (Qwen3.5 / MiniCPM5 with thinking on),
130
+ * so the model continues the chain-of-thought and emits only the closing
131
+ * `</think>`. Seeds ThinkingTagSplitter so reasoning is split out correctly.
132
+ * Thinking-off primes a CLOSED empty block (`<think>\n\n</think>`), and
133
+ * no-thinking templates have no `<think>` at all — both return false. */
134
+ export function promptEndsInOpenThink(rendered: string): boolean {
135
+ const open = rendered.lastIndexOf("<think>");
136
+ return open !== -1 && open > rendered.lastIndexOf("</think>");
137
+ }
138
+
139
+ /** Concatenate the text of an OpenAI content-part array, ignoring non-text
140
+ * parts. Tolerant of the part shapes clients actually send: `{type:"text",
141
+ * text}` (OpenAI/pi), `{type:"input_text", text}` (Responses-style), or any
142
+ * part carrying a string `text`. */
143
+ export function contentPartsToText(parts: Array<Record<string, unknown>>): string {
144
+ return parts
145
+ .map((p) => (p && typeof p.text === "string" ? p.text : ""))
146
+ .join("");
147
+ }
148
+
149
+ /** True if any content part is media — image OR audio — so the multimodal
150
+ * path must keep the array form for extractImages/extractAudio. */
151
+ export function hasMediaPart(parts: Array<Record<string, unknown>>): boolean {
152
+ return parts.some(
153
+ (p) =>
154
+ p &&
155
+ (p.type === "image" || p.type === "image_url" ||
156
+ p.type === "audio" || p.type === "input_audio" || p.type === "audio_url" ||
157
+ p.type === "video" || p.type === "video_url"),
158
+ );
159
+ }
160
+
161
+ /** OpenAI sends assistant tool_call arguments as JSON strings; the
162
+ * template renders the object form natively — normalize before render.
163
+ *
164
+ * Two more wire-format → template-format fixes, both "match the format the
165
+ * model's chat template expects":
166
+ *
167
+ * 1. Map the OpenAI reasoning-model "developer" role to "system": pi-ai (and
168
+ * OpenAI's own SDKs) rename the system prompt to `developer` whenever a
169
+ * model advertises reasoning, but our chat templates only know
170
+ * system/user/assistant/tool and raise "Unexpected message role." This is
171
+ * why Qwen3.5/MiniCPM5 chat got no messages while Gemma (non-reasoning, so
172
+ * pi keeps `system`) worked. `developer` IS the system prompt, so the remap
173
+ * is semantics-preserving.
174
+ *
175
+ * 2. Flatten text-only content-part arrays to a plain string. pi (and any
176
+ * OpenAI multimodal client) sends user content as `[{type:"text",text}]`,
177
+ * but non-vision chat templates expect `content` to be a STRING and render
178
+ * nothing for an array — so the user's turn silently vanishes and the model
179
+ * replies "I don't see any message." Arrays that carry a media part
180
+ * (image or audio) are left intact for the multimodal path
181
+ * (extractImages/extractAudio); only text-only arrays are collapsed
182
+ * here, which is a no-op for the vision templates too. */
183
+ export function normalizeMessages(messages: ChatMessage[]): ChatMessage[] {
184
+ return messages.map((raw) => {
185
+ let m = raw.role === "developer" ? { ...raw, role: "system" } : raw;
186
+ if (Array.isArray(m.content) && !hasMediaPart(m.content)) {
187
+ m = { ...m, content: contentPartsToText(m.content) };
188
+ }
189
+ // Pi and mlx-lm return `reasoning`; Qwen templates read
190
+ // `reasoning_content`. Keep the original field and immutable history,
191
+ // and let an explicitly supplied canonical string (including "") win.
192
+ if (typeof m.reasoning_content !== "string" && typeof m.reasoning === "string")
193
+ m = { ...m, reasoning_content: m.reasoning };
194
+ if (!m.tool_calls) return m;
195
+ return {
196
+ ...m,
197
+ tool_calls: m.tool_calls.map((tc) => ({
198
+ ...tc,
199
+ function: {
200
+ ...tc.function,
201
+ arguments:
202
+ typeof tc.function.arguments === "string"
203
+ ? (JSON.parse(tc.function.arguments) as Record<string, unknown>)
204
+ : tc.function.arguments,
205
+ },
206
+ })),
207
+ };
208
+ });
209
+ }
210
+
211
+ /** Coerce a wire `logit_bias` ({"tokenId": bias}) to numeric keys/values —
212
+ * mlx_lm.server's `{int(k): float(v)}` coercion; throws its exact error
213
+ * message on anything non-numeric (surfaced as a 400). JSON object keys
214
+ * always arrive as strings, hence the coercion. */
215
+ export function parseLogitBias(
216
+ raw: Record<string, number> | undefined | null,
217
+ ): Record<number, number> | undefined {
218
+ if (raw == null) return undefined;
219
+ if (typeof raw !== "object" || Array.isArray(raw))
220
+ throw new Error("logit_bias must be a dict of int to float");
221
+ const out: Record<number, number> = {};
222
+ for (const [k, v] of Object.entries(raw)) {
223
+ const id = Number(k);
224
+ const bias = Number(v);
225
+ if (!Number.isInteger(id) || !Number.isFinite(bias))
226
+ throw new Error("logit_bias must be a dict of int to float");
227
+ out[id] = bias;
228
+ }
229
+ return Object.keys(out).length ? out : undefined;
230
+ }
231
+
232
+ /** Default seed for a request that didn't pin one. `Date.now()` alone is NOT
233
+ * request-unique: under `--batch N` the batch lane serves ONLY default-seed
234
+ * requests (explicit-seed requests use the serial mechanism, see
235
+ * GenerationGateway.place),
236
+ * so two identical prompts arriving in the same millisecond would share a seed
237
+ * and — with per-row RNG keyed as stepKey(seed, generatedCount) — produce
238
+ * byte-identical completions, silently collapsing best-of-N diversity. Mix a
239
+ * per-process Weyl counter (golden-ratio increment, period 2^32) into the
240
+ * timestamp so every call yields a distinct uint32 within any given ms.
241
+ * Determinism contract unchanged: reproducibility is only promised for an
242
+ * EXPLICIT request seed (`req.seed ?? nextDefaultSeed()` — explicit wins,
243
+ * byte-identical to before); a default seed is fresh entropy per request. */
244
+ let seedWeyl = 0;
245
+ export function nextDefaultSeed(): number {
246
+ seedWeyl = (seedWeyl + 0x9e3779b9) >>> 0;
247
+ return ((Date.now() & 0xffffffff) ^ seedWeyl) >>> 0;
248
+ }
249
+
250
+ /** mlx_lm.server's logprobs request validation, copied exactly (server.py
251
+ * APIHandler.validate_model_parameters: `_validate("logprobs", bool)` and
252
+ * `_validate("top_logprobs", int, min_val=0, max_val=11, whitelist=[-1])`
253
+ * with defaults logprobs=False / top_logprobs=-1). Returns the reference's
254
+ * error message (→ 400) or null when valid. Note mlx-lm caps top_logprobs
255
+ * at 11, not OpenAI's 20 — we mirror the reference. */
256
+ /** Build a degrade-path system-prompt instruction for JSON output, mirroring
257
+ * oMLX's api.tool_calling.build_json_system_prompt (used when xgrammar
258
+ * compile fails — the response_format degrades to prompt injection rather
259
+ * than a 500, oMLX parity). Returns null for {type:"text"} / unset. */
260
+ export function degradeJsonSystemPrompt(body: ChatRequestParams): string | null {
261
+ if (body.guided_grammar) {
262
+ return "You must respond with text matching this grammar:\n\n" + body.guided_grammar;
263
+ }
264
+ const rf = body.response_format as { type?: string; json_schema?: { name?: string; description?: string; schema?: unknown } } | undefined;
265
+ if (rf && typeof rf === "object" && rf.type === "json_object") {
266
+ return "You must respond with valid JSON only. " +
267
+ "Do not include any explanation or text outside the JSON object.";
268
+ }
269
+ const schemaSpec = rf && typeof rf === "object" && rf.type === "json_schema"
270
+ ? rf.json_schema
271
+ : body.structured_outputs && typeof body.structured_outputs === "object"
272
+ ? { schema: body.structured_outputs }
273
+ : null;
274
+ if (schemaSpec) {
275
+ const spec = schemaSpec;
276
+ const schema = spec.schema ?? body.structured_outputs ?? {};
277
+ const name = spec.name ?? "response";
278
+ const description = spec.description ?? "";
279
+ let prompt = `You must respond with valid JSON matching the '${name}' schema.`;
280
+ if (description) prompt += ` ${description}`;
281
+ prompt += `\n\nJSON Schema:\n\`\`\`json\n${JSON.stringify(schema, null, 2)}\n\`\`\`\n\n` +
282
+ "Respond with only the JSON object, no additional text or explanation.";
283
+ return prompt;
284
+ }
285
+ if (body.guided_regex) {
286
+ return "You must respond with text matching this regular expression:\n\n" +
287
+ body.guided_regex;
288
+ }
289
+ if (body.guided_choice?.length) {
290
+ return "You must respond with exactly one of these choices:\n\n" +
291
+ body.guided_choice.map((choice) => JSON.stringify(choice)).join("\n");
292
+ }
293
+ return null;
294
+ }
295
+
296
+ export function applyGrammarDegrade(
297
+ body: ChatRequestParams,
298
+ degradeHint: string,
299
+ ): { body: ChatRequestParams; warning: string } {
300
+ const content = degradeJsonSystemPrompt(body) ??
301
+ "Follow the requested output constraint exactly.";
302
+ return {
303
+ warning:
304
+ `grammar not enforced: ${degradeHint} - falling back to prompt injection`,
305
+ body: {
306
+ ...body,
307
+ messages: [{ role: "system", content }, ...body.messages],
308
+ },
309
+ };
310
+ }
311
+
312
+ export function validateLogprobsParams(body: {
313
+ logprobs?: unknown;
314
+ top_logprobs?: unknown;
315
+ }): string | null {
316
+ const lp = body.logprobs ?? false;
317
+ if (typeof lp !== "boolean") return "logprobs must be of type bool";
318
+ const tl = body.top_logprobs ?? -1;
319
+ if (typeof tl !== "number" || !Number.isInteger(tl))
320
+ return "top_logprobs must be of type int";
321
+ if (tl === -1) return null; // the "unset" whitelist sentinel
322
+ if (tl < 0) return "top_logprobs must be at least 0";
323
+ if (tl > 11) return "top_logprobs must be at most 11";
324
+ return null;
325
+ }
326
+
327
+ const REASONING_EFFORT_LEVELS =
328
+ ["none", "minimal", "low", "medium", "high", "xhigh"] as const;
329
+
330
+ /** Validate `reasoning_effort` against the accepted level names. An invalid
331
+ * string must NOT pass through: resolveEnableThinking treats any defined
332
+ * value ≠ "none" as thinking-ON, so a typo like "hihg" silently flipped the
333
+ * thinking channel (and its hotter temperature) on thinking-off models and
334
+ * burned a full default-depth reasoning budget (2026-08-18 review). Same
335
+ * contract style as validateLogprobsParams above. */
336
+ export function validateReasoningEffort(body: {
337
+ reasoning_effort?: unknown;
338
+ }): string | null {
339
+ const v = body.reasoning_effort;
340
+ if (v === undefined || v === null) return null;
341
+ if (typeof v !== "string" || !(REASONING_EFFORT_LEVELS as readonly string[]).includes(v))
342
+ return "reasoning_effort must be one of " +
343
+ "'none', 'minimal', 'low', 'medium', 'high', 'xhigh'";
344
+ return null;
345
+ }
346
+
347
+ // ---------------------------------------------------------------------------
348
+ // The request objects a caller constructs and hands to the pipeline. The
349
+ // constructor is the validation boundary: structural checks that need no
350
+ // model (mlx_lm.server parity messages) throw RequestError(400) here, so a
351
+ // stage never sees a malformed request.
352
+
353
+ export class ChatRequest {
354
+ readonly params: ChatRequestParams;
355
+
356
+ constructor(params: ChatRequestParams) {
357
+ if (!Array.isArray(params.messages) || params.messages.length === 0)
358
+ throw new RequestError(400, "messages required");
359
+ const lpParamError = validateLogprobsParams(params);
360
+ if (lpParamError) throw new RequestError(400, lpParamError);
361
+ const effortError = validateReasoningEffort(params);
362
+ if (effortError) throw new RequestError(400, effortError);
363
+ this.params = params;
364
+ }
365
+
366
+ get stream(): boolean {
367
+ return this.params.stream === true;
368
+ }
369
+ }
370
+
371
+ /** POST /v1/completions — raw text, no chat template (mlx_lm.server's
372
+ * request_type "text"). Only a non-empty STRING prompt is accepted. */
373
+ export class TextCompletionRequest {
374
+ readonly params: TextCompletionParams & { prompt: string };
375
+
376
+ constructor(params: TextCompletionParams) {
377
+ if (typeof params.prompt !== "string" || params.prompt.length === 0)
378
+ throw new RequestError(
379
+ 400,
380
+ "prompt (a non-empty string) is required " +
381
+ "(token-array prompts are not accepted, matching mlx_lm.server)",
382
+ );
383
+ const lpParamError = validateLogprobsParams(params);
384
+ if (lpParamError) throw new RequestError(400, lpParamError);
385
+ this.params = params as TextCompletionParams & { prompt: string };
386
+ }
387
+
388
+ get stream(): boolean {
389
+ return this.params.stream === true;
390
+ }
391
+ }
@@ -0,0 +1,195 @@
1
+ // ChatStage: ChatRequest in, InferenceRequest out. Turns a chat conversation
2
+ // (messages, tools, media parts, sampling fields) into the prompt token ids,
3
+ // media embeddings, resolved generation options, adapter selection, grammar
4
+ // controller, and the token→text pipeline the model needs. Every chat-shaped
5
+ // surface (OpenAI chat, Anthropic Messages, Responses) runs this stage; the
6
+ // wire format is not its business.
7
+ import type { PromptCache } from "../prompt-cache";
8
+ import {
9
+ applyGrammarDegrade,
10
+ type ChatRequest,
11
+ type ChatRequestParams,
12
+ } from "./chat-request";
13
+ import type { InferenceRequest } from "./inference-request";
14
+ import type { ServingContext } from "./model-host";
15
+ import type { BuiltPrompt, ModelPromptBuilder } from "./model-binding";
16
+ import { modelPromptBuilder } from "../backends/mlx/model-serving";
17
+ import { RequestError } from "./pipeline";
18
+ import { RequestOwnership } from "./request-plan";
19
+ import type { RequestPrep } from "./request-prep";
20
+ import { StopMatcher, ThinkingTagSplitter } from "./token-streams";
21
+
22
+ export class ChatStage {
23
+ constructor(
24
+ private readonly ctx: ServingContext,
25
+ private readonly prep: RequestPrep,
26
+ private readonly promptCache: Pick<PromptCache, "peekPrefixLen">,
27
+ /** Admission ceiling (fit() / the GLM memory plan), resolved once. */
28
+ private readonly contextLimit: number | null,
29
+ /** `serve --adapter <dir>` startup default; a request's explicit
30
+ * `adapter` (incl. "none") wins. */
31
+ private readonly defaultAdapter?: string,
32
+ private readonly preparation?: import("./preparation").PreparationExecutor,
33
+ private readonly buildPrompt: ModelPromptBuilder = modelPromptBuilder(ctx),
34
+ ) {}
35
+
36
+ async run(request: ChatRequest, requestId?: string, signal?: AbortSignal): Promise<InferenceRequest> {
37
+ signal?.throwIfAborted();
38
+ const body = request.params;
39
+ const media = body.messages.some((message) => Array.isArray(message.content) &&
40
+ message.content.some((part) => part.type !== "text"));
41
+ const native = media || body.response_format != null || !!body.guided_grammar ||
42
+ !!body.guided_regex || !!body.guided_choice?.length || body.structured_outputs != null;
43
+ const build = async () => {
44
+ signal?.throwIfAborted();
45
+ const result = await this.runPrepared(request, requestId);
46
+ if (signal?.aborted) {
47
+ result.plan.ownership.dispose();
48
+ signal.throwIfAborted();
49
+ }
50
+ return result;
51
+ };
52
+ if (!native || !this.preparation) return build();
53
+ let reservation = await this.preparation.reserve?.(media ? "media" : "constraint", signal);
54
+ try {
55
+ const result = await this.preparation.run(build, signal);
56
+ if (reservation) result.plan.ownership.retain(reservation);
57
+ reservation = undefined;
58
+ return result;
59
+ } finally { reservation?.dispose(); }
60
+ }
61
+
62
+ private async runPrepared(request: ChatRequest, requestId?: string): Promise<InferenceRequest> {
63
+ const { ctx, prep } = this;
64
+ let body = request.params;
65
+ const id = requestId ?? `chatcmpl-${crypto.randomUUID()}`;
66
+ const tools =
67
+ body.tool_choice === "none" ? null : (body.tools?.length ? body.tools : null);
68
+ const ownership = new RequestOwnership();
69
+ const warnings: string[] = [];
70
+ const reject = (status: number, message: string): never => {
71
+ ownership.dispose();
72
+ throw new RequestError(status, message);
73
+ };
74
+
75
+ // Grammar-constrained decoding (src/grammar.ts). Compile BEFORE prompt
76
+ // rendering: on the degrade path (compile failed but a constraint was
77
+ // requested) inject a system message instructing valid JSON so the
78
+ // model still best-efforts schema-conformant output (oMLX parity —
79
+ // _compile_grammar_for_request returning None + build_json_system_prompt).
80
+ let grammarCtrl: import("../grammar").GrammarController | null = null;
81
+ const grammarReq =
82
+ body.response_format != null || !!body.guided_grammar ||
83
+ !!body.guided_regex || !!body.guided_choice?.length ||
84
+ body.structured_outputs != null;
85
+ if (grammarReq) {
86
+ const g = await prep.compileGrammarForRequest(body);
87
+ grammarCtrl = ownership.own(g.controller);
88
+ if (!g.controller && g.degradeHint) {
89
+ const degraded = applyGrammarDegrade(body, g.degradeHint);
90
+ warnings.push(degraded.warning);
91
+ body = degraded.body;
92
+ }
93
+ }
94
+
95
+ let built: BuiltPrompt;
96
+ try {
97
+ built = await this.buildPrompt(body, tools, ownership, prep);
98
+ } catch (e) {
99
+ if (e instanceof RequestError) return reject(e.status, e.message);
100
+ return reject(400, `prompt build failed: ${(e as Error).message}`);
101
+ }
102
+ const { promptIds, vision, startInThinking, diffusionPixels } = built;
103
+ ownership.own(vision?.embeddings);
104
+ ownership.own(vision?.imageMask);
105
+ ownership.own(vision?.multimodalMask);
106
+ ownership.own(diffusionPixels);
107
+
108
+ let options: ReturnType<RequestPrep["toOptions"]>;
109
+ try {
110
+ options = prep.toOptions(body);
111
+ // Stable cache boundary for the prompt-boundary snapshot (chat
112
+ // prompts end in a generation primer the next turn won't contain).
113
+ // The probe costs a second full render+encode (~150 ms at 16k), so
114
+ // run it ONLY when a snapshot can actually be taken: the cache
115
+ // must not already hold the strict prefix (warm repeats peek at
116
+ // len-1 → skip; the snapshot gate needs boundary > cached prefix).
117
+ if (built.probeStableLen) {
118
+ const cachePeek = this.promptCache.peekPrefixLen(
119
+ promptIds, options.adapters?.length ? ctx.adapters.cacheNamespace(options.adapters) : "");
120
+ if (cachePeek < promptIds.length - 1)
121
+ options.snapshotAt = prep.stableLenFor(body, tools, promptIds);
122
+ }
123
+ } catch (e) {
124
+ // bad logit_bias (non-numeric keys/values) — mlx-lm's coercion error
125
+ return reject(400, (e as Error).message);
126
+ }
127
+ if (diffusionPixels) options.visionPixels = diffusionPixels;
128
+ // Attach the compiled grammar controller (null when no constraint /
129
+ // degrade — generate() runs the unmasked fast pipelined loop).
130
+ if (grammarCtrl) options.grammar = grammarCtrl;
131
+ // Token fast-forwarding (K3, MLX_BUN_FILL=strict): the determined-span
132
+ // table for this request's tools. Refused for the shapes only this stage
133
+ // can see — a compiled grammar (forced tokens are its job), media prompts
134
+ // (embeddings prefill / mRoPE), a mounted draft model (the spec loop is a
135
+ // different executor and never reads options.fill), and quantized-KV
136
+ // schemes (post-conversion multi-token append is unvalidated). The
137
+ // body-level refusals live in prep.fillPlanFor.
138
+ let fillSession: import("../fill/fill-session").FillSession | null = null;
139
+ if (!grammarCtrl && !vision && !diffusionPixels && !ctx.draft &&
140
+ !options.kvBits && !options.kvConfig?.length && !options.turboQuant) {
141
+ fillSession = prep.fillPlanFor(body, tools, promptIds);
142
+ if (fillSession) options.fill = fillSession;
143
+ }
144
+ // Unset = no request-level ceiling; admission clamps to the admitted
145
+ // context (the program is the cap).
146
+ const requestedMaxTokens = options.maxTokens ?? Infinity;
147
+ let adapterIds: string[];
148
+ try {
149
+ // A request's explicit `adapter` (incl. "none") wins over the
150
+ // startup default from `serve --adapter <dir>`.
151
+ adapterIds = ctx.adapters.resolveSpec(body.adapter ?? this.defaultAdapter);
152
+ } catch (e) {
153
+ return reject(400, (e as Error).message);
154
+ }
155
+
156
+ const wantLogprobs = body.logprobs === true;
157
+ const topLogprobs =
158
+ typeof body.top_logprobs === "number" && body.top_logprobs > 0
159
+ ? body.top_logprobs : 0;
160
+ return {
161
+ requestId: id,
162
+ stream: request.stream,
163
+ warnings,
164
+ plan: {
165
+ promptIds,
166
+ options,
167
+ requestedMaxTokens,
168
+ contextLimit: this.contextLimit,
169
+ stream: request.stream,
170
+ wantLogprobs,
171
+ topLogprobs,
172
+ adapterIds,
173
+ hasVision: !!vision,
174
+ userSeed: body.seed !== undefined,
175
+ hasGrammar: !!grammarCtrl,
176
+ hasDraft: !!ctx.draft,
177
+ ownership,
178
+ },
179
+ ...(vision ? { vision } : {}),
180
+ pipeline: {
181
+ // The router's parse-failure hook disarms this request's strict fill
182
+ // rows (and records `usage.fill.parseFallback`).
183
+ router: prep.toolRouter(tools, fillSession ? () => fillSession!.noteParseFailure() : undefined),
184
+ stopper: new StopMatcher(options.stopSequences),
185
+ thinking: new ThinkingTagSplitter(
186
+ ctx.template.thinkingFormat === "think-tag",
187
+ startInThinking,
188
+ ),
189
+ collectToolCalls: true,
190
+ },
191
+ idToToken: (tokenId) => ctx.tokenizer.idToToken(tokenId),
192
+ };
193
+ }
194
+
195
+ }
@@ -0,0 +1,64 @@
1
+ import type { GenerateOptions } from "../generate";
2
+ import type { ResolvedExecution } from "../contracts/execution";
3
+ import { createHash } from "node:crypto";
4
+
5
+ /** Sort object fields only; array order is meaningful for adapter composition,
6
+ * stop handling and quantization recipes. */
7
+ function canonical(value: unknown): unknown {
8
+ if (Array.isArray(value)) return value.map(canonical);
9
+ if (value && typeof value === "object")
10
+ return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b))
11
+ .map(([key, item]) => [key, canonical(item)]));
12
+ return value;
13
+ }
14
+
15
+ /** Stable identity for an exactly resumable generation. The prompt already
16
+ * includes rendered messages and tool schemas; the remaining fields cover
17
+ * every sampling policy that can change the continuation. */
18
+ export function generationCheckpointKey(
19
+ promptIds: number[], options: GenerateOptions & { stopSequences?: readonly string[] }, cacheNs = "",
20
+ execution?: ResolvedExecution, bindingIdentity?: unknown,
21
+ ): string {
22
+ const policy = {
23
+ version: 4,
24
+ execution: execution && { method: execution.method, mechanism: execution.mechanism,
25
+ pagedKv: execution.pagedKv, fill: execution.fill,
26
+ compiledDecode: execution.compiledDecode, grammarJump: execution.grammarJump },
27
+ bindingIdentity,
28
+ stopSequences: options.stopSequences ?? [],
29
+ cacheNs,
30
+ promptIds,
31
+ maxTokens: options.maxTokens,
32
+ eosTokenIds: options.eosTokenIds,
33
+ temperature: options.temperature,
34
+ topP: options.topP,
35
+ topK: options.topK,
36
+ minP: options.minP,
37
+ minTokensToKeep: options.minTokensToKeep,
38
+ xtcProbability: options.xtcProbability,
39
+ xtcThreshold: options.xtcThreshold,
40
+ xtcSpecialTokens: options.xtcSpecialTokens,
41
+ // A missing client seed means "fresh randomness" for a new request, but
42
+ // an identical RETRY must find the durable checkpoint and restore the
43
+ // original random stream. Keep the generated seed out of request identity;
44
+ // it travels in checkpoint metadata instead. Explicit seeds remain part of
45
+ // the policy, so changing one cannot select an incompatible checkpoint.
46
+ seedMode: options.seedWasExplicit ? "explicit" : "server-default",
47
+ seed: options.seedWasExplicit ? options.seed : undefined,
48
+ hlg: options.hlg,
49
+ curve: options.curve,
50
+ logitBias: options.logitBias,
51
+ repetitionPenalty: options.repetitionPenalty,
52
+ repetitionContextSize: options.repetitionContextSize,
53
+ presencePenalty: options.presencePenalty,
54
+ presenceContextSize: options.presenceContextSize,
55
+ frequencyPenalty: options.frequencyPenalty,
56
+ frequencyContextSize: options.frequencyContextSize,
57
+ kvBits: options.kvBits,
58
+ kvGroupSize: options.kvGroupSize,
59
+ quantizedKvStart: options.quantizedKvStart,
60
+ kvConfig: options.kvConfig,
61
+ turboQuant: options.turboQuant,
62
+ };
63
+ return createHash("sha256").update(JSON.stringify(canonical(policy))).digest("hex");
64
+ }