mlx-bun 0.0.13 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (427) hide show
  1. package/README.md +71 -578
  2. package/bin/mlx-bun.mjs +1 -1
  3. package/docs/README.md +91 -0
  4. package/docs/{design → archive}/hlg-sampling.md +3 -3
  5. package/docs/{design → archive/investigations}/audio-input-plan.md +4 -4
  6. package/docs/{design → archive/investigations}/colibri-glm52-port.md +1 -1
  7. package/docs/{design → archive/investigations}/diffusion-gemma-port.md +3 -3
  8. package/docs/{design → archive/investigations}/faithful-l1-consolidation.md +4 -4
  9. package/docs/{investigations → archive/investigations}/hlg-sampling-investigation.md +1 -1
  10. package/docs/{design → archive/investigations}/minicpm5-decode-megakernel.md +11 -11
  11. package/docs/{design → archive/investigations}/optimization_plan.md +1 -1
  12. package/docs/archive/investigations/pareto-specialized-runtime-findings.md +92 -0
  13. package/docs/{design → archive/investigations}/pre-colibri-stabilization.md +5 -5
  14. package/docs/{design → archive/investigations}/the-dreaming-handoff.md +2 -2
  15. package/docs/{planning → archive/planning}/chat-ui-vision.md +1 -1
  16. package/docs/{planning → archive/planning}/chunk-task-roadmap.md +1 -1
  17. package/docs/{planning → archive/planning}/curve-sampler-research-plan.md +4 -4
  18. package/docs/{planning → archive/planning}/memory-docs-and-dag-plan.md +1 -1
  19. package/docs/{planning → archive/planning}/release-notes-v0.0.9.md +1 -1
  20. package/docs/archive/planning/release-notes-v0.2.0.md +49 -0
  21. package/docs/{planning → archive/planning}/website-readme-pass-plan.md +1 -1
  22. package/docs/design/batching.md +569 -0
  23. package/docs/design/decode-speed-program.md +3178 -169
  24. package/docs/design/dreaming-nightly-pipeline.md +675 -282
  25. package/docs/design/generic-model-support.md +579 -334
  26. package/docs/design/kv-cache.md +680 -0
  27. package/docs/design/orpo-training.md +673 -1239
  28. package/docs/design/speculative-decoding.md +1627 -0
  29. package/docs/design/turboquant.md +2102 -0
  30. package/docs/design/unified-engine-frontier-plan.md +1831 -727
  31. package/docs/design/web-chat-redesign.md +666 -1083
  32. package/docs/planning/PRODUCT_ROADMAP.md +34 -1
  33. package/docs/planning/release-notes-v0.3.0.md +35 -0
  34. package/docs/planning/video-series.md +421 -0
  35. package/docs/reference/benchmarks.md +1162 -0
  36. package/docs/reference/cli.md +395 -149
  37. package/docs/reference/distribution.md +321 -181
  38. package/docs/reference/environment.md +348 -0
  39. package/docs/reference/glossary.md +39 -0
  40. package/docs/reference/library-api.md +580 -142
  41. package/docs/reference/memory.md +227 -64
  42. package/docs/reference/models.md +137 -6
  43. package/docs/reference/server-api.md +938 -422
  44. package/docs/reference/server-config.md +652 -320
  45. package/docs/reference/training.md +393 -254
  46. package/docs/reference/troubleshooting.md +166 -0
  47. package/package.json +10 -5
  48. package/src/admission-plan.ts +18 -0
  49. package/src/anthropic.ts +92 -3
  50. package/src/audio/decode.ts +2 -2
  51. package/src/audio/features.ts +2 -2
  52. package/src/backends/mlx/autoregressive.ts +108 -0
  53. package/src/backends/mlx/batch-group.ts +1646 -0
  54. package/src/backends/mlx/diffusion.ts +39 -0
  55. package/src/backends/mlx/draft-target.ts +47 -0
  56. package/src/backends/mlx/evaluation.ts +105 -0
  57. package/src/backends/mlx/fill-append.ts +45 -0
  58. package/src/backends/mlx/gateway-binding.ts +71 -0
  59. package/src/backends/mlx/graph.ts +38 -0
  60. package/src/backends/mlx/memory-client.ts +189 -0
  61. package/src/backends/mlx/memory-guard.ts +40 -0
  62. package/src/backends/mlx/methods.ts +90 -0
  63. package/src/backends/mlx/model-prompt.ts +164 -0
  64. package/src/backends/mlx/model-serving.ts +79 -0
  65. package/src/backends/mlx/prefill.ts +33 -0
  66. package/src/backends/mlx/request-policy.ts +20 -0
  67. package/src/backends/mlx/rollback.ts +51 -0
  68. package/src/backends/mlx/serial-executor.ts +273 -0
  69. package/src/backends/mlx/speculative.ts +114 -0
  70. package/src/backends/mlx/state-views.ts +26 -0
  71. package/src/backends/mlx/text-engine.ts +73 -0
  72. package/src/cli.ts +189 -61
  73. package/src/client.ts +66 -0
  74. package/src/config.ts +21 -4
  75. package/src/contracts/completion.ts +16 -0
  76. package/src/contracts/execution.ts +49 -0
  77. package/src/contracts/generation.ts +103 -0
  78. package/src/contracts/host.ts +7 -0
  79. package/src/contracts/jobs.ts +66 -0
  80. package/src/contracts/pi-provider.ts +30 -0
  81. package/src/contracts/pi.ts +259 -0
  82. package/src/contracts/resources.ts +18 -0
  83. package/src/contracts/scheduling.ts +27 -0
  84. package/src/contracts/tsconfig.json +14 -0
  85. package/src/diffusion/diffusion-generate.ts +218 -153
  86. package/src/embed.ts +1 -1
  87. package/src/engine/admission.ts +59 -0
  88. package/src/engine/cancellation.ts +38 -0
  89. package/src/engine/engine.ts +56 -0
  90. package/src/engine/execution-coordinator.ts +70 -0
  91. package/src/engine/execution-plan.ts +51 -0
  92. package/src/engine/resources.ts +42 -0
  93. package/src/engine/scheduler.ts +67 -0
  94. package/src/engine/session.ts +247 -0
  95. package/src/eval/README.md +44 -1
  96. package/src/eval/kl.ts +263 -23
  97. package/src/eval/reference-logits.ts +207 -0
  98. package/src/eval/runner.ts +10 -79
  99. package/src/eval/tasks/bfcl.ts +2 -1
  100. package/src/eval/tasks/gsm8k.ts +2 -1
  101. package/src/eval/tasks/hashhop.ts +2 -1
  102. package/src/eval/tasks/humaneval.ts +5 -4
  103. package/src/eval/tasks/ifeval.ts +2 -1
  104. package/src/eval/tasks/mmlu.ts +2 -1
  105. package/src/expert-io.ts +3 -1
  106. package/src/expert-offload.ts +3 -1
  107. package/src/fill/echo-index.ts +188 -0
  108. package/src/fill/fill-session.ts +452 -0
  109. package/src/fill/proposal.ts +68 -0
  110. package/src/fill/schema-rows.ts +506 -0
  111. package/src/fill/tool-boundary.ts +103 -0
  112. package/src/fit.ts +24 -43
  113. package/src/generate.ts +740 -337
  114. package/src/grammar.ts +14 -8
  115. package/src/harness-pi.ts +15 -34
  116. package/src/hub-rest.ts +4 -4
  117. package/src/index.ts +27 -0
  118. package/src/inference/denoising.ts +17 -0
  119. package/src/inference/graph.ts +33 -0
  120. package/src/inference/prefill.ts +39 -0
  121. package/src/inference/rollback.ts +7 -0
  122. package/src/jobs/db.ts +4 -0
  123. package/src/jobs/index.ts +2 -0
  124. package/src/jobs/job-entry.ts +15 -14
  125. package/src/jobs/runner.ts +98 -13
  126. package/src/jobs/task-client.ts +19 -0
  127. package/src/jobs/types.ts +2 -58
  128. package/src/kv-scheme.ts +199 -0
  129. package/src/kv-store.ts +459 -234
  130. package/src/lab/README.md +18 -0
  131. package/src/{assets → lab/curve}/curve-designer.html +2 -2
  132. package/src/{curve-sampler.ts → lab/curve/curve-sampler.ts} +2 -2
  133. package/src/{expert-trace.ts → lab/expert-trace/expert-trace.ts} +7 -5
  134. package/src/{model → lab/paged-kv}/paged-kv.ts +9 -6
  135. package/src/library.ts +40 -0
  136. package/src/lora.ts +18 -0
  137. package/src/media-fetch.ts +20 -4
  138. package/src/memory/article.ts +1 -1
  139. package/src/memory/chunk.ts +1 -1
  140. package/src/memory/model.ts +32 -170
  141. package/src/memory/prompts.ts +1 -1
  142. package/src/memory/rest.ts +1 -1
  143. package/src/memory/schedule.ts +1 -1
  144. package/src/memory/stages.ts +4 -1
  145. package/src/memory/vault.ts +3 -4
  146. package/src/mlx/array.ts +9 -10
  147. package/src/mlx/compile.ts +12 -2
  148. package/src/mlx/ffi.ts +66 -22
  149. package/src/mlx/materialize.ts +23 -0
  150. package/src/mlx/metal-kernel.ts +10 -11
  151. package/src/mlx/ops.ts +64 -19
  152. package/src/mlx/quantized-rows.ts +36 -0
  153. package/src/mlx/token-bitmask.ts +52 -0
  154. package/src/mlx/turboquant-kv-decode.ts +82 -0
  155. package/src/mlx/turboquant-kv-inverse.ts +122 -0
  156. package/src/mlx/turboquant-ops.ts +33 -3
  157. package/src/mlx/turboquant-tables.ts +2 -2
  158. package/src/model/batched-mask.ts +9 -3
  159. package/src/model/batched-quant.ts +2 -2
  160. package/src/model/batched-rotating-quant.ts +45 -99
  161. package/src/model/batched-rotating-state.ts +73 -0
  162. package/src/model/batched-rotating.ts +57 -106
  163. package/src/model/batched-row-storage.ts +127 -0
  164. package/src/model/compiled-decode.ts +4 -3
  165. package/src/model/diffusion-gemma.ts +8 -8
  166. package/src/model/factory.ts +87 -49
  167. package/src/model/fingerprint.ts +1 -1
  168. package/src/model/flash-attention.ts +3 -3
  169. package/src/model/gemma4-base.ts +187 -17
  170. package/src/model/gemma4.ts +6 -6
  171. package/src/model/generated/gemma4-12b.ts +52 -148
  172. package/src/model/generated/gemma4-26b.ts +34 -94
  173. package/src/model/generated/gemma4-e4b.ts +28 -76
  174. package/src/model/glm52-cache.ts +19 -9
  175. package/src/model/glm52-dsa.ts +13 -9
  176. package/src/model/glm52-memory.ts +28 -1
  177. package/src/model/implementation.ts +69 -0
  178. package/src/model/minicpm5.ts +7 -8
  179. package/src/model/profile.ts +443 -0
  180. package/src/model/qwen-append.ts +12 -0
  181. package/src/model/qwen-conv.ts +52 -0
  182. package/src/model/qwen3-delta.ts +98 -2
  183. package/src/model/qwen3-mrope.ts +252 -0
  184. package/src/model/qwen3_5-checkpoint.ts +164 -0
  185. package/src/model/qwen3_5.ts +336 -60
  186. package/src/model/support.ts +1 -1
  187. package/src/model/trellis-balanced-scatter.ts +51 -0
  188. package/src/model/trellis-linear.ts +672 -0
  189. package/src/model/trellis-shared-m.ts +151 -0
  190. package/src/model/trellis-shared-scatter.ts +144 -0
  191. package/src/model/trellis-splitk-prefill.ts +158 -0
  192. package/src/model/trellis-tiled-prefill.ts +136 -0
  193. package/src/model/trellis-vector-expand.ts +62 -0
  194. package/src/model/trellis-wide-prefill.ts +94 -0
  195. package/src/model/universal/dense.ts +1 -1
  196. package/src/model/universal/modules.ts +8 -1
  197. package/src/native/frame_extract.swift +140 -0
  198. package/src/native-pack.ts +15 -7
  199. package/src/pi-provider.ts +6 -64
  200. package/src/pi-web.ts +14 -320
  201. package/src/prompt-cache.ts +97 -54
  202. package/src/quantize/allocator.ts +1 -1
  203. package/src/quantize/atomic-output.ts +38 -0
  204. package/src/quantize/calibration.ts +2 -1
  205. package/src/quantize/config-writer.ts +16 -3
  206. package/src/quantize/index.ts +28 -1
  207. package/src/quantize/job.ts +11 -0
  208. package/src/quantize/quantizer.ts +265 -100
  209. package/src/quantize/rotate.ts +603 -0
  210. package/src/quantize/safetensors-writer.ts +79 -1
  211. package/src/quantize/sensitivity.ts +41 -59
  212. package/src/quantize/trellis.ts +586 -0
  213. package/src/quantize/weight-transform.ts +292 -0
  214. package/src/responses.ts +130 -3
  215. package/src/runtime-config.ts +81 -0
  216. package/src/sampler.ts +276 -4
  217. package/src/serve/admin-routes.ts +184 -0
  218. package/src/serve/aux-routes.ts +132 -0
  219. package/src/serve/batch-scheduler.ts +8 -1558
  220. package/src/serve/chat-request.ts +391 -0
  221. package/src/serve/chat-stage.ts +195 -0
  222. package/src/serve/checkpoint-identity.ts +64 -0
  223. package/src/serve/completion-executor.ts +354 -0
  224. package/src/serve/completion-sink.ts +157 -0
  225. package/src/serve/discovery-routes.ts +204 -0
  226. package/src/serve/generation-gateway.ts +211 -243
  227. package/src/serve/http.ts +159 -0
  228. package/src/serve/inference-request.ts +79 -0
  229. package/src/serve/isolate.ts +297 -105
  230. package/src/serve/kv-budget.ts +14 -0
  231. package/src/serve/lab-routes.ts +341 -0
  232. package/src/serve/lane-registry.ts +2 -1
  233. package/src/serve/model-admin-routes.ts +155 -0
  234. package/src/serve/model-binding.ts +43 -0
  235. package/src/serve/model-host.ts +512 -0
  236. package/src/serve/openai-wire.ts +103 -0
  237. package/src/serve/parent-application.ts +56 -0
  238. package/src/serve/pipeline.ts +33 -0
  239. package/src/serve/preparation.ts +33 -0
  240. package/src/serve/prompt-response-trace.ts +147 -0
  241. package/src/serve/request-plan.ts +162 -0
  242. package/src/serve/request-prep.ts +346 -0
  243. package/src/serve/response-lease.ts +24 -0
  244. package/src/serve/responses-client.ts +69 -0
  245. package/src/serve/session-completion-engine.ts +110 -0
  246. package/src/serve/session-search.ts +1 -1
  247. package/src/serve/static-routes.ts +101 -0
  248. package/src/serve/text-completion-stage.ts +104 -0
  249. package/src/serve/token-streams.ts +519 -0
  250. package/src/server.ts +523 -3551
  251. package/src/spec/assistant-source.ts +10 -44
  252. package/src/spec/deepspec-source.ts +6 -6
  253. package/src/spec/dflash-source.ts +9 -8
  254. package/src/spec/dspark/ab-stats.ts +3 -3
  255. package/src/spec/dspark/calibration.ts +2 -2
  256. package/src/spec/dspark/deepspec-module.ts +2 -2
  257. package/src/spec/dspark/generate-dflash.ts +2 -2
  258. package/src/spec/dspark/generate.ts +2 -2
  259. package/src/spec/dspark/loader.ts +1 -1
  260. package/src/spec/dspark/module-dflash.ts +5 -4
  261. package/src/spec/dspark/module.ts +3 -3
  262. package/src/spec/dspark/quantize-drafter.ts +1 -1
  263. package/src/spec/generate.ts +1 -1
  264. package/src/spec/glm52-mtp-source.ts +9 -1
  265. package/src/spec/ngram-source.ts +1 -1
  266. package/src/spec/prefix-state.ts +45 -0
  267. package/src/spec/qwen-mtp-source.ts +528 -0
  268. package/src/spec/serve-loop.ts +179 -213
  269. package/src/spec/source.ts +42 -10
  270. package/src/spec/two-model.ts +1 -1
  271. package/src/ssd-cache.ts +95 -24
  272. package/src/ssd-durability.ts +207 -0
  273. package/src/tool-call.ts +51 -14
  274. package/src/train/flash-cce.ts +14 -13
  275. package/src/train/forward.ts +38 -1
  276. package/src/train/loss.ts +3 -2
  277. package/src/train/prefix-shared.ts +5 -1
  278. package/src/train/segmented.ts +9 -6
  279. package/src/train/trainer.ts +9 -8
  280. package/src/vision/preprocess.ts +1 -1
  281. package/src/vision/prompt.ts +48 -2
  282. package/src/vision/qwen3vl-preprocess.ts +409 -0
  283. package/src/vision/qwen3vl-prompt.ts +196 -0
  284. package/src/vision/qwen3vl-tower.ts +555 -0
  285. package/src/vision/video-frames.ts +148 -0
  286. package/src/web/app.html +1 -1
  287. package/src/web/app.js +15 -6
  288. package/src/web/src/adapters-panel.ts +3 -3
  289. package/src/web/src/assistant.ts +2 -36
  290. package/src/web/src/chat.ts +16 -9
  291. package/src/web/src/composer.ts +4 -4
  292. package/src/web/src/hub.ts +1 -1
  293. package/src/web/src/markdown.ts +0 -0
  294. package/src/web/src/memory-panel.ts +1 -1
  295. package/src/web/src/model-picker.ts +1 -1
  296. package/src/web/src/palette.ts +1 -1
  297. package/src/web/src/protocol.ts +3 -39
  298. package/src/web/src/sessions.ts +1 -1
  299. package/src/web/src/ui-catalog.ts +4 -18
  300. package/src/web-assets.ts +47 -0
  301. package/src/weights.ts +134 -33
  302. package/docs/archive/mlx-bun-lab-report.html +0 -275
  303. package/docs/archive/wwdc-mlx-bun.html +0 -848
  304. package/docs/design/adapters-end-to-end.md +0 -205
  305. package/docs/design/batching-perf-path.md +0 -255
  306. package/docs/design/batching-v2-plan.md +0 -529
  307. package/docs/design/bucketing-stage.md +0 -197
  308. package/docs/design/compat-cli-surface-design.md +0 -230
  309. package/docs/design/docs-reorg-plan.md +0 -165
  310. package/docs/design/dspark-serving-program.md +0 -371
  311. package/docs/design/dspark-speculative-decoding.md +0 -268
  312. package/docs/design/grammar-spec-batching-integration.md +0 -301
  313. package/docs/design/memory-inference-path.md +0 -422
  314. package/docs/design/memory-synthesis.md +0 -287
  315. package/docs/design/memory-system.md +0 -237
  316. package/docs/design/mlx-lm-tool-parity-plan.md +0 -545
  317. package/docs/design/omlx-adoption-map.md +0 -98
  318. package/docs/design/orpo-dynamic-lambda.md +0 -220
  319. package/docs/design/orpo-future-enhancements.md +0 -340
  320. package/docs/design/paged-kv-cache.md +0 -214
  321. package/docs/design/parallel-slots.md +0 -567
  322. package/docs/design/parity-tier-dag.md +0 -161
  323. package/docs/design/repo-cleanup-plan.md +0 -189
  324. package/docs/design/runtime-isolation.md +0 -263
  325. package/docs/design/segmented-backward-training.md +0 -543
  326. package/docs/design/spec-decode-larger-targets.md +0 -229
  327. package/docs/design/ssd-kv-cold-tier.md +0 -288
  328. package/docs/design/structured-output.md +0 -484
  329. package/docs/design/tauri-desktop-app.md +0 -127
  330. package/docs/design/the-dreaming-master-plan.md +0 -445
  331. package/docs/design/turboquant-kv.md +0 -235
  332. package/docs/design/w4a16-compute-precision-spike.md +0 -232
  333. package/docs/design/web-chat-beat-matrix.md +0 -315
  334. package/docs/design/write-pipeline-entity.md +0 -468
  335. package/docs/investigations/curve-runs/bisector-route.json +0 -98
  336. package/docs/investigations/curve-runs/bisector-trace.json +0 -1393
  337. package/docs/investigations/curve-runs/curve-terrain.json +0 -1179
  338. package/docs/investigations/curve-runs/curve-terrain.svg +0 -42
  339. package/docs/investigations/hlg-ab.html +0 -34
  340. package/docs/investigations/hlg-explorer.html +0 -238
  341. package/docs/investigations/hlg-figs/cliff.svg +0 -1
  342. package/docs/investigations/hlg-figs/cross.svg +0 -1
  343. package/docs/investigations/hlg-figs/edges.svg +0 -1
  344. package/docs/investigations/hlg-figs/k20-grids.svg +0 -1
  345. package/docs/investigations/hlg-figs/k20-peaks.svg +0 -1
  346. package/docs/investigations/hlg-figs/lw-curve.svg +0 -1
  347. package/docs/investigations/hlg-figs/terrain.svg +0 -1
  348. package/docs/investigations/hlg-report.html +0 -262
  349. package/docs/investigations/hlg-runs/hlg-ab.json +0 -149
  350. package/docs/investigations/hlg-runs/hlg-blind-prompts.md +0 -87
  351. package/docs/investigations/hlg-runs/hlg-blind.json +0 -107
  352. package/docs/investigations/hlg-runs/hlg-cliff.json +0 -197
  353. package/docs/investigations/hlg-runs/hlg-cross.json +0 -505
  354. package/docs/investigations/hlg-runs/hlg-edge.json +0 -388
  355. package/docs/investigations/hlg-runs/hlg-eval1.json +0 -25
  356. package/docs/investigations/hlg-runs/hlg-frontier.json +0 -86
  357. package/docs/investigations/hlg-runs/hlg-grid.json +0 -242
  358. package/docs/investigations/hlg-runs/hlg-grid2.json +0 -343
  359. package/docs/investigations/hlg-runs/hlg-lw-coarse.json +0 -110
  360. package/docs/investigations/hlg-runs/hlg-map.json +0 -383
  361. package/docs/investigations/hlg-runs/run0-original.log +0 -159
  362. package/docs/investigations/hlg-runs/runA-recommended.log +0 -131
  363. package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +0 -26
  364. package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +0 -18
  365. package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +0 -25
  366. package/docs/investigations/hlg-runs/runAD-ab-samples.log +0 -8
  367. package/docs/investigations/hlg-runs/runAE-edge-e4b.log +0 -51
  368. package/docs/investigations/hlg-runs/runAF-logit-calibration.log +0 -10
  369. package/docs/investigations/hlg-runs/runAG-eval-config2.log +0 -16
  370. package/docs/investigations/hlg-runs/runB-gain-variants.log +0 -192
  371. package/docs/investigations/hlg-runs/runC-pivots.log +0 -162
  372. package/docs/investigations/hlg-runs/runD-oetf.log +0 -132
  373. package/docs/investigations/hlg-runs/runE-eotf.log +0 -162
  374. package/docs/investigations/hlg-runs/runF-shaper-working.log +0 -86
  375. package/docs/investigations/hlg-runs/runG-shaper-sweep.log +0 -101
  376. package/docs/investigations/hlg-runs/runH-wsweep.log +0 -176
  377. package/docs/investigations/hlg-runs/runI-asweep.log +0 -176
  378. package/docs/investigations/hlg-runs/runJ-knobmap.log +0 -49
  379. package/docs/investigations/hlg-runs/runK-frontier.log +0 -19
  380. package/docs/investigations/hlg-runs/runL-widemap.log +0 -65
  381. package/docs/investigations/hlg-runs/runM-frontier-emb.log +0 -15
  382. package/docs/investigations/hlg-runs/runN-verify.log +0 -116
  383. package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +0 -15
  384. package/docs/investigations/hlg-runs/runP-grid-AxSm.log +0 -15
  385. package/docs/investigations/hlg-runs/runQ-grid-tgW.log +0 -15
  386. package/docs/investigations/hlg-runs/runR-blind-transcript.log +0 -79
  387. package/docs/investigations/hlg-runs/runS-cross-coherence.log +0 -33
  388. package/docs/investigations/hlg-runs/runT-cross-cpm5.log +0 -33
  389. package/docs/investigations/hlg-runs/runU-cross-12b.log +0 -33
  390. package/docs/investigations/hlg-runs/runV-lw-e4b.log +0 -22
  391. package/docs/investigations/hlg-runs/runW-lw-12b.log +0 -22
  392. package/docs/investigations/hlg-runs/runX-lw-cpm5.log +0 -22
  393. package/docs/investigations/hlg-runs/runY-cliff-e4b.log +0 -26
  394. package/docs/investigations/hlg-runs/runZ-cliff-12b.log +0 -26
  395. package/docs/planning/IDEAS.md +0 -69
  396. package/docs/reference/embedding.md +0 -197
  397. package/docs/reference/features-matrix.md +0 -100
  398. package/docs/reference/orpo-quickstart.md +0 -135
  399. package/src/flags.ts +0 -13
  400. /package/docs/{investigations → archive/investigations}/chunk-finetune-experiment.md +0 -0
  401. /package/docs/{investigations → archive/investigations}/colibri-oracle-pin.md +0 -0
  402. /package/docs/{investigations → archive/investigations}/curve-bisector-routing.md +0 -0
  403. /package/docs/{investigations → archive/investigations}/decode-roofline-lookagain.md +0 -0
  404. /package/docs/{investigations → archive/investigations}/dspark-handoff.md +0 -0
  405. /package/docs/{investigations → archive/investigations}/expert-offload-single-user-moe.md +0 -0
  406. /package/docs/{investigations → archive/investigations}/indexshare-performance-spike.md +0 -0
  407. /package/docs/{investigations → archive/investigations}/kernel-perf-review-2026-07.md +0 -0
  408. /package/docs/{investigations → archive/investigations}/lab-build-journal.md +0 -0
  409. /package/docs/{investigations → archive/investigations}/openwiki-evaluation.md +0 -0
  410. /package/docs/{investigations → archive/investigations}/orpo-base-uf-experiment-and-directions.md +0 -0
  411. /package/docs/{investigations → archive/investigations}/orpo-flash-cce-pin-leak.md +0 -0
  412. /package/docs/{investigations → archive/investigations}/orpo-uf-testing-handoff.md +0 -0
  413. /package/docs/{investigations → archive/investigations}/pi-builtin-investigation.md +0 -0
  414. /package/docs/{investigations → archive/investigations}/segmented-backward-handoff.md +0 -0
  415. /package/docs/{investigations → archive/investigations}/starter-model-port-handoff.md +0 -0
  416. /package/docs/{investigations → archive/investigations}/steel-flash-cce-handoff.md +0 -0
  417. /package/docs/{investigations → archive/investigations}/trainer-validation-experiment.md +0 -0
  418. /package/docs/{planning → archive/planning}/chat-ui-vision-memory-first.md +0 -0
  419. /package/docs/{planning → archive/planning}/journal.md +0 -0
  420. /package/docs/{planning → archive/planning}/pi-integration-review.md +0 -0
  421. /package/docs/{planning → archive/planning}/release-notes-v0.0.10.md +0 -0
  422. /package/docs/{planning → archive/planning}/release-notes-v0.0.11.md +0 -0
  423. /package/docs/{planning → archive/planning}/release-notes-v0.0.12.md +0 -0
  424. /package/docs/{planning → archive/planning}/release-notes-v0.0.13.md +0 -0
  425. /package/docs/{planning → archive/planning}/web-ui-pass-plan.md +0 -0
  426. /package/{docs/investigations → src/serve/assets}/curve-terrain.html +0 -0
  427. /package/{docs/dag → src/serve/assets}/training-inference-map.html +0 -0
@@ -0,0 +1,104 @@
1
+ // TextCompletionStage: TextCompletionRequest in, InferenceRequest out.
2
+ // mlx_lm.server's raw text completion: NO chat template — the prompt string
3
+ // is tokenized directly (tokenizer.encode with the tokenizer's own
4
+ // special-token handling, exactly mlx-lm's `tokenizer.encode(request.prompt)`).
5
+ // Same admission / adapter / lane path as chat; no tool router or thinking
6
+ // splitter (raw text in, raw text out).
7
+ import type { ServingContext } from "./model-host";
8
+ import type { TextCompletionRequest } from "./chat-request";
9
+ import type { InferenceRequest } from "./inference-request";
10
+ import { RequestError } from "./pipeline";
11
+ import { RequestOwnership } from "./request-plan";
12
+ import type { RequestPrep } from "./request-prep";
13
+ import { StopMatcher, ThinkingTagSplitter, ToolAwareStream } from "./token-streams";
14
+
15
+ export class TextCompletionStage {
16
+ constructor(
17
+ private readonly ctx: ServingContext,
18
+ private readonly prep: RequestPrep,
19
+ private readonly maxSafeContext: number,
20
+ /** `--max-tokens` / the GLM plan's reservation. No invented fallback:
21
+ * unset runs to EOS or the admitted context (with no template an EOS
22
+ * may never come — that run is bounded by admission, the real limit). */
23
+ private readonly defaultGeneratedTokens: number | undefined,
24
+ private readonly defaultAdapter?: string,
25
+ ) {}
26
+
27
+ async run(request: TextCompletionRequest, requestId?: string): Promise<InferenceRequest> {
28
+ const { ctx, prep } = this;
29
+ const body = request.params;
30
+ const id = requestId ?? `cmpl-${crypto.randomUUID()}`;
31
+ const promptIds = ctx.tokenizer.encode(body.prompt);
32
+ const ownership = new RequestOwnership();
33
+ const warnings: string[] = [];
34
+ const reject = (message: string): never => {
35
+ ownership.dispose();
36
+ throw new RequestError(400, message);
37
+ };
38
+ let options: ReturnType<RequestPrep["toOptions"]>;
39
+ try {
40
+ options = prep.toOptions(body as never);
41
+ } catch (e) {
42
+ // bad logit_bias (non-numeric keys/values) — mlx-lm's coercion error
43
+ return reject((e as Error).message);
44
+ }
45
+ // Grammar-constrained decoding on raw completions too (response_format /
46
+ // guided_* / structured_outputs). The degrade path has no chat template
47
+ // to inject a system message into, so a degrade only emits the Warning
48
+ // header (no prompt injection) — documented gap vs the chat lane, which
49
+ // mirrors oMLX's text-completions behavior.
50
+ const grammarReq =
51
+ body.response_format != null || !!body.guided_grammar ||
52
+ !!body.guided_regex || !!body.guided_choice?.length ||
53
+ body.structured_outputs != null;
54
+ if (grammarReq) {
55
+ const g = await prep.compileGrammarForRequest(body as never);
56
+ if (g.controller) options.grammar = ownership.own(g.controller);
57
+ else if (g.degradeHint)
58
+ warnings.push(
59
+ `grammar not enforced: ${g.degradeHint} - no prompt injection on /v1/completions`);
60
+ }
61
+ // Unset = no request-level ceiling; admission clamps to the admitted
62
+ // context. DEVIATION from mlx_lm.server (which stops a defaulted
63
+ // request at 512): pass --max-tokens 512 to reproduce the reference.
64
+ const requestedMaxTokens = body.max_completion_tokens ?? body.max_tokens ??
65
+ this.defaultGeneratedTokens ?? Infinity;
66
+ let adapterIds: string[];
67
+ try {
68
+ adapterIds = ctx.adapters.resolveSpec(body.adapter ?? this.defaultAdapter);
69
+ } catch (e) {
70
+ return reject((e as Error).message);
71
+ }
72
+ const wantLogprobs = body.logprobs === true;
73
+ const topLogprobs =
74
+ typeof body.top_logprobs === "number" && body.top_logprobs > 0
75
+ ? body.top_logprobs : 0;
76
+ return {
77
+ requestId: id,
78
+ stream: request.stream,
79
+ warnings,
80
+ plan: {
81
+ promptIds,
82
+ options,
83
+ requestedMaxTokens,
84
+ maxSafeContext: this.maxSafeContext,
85
+ stream: request.stream,
86
+ wantLogprobs,
87
+ topLogprobs,
88
+ adapterIds,
89
+ hasVision: false,
90
+ userSeed: body.seed !== undefined,
91
+ hasGrammar: !!options.grammar,
92
+ hasDraft: !!ctx.draft,
93
+ ownership,
94
+ },
95
+ pipeline: {
96
+ router: new ToolAwareStream(ctx.tokenizer, "plain", null),
97
+ stopper: new StopMatcher(options.stopSequences),
98
+ thinking: new ThinkingTagSplitter(false),
99
+ collectToolCalls: false,
100
+ },
101
+ idToToken: (tokenId) => ctx.tokenizer.idToToken(tokenId),
102
+ };
103
+ }
104
+ }
@@ -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
+ }