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,519 @@
1
+ // Token → text streaming stages shared by every completion surface:
2
+ // StreamDecoder (mlx-lm byte-parity incremental detokenizer), ToolAwareStream
3
+ // (tool-call / reasoning-channel routing), StopMatcher (decoded-text stop
4
+ // sequences), ThinkingTagSplitter (<think> markup → reasoning deltas).
5
+ // Extracted from src/server.ts (repo-taming Phase 4).
6
+ import type { ToolDefinition } from "../chat-template";
7
+ import type { LoadedTokenizer } from "../tokenizer";
8
+ import {
9
+ CHANNEL_END, CHANNEL_START, parseGeneratedToolCalls, parseToolCalls,
10
+ TOOL_CALL_END, TOOL_CALL_START,
11
+ } from "../tool-call";
12
+
13
+ export interface OpenAIToolCall {
14
+ id: string;
15
+ type: "function";
16
+ function: { name: string; arguments: string };
17
+ }
18
+
19
+ export type ToolStreamMode = "gemma-sentinel" | "plain" | "buffered-text";
20
+
21
+ /** Pick the stream router for a model family. The token-id sentinel router is
22
+ * Gemma-4-ONLY: ids 48/49 (<|tool_call>/<tool_call|>) and 100/101
23
+ * (<|channel>/<channel|>) are special tokens of that tokenizer family
24
+ * (src/tool-call.ts). On every other tokenizer — MiniCPM5, Qwen3/3.5, and
25
+ * the Tier-0 generics (llama, qwen2, phi3, …) — those ids are ordinary
26
+ * low-id vocab, so the sentinel router would silently swallow output into a
27
+ * phantom tool/reasoning segment. Everyone else parses tool calls from
28
+ * decoded text (buffered-text; parseGeneratedToolCalls covers the
29
+ * OpenAI-JSON <tool_call>, Qwen <function=…>, and MiniCPM5 <function name=…>
30
+ * shapes) when tools are present, and streams plain otherwise. Models whose
31
+ * markup isn't covered fail soft: the markup stays in content. */
32
+ export function selectToolStreamMode(modelType: string, hasTools: boolean): ToolStreamMode {
33
+ if (modelType.startsWith("gemma4")) return "gemma-sentinel";
34
+ return hasTools ? "buffered-text" : "plain";
35
+ }
36
+
37
+ /** Routes generated tokens. Gemma uses family-specific sentinel token ids;
38
+ * MiniCPM5 and other text-template models use decoded-text parsing so
39
+ * ordinary tokenizer ids like "<" are never swallowed globally. Exported for
40
+ * unit tests (gemma-channel reasoning split). */
41
+ export class ToolAwareStream {
42
+ readonly #decoder: StreamDecoder;
43
+ #inTool = false;
44
+ #toolTokens: number[] = [];
45
+ #text = "";
46
+ /** Chars of #text already returned as content. */
47
+ #sent = 0;
48
+ /** Index where tool markup starts; content emission stops there. */
49
+ #frozen = -1;
50
+ #textToolCalls: OpenAIToolCall[] | null = null;
51
+ #textToolParseFailed = false;
52
+ readonly toolSegments: number[][] = [];
53
+
54
+ /** Gemma reasoning-channel state (gemma-sentinel mode). The model wraps
55
+ * chain-of-thought as `<|channel>thought\n…<channel|>` using special tokens
56
+ * 100/101 that the content decoder strips, so reasoning is captured here at
57
+ * the token level. A SEPARATE decoder keeps the reasoning byte-stream's
58
+ * incremental state independent of content's. The `thought` channel-name
59
+ * word is stripped before the reasoning text (mlx-lm's think-start marker
60
+ * is `<|channel>thought`; the "\n" after it is reasoning content). */
61
+ readonly #channelDecoder: StreamDecoder;
62
+ #inChannel = false;
63
+ #channelNamePending = "";
64
+ #channelNameDone = false;
65
+ #reasoning = "";
66
+
67
+ /** Decoded-text markers that open tool markup (oracle: the streaming
68
+ * parser buffers from `<tool_call`/`<function` on, never the whole
69
+ * response — content before a tool call still streams live). */
70
+ static readonly TOOL_MARKERS = ["<tool_call>", "<function"];
71
+
72
+ constructor(
73
+ readonly tokenizer: LoadedTokenizer,
74
+ readonly mode: ToolStreamMode,
75
+ readonly tools: ToolDefinition[] | null,
76
+ /** Fired when parseGeneratedToolCalls rejects the emitted markup. The
77
+ * fill table (src/fill/) subscribes: a request whose tool call the
78
+ * parser refuses is a request whose template rendering disagreed with
79
+ * what the model emits, so its strict rows are disarmed. */
80
+ private readonly onParseFailure?: () => void,
81
+ ) {
82
+ this.#decoder = new StreamDecoder(tokenizer, mode !== "buffered-text");
83
+ this.#channelDecoder = new StreamDecoder(tokenizer, true);
84
+ }
85
+
86
+ /** Feed decoded channel text, stripping the leading `thought` channel-name
87
+ * word (which may arrive across tokens). Returns the reasoning delta.
88
+ * mlx-lm parity: its think-start marker is exactly `<|channel>thought`
89
+ * (tokenizer_utils.py `_infer_thinking`) and only the MARKER tokens get
90
+ * their text blanked (server.py `_process_control_tokens`) — the "\n"
91
+ * after the name is an ordinary generated token, so it is the FIRST byte
92
+ * of the reasoning stream and must be kept, not swallowed with the name. */
93
+ #feedChannel(text: string): string {
94
+ if (this.#channelNameDone) return text;
95
+ this.#channelNamePending += text;
96
+ const nl = this.#channelNamePending.indexOf("\n");
97
+ if (nl === -1) return ""; // still inside the channel-name word
98
+ this.#channelNameDone = true;
99
+ const rest = this.#channelNamePending.slice(nl); // keep the "\n" (mlx-lm does)
100
+ this.#channelNamePending = "";
101
+ return rest;
102
+ }
103
+
104
+ /** Drain reasoning captured since the last call (gemma-channel thinking). */
105
+ takeReasoning(): string {
106
+ const r = this.#reasoning;
107
+ this.#reasoning = "";
108
+ return r;
109
+ }
110
+
111
+ /** Emit the longest #text prefix that cannot be (the start of) tool
112
+ * markup; hold back ambiguous tails until disambiguated. */
113
+ #textDelta(): string {
114
+ if (this.#frozen >= 0) return "";
115
+ let markerAt = -1;
116
+ for (const mk of ToolAwareStream.TOOL_MARKERS) {
117
+ const i = this.#text.indexOf(mk, this.#sent);
118
+ if (i !== -1 && (markerAt === -1 || i < markerAt)) markerAt = i;
119
+ }
120
+ if (markerAt !== -1) {
121
+ this.#frozen = markerAt;
122
+ const out = this.#text.slice(this.#sent, markerAt);
123
+ this.#sent = markerAt;
124
+ return out;
125
+ }
126
+ let hold = 0;
127
+ for (const mk of ToolAwareStream.TOOL_MARKERS) {
128
+ const max = Math.min(mk.length - 1, this.#text.length - this.#sent);
129
+ for (let k = max; k > hold; k--) {
130
+ if (this.#text.endsWith(mk.slice(0, k))) { hold = k; break; }
131
+ }
132
+ }
133
+ const limit = this.#text.length - hold;
134
+ if (limit <= this.#sent) return "";
135
+ const out = this.#text.slice(this.#sent, limit);
136
+ this.#sent = limit;
137
+ return out;
138
+ }
139
+
140
+ /** Returns the content text delta for this token ("" while capturing). */
141
+ push(token: number): string {
142
+ if (this.mode !== "gemma-sentinel") {
143
+ this.#text += this.#decoder.push(token);
144
+ if (this.mode === "plain") {
145
+ const out = this.#text.slice(this.#sent);
146
+ this.#sent = this.#text.length;
147
+ return out;
148
+ }
149
+ return this.#textDelta();
150
+ }
151
+ if (this.#inTool) {
152
+ if (token === TOOL_CALL_END) {
153
+ this.#inTool = false;
154
+ this.toolSegments.push(this.#toolTokens);
155
+ this.#toolTokens = [];
156
+ } else {
157
+ this.#toolTokens.push(token);
158
+ }
159
+ return "";
160
+ }
161
+ // Reasoning channel: tokens between <|channel> and <channel|> are thought,
162
+ // captured to #reasoning (drained via takeReasoning), never content. An
163
+ // empty block (<|channel>thought\n<channel|>, emitted by larger Gemmas even
164
+ // with thinking off) yields only the "\n" as reasoning (mlx-lm parity) and
165
+ // leaks nothing into content.
166
+ if (this.#inChannel) {
167
+ if (token === CHANNEL_END) {
168
+ this.#inChannel = false;
169
+ this.#reasoning += this.#feedChannel(this.#channelDecoder.flush());
170
+ } else {
171
+ this.#reasoning += this.#feedChannel(this.#channelDecoder.push(token));
172
+ }
173
+ return "";
174
+ }
175
+ if (token === CHANNEL_START) {
176
+ this.#inChannel = true;
177
+ this.#channelNameDone = false;
178
+ this.#channelNamePending = "";
179
+ return "";
180
+ }
181
+ if (token === TOOL_CALL_START) {
182
+ this.#inTool = true;
183
+ return "";
184
+ }
185
+ return this.#decoder.push(token);
186
+ }
187
+
188
+ flush(): string {
189
+ if (this.mode !== "gemma-sentinel") {
190
+ this.#text += this.#decoder.flush();
191
+ if (this.mode === "buffered-text") {
192
+ const calls = this.toolCalls();
193
+ if (calls.length && !this.#textToolParseFailed && this.#frozen >= 0) {
194
+ // markup parsed into tool_calls — emit any prose still held
195
+ // before it; the markup itself never reaches content
196
+ const out = this.#text.slice(this.#sent, this.#frozen);
197
+ this.#sent = this.#text.length;
198
+ return out;
199
+ }
200
+ // no tool call (or parse fallback): release everything withheld
201
+ const out = this.#text.slice(this.#sent);
202
+ this.#sent = this.#text.length;
203
+ return out;
204
+ }
205
+ const out = this.#text.slice(this.#sent);
206
+ this.#sent = this.#text.length;
207
+ return out;
208
+ }
209
+ if (this.#inChannel) {
210
+ // truncated mid-reasoning (hit max_tokens); surface the partial thought
211
+ this.#reasoning += this.#feedChannel(this.#channelDecoder.flush());
212
+ this.#inChannel = false;
213
+ return "";
214
+ }
215
+ if (this.#inTool && this.#toolTokens.length) {
216
+ // truncated mid-tool-call (hit max_tokens); surface what we have
217
+ this.toolSegments.push(this.#toolTokens);
218
+ this.#toolTokens = [];
219
+ }
220
+ return this.#decoder.flush();
221
+ }
222
+
223
+ toolCalls(): OpenAIToolCall[] {
224
+ if (this.mode !== "gemma-sentinel") {
225
+ if (this.#textToolCalls) return this.#textToolCalls;
226
+ try {
227
+ this.#textToolCalls = parseGeneratedToolCalls(this.#text, this.tools ?? []).map((c) => ({
228
+ id: `call_${crypto.randomUUID().slice(0, 8)}`,
229
+ type: "function",
230
+ function: { name: c.name, arguments: JSON.stringify(c.arguments) },
231
+ }));
232
+ } catch {
233
+ this.#textToolParseFailed = true;
234
+ this.#textToolCalls = [];
235
+ this.onParseFailure?.();
236
+ }
237
+ return this.#textToolCalls;
238
+ }
239
+ const out: OpenAIToolCall[] = [];
240
+ for (const seg of this.toolSegments) {
241
+ const text = this.tokenizer.decode(seg, false); // keep <|"|> markers
242
+ for (const c of parseToolCalls(text)) {
243
+ out.push({
244
+ id: `call_${crypto.randomUUID().slice(0, 8)}`,
245
+ type: "function",
246
+ function: { name: c.name, arguments: JSON.stringify(c.arguments) },
247
+ });
248
+ }
249
+ }
250
+ return out;
251
+ }
252
+ }
253
+
254
+ /** Decoded-text stop-sequence matcher with streaming hold-back. Matching
255
+ * on text (not token ids) catches sequences that span token boundaries
256
+ * or tokenize differently in context; current mlx-lm uses token-id
257
+ * state machines and misses those. Text that could be the start of a
258
+ * stop sequence is withheld until disambiguated, so SSE clients never
259
+ * see any part of the stop sequence itself. */
260
+ export class StopMatcher {
261
+ #pending = "";
262
+ stopped = false;
263
+
264
+ constructor(readonly sequences: string[]) {}
265
+
266
+ /** Feed a text delta; returns the prefix that is now safe to emit.
267
+ * After a match fires (`stopped`), text before the match is returned
268
+ * and everything from the match on is discarded. */
269
+ push(text: string): string {
270
+ if (this.stopped) return "";
271
+ if (this.sequences.length === 0) return text;
272
+ this.#pending += text;
273
+ // earliest full match wins
274
+ let cut = -1;
275
+ for (const seq of this.sequences) {
276
+ const i = this.#pending.indexOf(seq);
277
+ if (i !== -1 && (cut === -1 || i < cut)) cut = i;
278
+ }
279
+ if (cut !== -1) {
280
+ this.stopped = true;
281
+ const out = this.#pending.slice(0, cut);
282
+ this.#pending = "";
283
+ return out;
284
+ }
285
+ // hold back the longest tail that is a proper prefix of any sequence
286
+ let hold = 0;
287
+ for (const seq of this.sequences) {
288
+ const max = Math.min(seq.length - 1, this.#pending.length);
289
+ for (let k = max; k > hold; k--) {
290
+ if (this.#pending.endsWith(seq.slice(0, k))) {
291
+ hold = k;
292
+ break;
293
+ }
294
+ }
295
+ }
296
+ if (hold === 0) {
297
+ const out = this.#pending;
298
+ this.#pending = "";
299
+ return out;
300
+ }
301
+ const out = this.#pending.slice(0, -hold);
302
+ this.#pending = this.#pending.slice(-hold);
303
+ return out;
304
+ }
305
+
306
+ /** Generation ended without a match — release any held-back text. */
307
+ flush(): string {
308
+ const out = this.#pending;
309
+ this.#pending = "";
310
+ return out;
311
+ }
312
+ }
313
+
314
+ /** Split Qwen-style inline <think>...</think> markup into OpenAI reasoning
315
+ * deltas/content. This keeps raw tags out of normal chat text while giving
316
+ * pi (TUI + web) proper thinking_delta events. It is streaming-safe: partial
317
+ * tag prefixes are held until disambiguated. */
318
+ export class ThinkingTagSplitter {
319
+ #pending = "";
320
+ #inThinking: boolean;
321
+ reasoning = "";
322
+ content = "";
323
+
324
+ /** `startInThinking` seeds the parser INSIDE a <think> block. Needed for
325
+ * templates (Qwen3.5, MiniCPM5) that prime an OPEN `<think>` in the
326
+ * generation prompt when thinking is enabled: the model's output then
327
+ * starts mid-reasoning and emits only the closing `</think>`, never an
328
+ * opening tag. Without this seed the whole chain-of-thought leaks into
329
+ * `content` and the `reasoning` field stays empty. */
330
+ constructor(private readonly enabled: boolean, startInThinking = false) {
331
+ this.#inThinking = startInThinking;
332
+ }
333
+
334
+ #safePrefixUntilTag(tag: string): string {
335
+ const i = this.#pending.indexOf(tag);
336
+ if (i !== -1) return this.#pending.slice(0, i);
337
+ let hold = 0;
338
+ for (let k = Math.min(tag.length - 1, this.#pending.length); k > 0; k--) {
339
+ if (this.#pending.endsWith(tag.slice(0, k))) { hold = k; break; }
340
+ }
341
+ return this.#pending.slice(0, this.#pending.length - hold);
342
+ }
343
+
344
+ push(text: string): { content: string; reasoning: string } {
345
+ if (!this.enabled) {
346
+ this.content += text;
347
+ return { content: text, reasoning: "" };
348
+ }
349
+ this.#pending += text;
350
+ let content = "";
351
+ let reasoning = "";
352
+ while (this.#pending) {
353
+ const tag = this.#inThinking ? "</think>" : "<think>";
354
+ const i = this.#pending.indexOf(tag);
355
+ const emit = i === -1 ? this.#safePrefixUntilTag(tag) : this.#pending.slice(0, i);
356
+ if (!emit && i === -1) break;
357
+ if (emit) {
358
+ if (this.#inThinking) reasoning += emit;
359
+ else content += emit;
360
+ this.#pending = this.#pending.slice(emit.length);
361
+ }
362
+ if (i !== -1 && this.#pending.startsWith(tag)) {
363
+ this.#pending = this.#pending.slice(tag.length);
364
+ this.#inThinking = !this.#inThinking;
365
+ continue;
366
+ }
367
+ if (i === -1) break;
368
+ }
369
+ this.content += content;
370
+ this.reasoning += reasoning;
371
+ return { content, reasoning };
372
+ }
373
+
374
+ flush(): { content: string; reasoning: string } {
375
+ if (!this.enabled) return { content: "", reasoning: "" };
376
+ const out = this.#inThinking
377
+ ? { content: "", reasoning: this.#pending }
378
+ : { content: this.#pending, reasoning: "" };
379
+ this.#pending = "";
380
+ this.content += out.content;
381
+ this.reasoning += out.reasoning;
382
+ return out;
383
+ }
384
+ }
385
+
386
+ /** Incremental detokenizer: emits the longest stable decoded prefix.
387
+ *
388
+ * Byte parity with mlx-lm's streaming detokenizers (the drop-in contract is
389
+ * rendered BYTES, not just token ids). For BPE/ByteLevel tokenizers, two
390
+ * mlx-lm 0.31.3 BPEStreamingDetokenizer behaviors our full-sequence decode
391
+ * lacks (tokenizer_utils.py:195-226):
392
+ *
393
+ * 1. `trimsLeadingSpace` — mlx-lm drops ONE leading " " at the start of the
394
+ * generated sequence (`_maybe_trim_space`); trim it here. SPM decode
395
+ * already matches (see LoadedTokenizer.trimsLeadingSpace).
396
+ * 2. `bareSpaceTokenId` — add_token WITHHOLDS a single-char byte-32 token
397
+ * ("Ġ") in `_unflushed` ("For single spaces wait until the next token"),
398
+ * flushing it together with the NEXT token — and mlx_lm.server NEVER
399
+ * calls detokenizer.finalize() (zero hits in server.py 0.31.3), so a
400
+ * generation ENDING on bare-space token(s) silently drops their spaces
401
+ * from the served bytes. push() withholds those spaces; flush() drops a
402
+ * trailing bare-space run (the held text dies with the request, exactly
403
+ * like mlx-lm serve). Re-check both if upstream ever adds a finalize()
404
+ * call. LATENT HAZARD (deliberately not emulated): models with
405
+ * clean_up_tokenization_spaces=true get an ADDITIONAL mid-stream rule
406
+ * (`_space_matches`: held space dropped before "." "," "'s" …) — both
407
+ * current BPE targets have it false (MiniCPM5), so it never fires here.
408
+ *
409
+ * Exported for unit tests (serve-detok mlx-lm byte parity). */
410
+ export class StreamDecoder {
411
+ static readonly #WINDOW_TOKENS = 32;
412
+ #ids: number[] = [];
413
+ #emitted = "";
414
+ #warnedRevision = false;
415
+ /** Token index where the bounded decode suffix begins. */
416
+ #windowStart = 0;
417
+ /** Exact decoded text before #windowStart, established by suffix matching. */
418
+ #windowPrefix = "";
419
+ readonly #trimLeadingSpace: boolean;
420
+ readonly #bareSpaceId: number | undefined;
421
+
422
+ constructor(
423
+ readonly tokenizer: LoadedTokenizer,
424
+ readonly skipSpecialTokens = true,
425
+ ) {
426
+ this.#trimLeadingSpace = tokenizer.trimsLeadingSpace === true;
427
+ this.#bareSpaceId = tokenizer.bareSpaceTokenId;
428
+ }
429
+
430
+ #decode(ids: number[]): string {
431
+ const full = this.#decodeRaw(ids);
432
+ return this.#trimLeadingSpace && full.startsWith(" ") ? full.slice(1) : full;
433
+ }
434
+
435
+ #decodeRaw(ids: number[]): string {
436
+ return this.tokenizer.decode(ids, this.skipSpecialTokens);
437
+ }
438
+
439
+ /** Decode only a bounded suffix once an exact text anchor can be proven.
440
+ *
441
+ * At each rebase, decode a candidate suffix independently and accept it only
442
+ * when it is literally the suffix of the exact text decoded so far. This
443
+ * preserves ByteLevel/SPM boundary behavior while reducing steady-state
444
+ * decode work from the whole generation to 32–64 tokens. If no suffix can be
445
+ * anchored (an unusual/global decoder), leave #windowStart at zero and keep
446
+ * the full-history correctness path. */
447
+ #decodeIncremental(): string {
448
+ const full = this.#windowStart === 0
449
+ ? this.#decode(this.#ids)
450
+ : this.#windowPrefix + this.#decodeRaw(this.#ids.slice(this.#windowStart));
451
+
452
+ if (this.#ids.length - this.#windowStart < StreamDecoder.#WINDOW_TOKENS * 2)
453
+ return full;
454
+
455
+ const preferred = this.#ids.length - StreamDecoder.#WINDOW_TOKENS;
456
+ const oldest = Math.max(this.#windowStart, preferred - StreamDecoder.#WINDOW_TOKENS);
457
+ for (let start = preferred; start >= oldest; start--) {
458
+ const tail = this.#decodeRaw(this.#ids.slice(start));
459
+ if ((tail.length > 0 || full.length === 0) && full.endsWith(tail)) {
460
+ this.#windowStart = start;
461
+ this.#windowPrefix = full.slice(0, full.length - tail.length);
462
+ break;
463
+ }
464
+ }
465
+ return full;
466
+ }
467
+
468
+ push(token: number): string {
469
+ this.#ids.push(token);
470
+ // Bare-space hold-back (mlx-lm add_token keeps "Ġ" in _unflushed): don't
471
+ // advance #emitted; the held space(s) flush as part of the next
472
+ // non-bare-space token's delta — consecutive bare spaces accumulate.
473
+ if (token === this.#bareSpaceId) return "";
474
+ const full = this.#decodeIncremental();
475
+ // hold back a trailing replacement char (partial multi-byte sequence)
476
+ const stable = full.endsWith("�") ? full.slice(0, -1) : full;
477
+ if (!stable.startsWith(this.#emitted)) {
478
+ // The decoder revised already-streamed text (cleanup rules — never
479
+ // fires for the shipped tokenizers, which all have
480
+ // clean_up_tokenization_spaces=false; see the LATENT HAZARD note in
481
+ // the class doc). An SSE client cannot un-receive bytes, so the old
482
+ // "re-emit from scratch" answer DUPLICATED the whole stream
483
+ // (2026-07-07 review). Truncate-safe resync instead: emit only the
484
+ // length-extension and keep the emitted watermark monotone — byte
485
+ // drift is confined to the revised span, and the once-per-stream
486
+ // warning makes the tokenizer that needs real _space_matches
487
+ // emulation loud instead of silently wrong.
488
+ if (!this.#warnedRevision) {
489
+ this.#warnedRevision = true;
490
+ console.warn(
491
+ "[detok] decoder revised already-streamed text (clean_up_tokenization_spaces?) — " +
492
+ "resyncing without re-emit; add mlx-lm's cleanup-rule emulation for this tokenizer",
493
+ );
494
+ }
495
+ if (stable.length <= this.#emitted.length) return "";
496
+ const out = stable.slice(this.#emitted.length);
497
+ this.#emitted = stable;
498
+ return out;
499
+ }
500
+ const delta = stable.slice(this.#emitted.length);
501
+ this.#emitted = stable;
502
+ return delta;
503
+ }
504
+
505
+ flush(): string {
506
+ // mlx_lm.server never finalize()s: a trailing bare-space run stays
507
+ // withheld forever, so its text is dropped from the served bytes.
508
+ let ids = this.#ids;
509
+ if (this.#bareSpaceId !== undefined) {
510
+ let n = ids.length;
511
+ while (n > 0 && ids[n - 1] === this.#bareSpaceId) n--;
512
+ if (n < ids.length) ids = ids.slice(0, n);
513
+ }
514
+ const full = this.#decode(ids);
515
+ const delta = full.slice(this.#emitted.length);
516
+ this.#emitted = full;
517
+ return delta;
518
+ }
519
+ }