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
package/src/ssd-cache.ts CHANGED
@@ -1,4 +1,4 @@
1
- // SSD cold tier for the prompt/KV cache (docs/design/ssd-kv-cold-tier.md).
1
+ // SSD cold tier for the prompt/KV cache (docs/design/kv-cache.md).
2
2
  //
3
3
  // Files ARE the database: no sidecar index. Layout
4
4
  // <dir>/<configFingerprint>/<nsHash>/<uuid>.mlxkv
@@ -22,6 +22,7 @@ import { randomUUID } from "node:crypto";
22
22
  import { commonPrefixLength } from "./prompt-cache";
23
23
  import {
24
24
  saveKvCache, saveKvCacheAsync, loadKvCache, readKvHeader,
25
+ cacheHeadersTrimmable, legacyCacheCodecs, type CacheCodecProvider,
25
26
  type KvSaveMeta, type KvLoadExpect, type LoadedKvCache,
26
27
  } from "./kv-store";
27
28
  import type { Cache } from "./model/gemma4-base";
@@ -38,22 +39,11 @@ export interface SsdIndexEntry {
38
39
  * over a shorter usable one (the 2026-07-06 restart-0 defect: the
39
40
  * [prompt+gen] file always won, always got rejected after restore). */
40
41
  trimmable: boolean;
42
+ generationCheckpoint?: NonNullable<KvSaveMeta["generationCheckpoint"]>;
41
43
  }
42
44
 
43
- /** Trimmability from a file header alone (mirror of the live caches'
44
- * isTrimmable(): kv/qkv/turboquant always trim; rotating kinds only pre-wrap
45
- * (offset < maxSize); GLM compressed kinds always trim; ssm never). Keep in
46
- * sync with gemma4-base/qwen3-delta/glm52-cache. */
47
- const headerTrimmable = (caches: { kind: string; offset: number; maxSize?: number }[]): boolean =>
48
- caches.every((c) =>
49
- c.kind === "kv" || c.kind === "qkv" || c.kind === "turboquant" ||
50
- c.kind === "mla" || c.kind === "mla-dsa" || c.kind === "mtp-mla"
51
- ? true
52
- : c.kind === "rotating" || c.kind === "rotating-qkv"
53
- ? c.offset < (c.maxSize ?? 0)
54
- : false);
55
-
56
45
  export interface SsdStoreOptions {
46
+ codecs?: CacheCodecProvider;
57
47
  dir: string;
58
48
  maxBytes: number;
59
49
  /** Server identity — written into every file, enforced on scan/load. */
@@ -69,6 +59,7 @@ const nsHash = (ns: string): string =>
69
59
 
70
60
  export class SsdCacheStore {
71
61
  readonly #opts: SsdStoreOptions;
62
+ readonly #codecs: CacheCodecProvider;
72
63
  readonly #root: string; // <dir>/<configFingerprint>
73
64
  #index: SsdIndexEntry[] = [];
74
65
  #warnedWriteFailure = false;
@@ -76,6 +67,7 @@ export class SsdCacheStore {
76
67
 
77
68
  constructor(opts: SsdStoreOptions) {
78
69
  this.#opts = opts;
70
+ this.#codecs = opts.codecs ?? legacyCacheCodecs;
79
71
  this.#root = join(opts.dir, opts.configFingerprint);
80
72
  }
81
73
 
@@ -91,6 +83,21 @@ export class SsdCacheStore {
91
83
  return this.#opts.maxBytes;
92
84
  }
93
85
 
86
+ /** Longest token prefix whose atomic cache file is present in the index. */
87
+ get longestDurablePrefixTokens(): number {
88
+ return this.#index.reduce((best, entry) => Math.max(best, entry.tokens.length), 0);
89
+ }
90
+
91
+ /** True when the SSD index can seed every token in this prefix. A longer
92
+ * trimmable descendant is equivalent to an exact boundary snapshot. */
93
+ hasDurablePrefix(tokens: number[], ns = ""): boolean {
94
+ return this.#index.some((entry) => {
95
+ if (entry.ns !== ns || entry.tokens.length < tokens.length) return false;
96
+ if (commonPrefixLength(entry.tokens, tokens) !== tokens.length) return false;
97
+ return entry.tokens.length === tokens.length || entry.trimmable;
98
+ });
99
+ }
100
+
94
101
  /** Startup recovery: rebuild the in-memory index from disk. Corrupt
95
102
  * headers and metadata mismatches are unlinked; `.tmp` orphans reaped.
96
103
  * Only OUR fingerprint dir is touched. Returns entries indexed. */
@@ -118,10 +125,14 @@ export class SsdCacheStore {
118
125
  h.tokenizerHash !== this.#opts.tokenizerHash
119
126
  )
120
127
  throw new Error("metadata mismatch");
128
+ if ((h.codecProvider ?? legacyCacheCodecs.id) !== this.#codecs.id) continue;
121
129
  const st = statSync(path);
122
130
  this.#index.push({
123
131
  path, ns: h.ns ?? "", tokens: h.tokens, bytes: st.size, mtimeMs: st.mtimeMs,
124
- trimmable: headerTrimmable(h.caches),
132
+ trimmable: cacheHeadersTrimmable(h.caches, this.#codecs),
133
+ ...(h.generationCheckpoint
134
+ ? { generationCheckpoint: h.generationCheckpoint }
135
+ : {}),
125
136
  });
126
137
  } catch {
127
138
  try { rmSync(path, { force: true }); } catch {}
@@ -168,7 +179,7 @@ export class SsdCacheStore {
168
179
  ns: entry.ns,
169
180
  verify: this.#opts.verify,
170
181
  };
171
- const loaded = loadKvCache(entry.path, model, expect);
182
+ const loaded = loadKvCache(entry.path, model, expect, this.#codecs);
172
183
  const now = new Date();
173
184
  try { utimesSync(entry.path, now, now); entry.mtimeMs = now.getTime(); } catch {}
174
185
  this.stats.restores++;
@@ -195,7 +206,7 @@ export class SsdCacheStore {
195
206
  const path = join(dir, `${randomUUID()}.mlxkv`);
196
207
  try {
197
208
  mkdirSync(dir, { recursive: true });
198
- saveKvCache(path, tokens, caches, this.#meta(ns));
209
+ saveKvCache(path, tokens, caches, this.#meta(ns), this.#codecs);
199
210
  return this.#indexStored(path, tokens, caches, ns);
200
211
  } catch (err) {
201
212
  return this.#storeFailed(path, err);
@@ -207,21 +218,75 @@ export class SsdCacheStore {
207
218
  * yields the event loop between tensors so serving interleaves.
208
219
  * Caller passes zero-copy CLONES it owns (a consistent snapshot no
209
220
  * matter what the live entry does meanwhile) and disposes them after.
210
- * `waitTurn` gates every per-tensor step (see saveKvCacheAsync) — the
211
- * server passes the gateway's onIdle so the flush only progresses while
212
- * the engine is idle, never mid-decode. */
213
- async storeAsync(tokens: number[], caches: Cache[], ns = "", waitTurn?: () => Promise<void>): Promise<boolean> {
221
+ * `runStep` owns every per-tensor step (see saveKvCacheAsync) — the
222
+ * server passes the gateway's exclusive runner so a request cannot start
223
+ * between an idle check and the blocking MLX readback. */
224
+ async storeAsync(
225
+ tokens: number[], caches: Cache[], ns = "",
226
+ runStep?: <T>(step: () => T) => Promise<T>,
227
+ ): Promise<boolean> {
214
228
  const dir = join(this.#root, nsHash(ns));
215
229
  const path = join(dir, `${randomUUID()}.mlxkv`);
216
230
  try {
217
231
  mkdirSync(dir, { recursive: true });
218
- await saveKvCacheAsync(path, tokens, caches, this.#meta(ns), waitTurn);
232
+ await saveKvCacheAsync(path, tokens, caches, this.#meta(ns), runStep, this.#codecs);
219
233
  return this.#indexStored(path, tokens, caches, ns);
220
234
  } catch (err) {
221
235
  return this.#storeFailed(path, err);
222
236
  }
223
237
  }
224
238
 
239
+ /** Longest durable in-flight generation for an identical request. The
240
+ * checkpoint namespace is separate from the ordinary prompt-cache tier,
241
+ * so incomplete assistant output can never seed an unrelated prefill. */
242
+ findGenerationCheckpoint(
243
+ prompt: number[], key: string, cacheNs = "",
244
+ ): SsdIndexEntry | null {
245
+ let best: SsdIndexEntry | null = null;
246
+ for (const entry of this.#index) {
247
+ const checkpoint = entry.generationCheckpoint;
248
+ if (!checkpoint || checkpoint.key !== key || checkpoint.cacheNs !== cacheNs)
249
+ continue;
250
+ if (checkpoint.originalPromptTokens !== prompt.length) continue;
251
+ if (entry.tokens.length <= prompt.length) continue;
252
+ if (commonPrefixLength(entry.tokens, prompt) !== prompt.length) continue;
253
+ if (!best || entry.tokens.length > best.tokens.length) best = entry;
254
+ }
255
+ return best;
256
+ }
257
+
258
+ /** Persist one in-flight generation atomically. A completed newer
259
+ * checkpoint supersedes older checkpoints for the same request only after
260
+ * its rename succeeds, so a crash during a write leaves the prior point. */
261
+ async storeGenerationCheckpoint(
262
+ tokens: number[], caches: Cache[], checkpoint: NonNullable<KvSaveMeta["generationCheckpoint"]>,
263
+ ): Promise<boolean> {
264
+ const ns = `__generation_checkpoint__:${checkpoint.key}`;
265
+ const dir = join(this.#root, nsHash(ns));
266
+ const path = join(dir, `${randomUUID()}.mlxkv`);
267
+ try {
268
+ mkdirSync(dir, { recursive: true });
269
+ await saveKvCacheAsync(path, tokens, caches, {
270
+ ...this.#meta(ns), generationCheckpoint: checkpoint,
271
+ }, undefined, this.#codecs);
272
+ const stored = this.#indexStored(path, tokens, caches, ns, checkpoint);
273
+ if (stored) {
274
+ for (const entry of [...this.#index]) {
275
+ if (entry.path === path) continue;
276
+ if (entry.generationCheckpoint?.key === checkpoint.key) this.remove(entry.path);
277
+ }
278
+ }
279
+ return stored;
280
+ } catch (err) {
281
+ return this.#storeFailed(path, err);
282
+ }
283
+ }
284
+
285
+ removeGenerationCheckpoints(key: string): void {
286
+ for (const entry of [...this.#index])
287
+ if (entry.generationCheckpoint?.key === key) this.remove(entry.path);
288
+ }
289
+
225
290
  #meta(ns: string): KvSaveMeta {
226
291
  return {
227
292
  modelId: this.#opts.modelId,
@@ -231,7 +296,10 @@ export class SsdCacheStore {
231
296
  };
232
297
  }
233
298
 
234
- #indexStored(path: string, tokens: number[], caches: Cache[], ns: string): boolean {
299
+ #indexStored(
300
+ path: string, tokens: number[], caches: Cache[], ns: string,
301
+ generationCheckpoint?: NonNullable<KvSaveMeta["generationCheckpoint"]>,
302
+ ): boolean {
235
303
  const st = statSync(path);
236
304
  if (st.size > this.#opts.maxBytes) {
237
305
  rmSync(path, { force: true });
@@ -257,7 +325,10 @@ export class SsdCacheStore {
257
325
  if (commonPrefixLength(e.tokens, tokens) === e.tokens.length) this.remove(e.path);
258
326
  }
259
327
  }
260
- this.#index.push({ path, ns, tokens, bytes: st.size, mtimeMs: Date.now(), trimmable });
328
+ this.#index.push({
329
+ path, ns, tokens, bytes: st.size, mtimeMs: Date.now(), trimmable,
330
+ ...(generationCheckpoint ? { generationCheckpoint } : {}),
331
+ });
261
332
  this.stats.spills++;
262
333
  this.evictToCap();
263
334
  return true;
@@ -0,0 +1,207 @@
1
+ import type { Cache } from "./model/gemma4";
2
+ import type { PromptCacheEntry } from "./prompt-cache";
3
+ import type { SpillItem, SpillQueue } from "./kv-store";
4
+
5
+ export interface DurabilityGateway {
6
+ readonly busy: boolean;
7
+ runExclusive<T>(fn: () => Promise<T>): Promise<T>;
8
+ }
9
+
10
+ export interface DurabilityPromptCache {
11
+ findExact(tokens: number[], ns?: string): PromptCacheEntry | null;
12
+ }
13
+
14
+ export interface DurabilitySnapshotStats {
15
+ pendingSnapshots: number;
16
+ pendingSpills: number;
17
+ pendingSpillBytes: number;
18
+ droppedSpills: number;
19
+ failedSpills: number;
20
+ }
21
+
22
+ export interface DurabilityFlushResult extends DurabilitySnapshotStats {
23
+ durable: boolean;
24
+ flushedSnapshots: number;
25
+ missingSnapshots: number;
26
+ elapsedMs: number;
27
+ }
28
+
29
+ interface DirtySnapshot {
30
+ key: string;
31
+ tokens: number[];
32
+ ns: string;
33
+ }
34
+
35
+ type StoreOutcome = "stored" | "missing" | "failed";
36
+
37
+ /**
38
+ * Turns debounced prompt-cache puts into a durability boundary.
39
+ *
40
+ * A dirty record stays present until SpillQueue reports that its atomic store
41
+ * completed. Queue drops and write failures therefore remain retryable during
42
+ * an explicit flush or graceful shutdown.
43
+ */
44
+ export class SsdDurabilityCoordinator {
45
+ readonly #dirty = new Map<string, DirtySnapshot>();
46
+ readonly #timers = new Map<string, ReturnType<typeof setTimeout>>();
47
+ readonly #attempts = new Map<string, Promise<StoreOutcome>>();
48
+ #flush: Promise<DurabilityFlushResult> | null = null;
49
+
50
+ constructor(
51
+ readonly gateway: DurabilityGateway,
52
+ readonly promptCache: DurabilityPromptCache,
53
+ readonly spillQueue: SpillQueue,
54
+ readonly cloneCaches: (caches: Cache[]) => Cache[],
55
+ readonly isAlreadyDurable: (tokens: number[], ns: string) => boolean = () => false,
56
+ readonly debounceMs = 1_000,
57
+ readonly busyRetryMs = 5_000,
58
+ ) {}
59
+
60
+ schedule(tokens: number[], ns = ""): void {
61
+ if (tokens.length === 0) return;
62
+ // PromptCache can hold unrelated entries with the same namespace and
63
+ // length. Include the tokens so one conversation cannot cancel another
64
+ // conversation's pending durability record.
65
+ const key = `${ns.length}:${ns}:${tokens.join(",")}`;
66
+ const rec: DirtySnapshot = { key, tokens: [...tokens], ns };
67
+ this.#dirty.set(key, rec);
68
+ this.#arm(key, this.debounceMs);
69
+ }
70
+
71
+ get stats(): DurabilitySnapshotStats {
72
+ return {
73
+ pendingSnapshots: this.#dirty.size,
74
+ pendingSpills: this.spillQueue.pendingCount,
75
+ pendingSpillBytes: this.spillQueue.pendingBytes,
76
+ droppedSpills: this.spillQueue.droppedCount,
77
+ failedSpills: this.spillQueue.failedCount,
78
+ };
79
+ }
80
+
81
+ /** Force every dirty RAM snapshot through the queue and wait for storage. */
82
+ flush(): Promise<DurabilityFlushResult> {
83
+ if (this.#flush) return this.#flush;
84
+ const started = performance.now();
85
+ this.#flush = this.#flushInner(started).finally(() => { this.#flush = null; });
86
+ return this.#flush;
87
+ }
88
+
89
+ #arm(key: string, delayMs: number): void {
90
+ const old = this.#timers.get(key);
91
+ if (old) clearTimeout(old);
92
+ const timer = setTimeout(() => {
93
+ this.#timers.delete(key);
94
+ void this.#attempt(key, false);
95
+ }, delayMs);
96
+ timer.unref?.();
97
+ this.#timers.set(key, timer);
98
+ }
99
+
100
+ async #attempt(key: string, force: boolean): Promise<StoreOutcome> {
101
+ const running = this.#attempts.get(key);
102
+ if (running) {
103
+ const outcome = await running;
104
+ if (!force || !this.#dirty.has(key)) return outcome;
105
+ }
106
+
107
+ const rec = this.#dirty.get(key);
108
+ if (!rec) return "stored";
109
+ if (!force && this.gateway.busy) {
110
+ this.#arm(key, this.busyRetryMs);
111
+ return "failed";
112
+ }
113
+
114
+ const task = this.#store(rec);
115
+ this.#attempts.set(key, task);
116
+ try {
117
+ const outcome = await task;
118
+ if (outcome === "stored" && this.#dirty.get(key) === rec)
119
+ this.#dirty.delete(key);
120
+ if (outcome !== "stored" && !force && this.#dirty.get(key) === rec)
121
+ this.#arm(key, this.busyRetryMs);
122
+ return outcome;
123
+ } finally {
124
+ if (this.#attempts.get(key) === task) this.#attempts.delete(key);
125
+ }
126
+ }
127
+
128
+ async #store(rec: DirtySnapshot): Promise<StoreOutcome> {
129
+ let snap: SpillItem | null = null;
130
+ try {
131
+ snap = await this.gateway.runExclusive(async () => {
132
+ if (this.isAlreadyDurable(rec.tokens, rec.ns)) return null;
133
+ const entry = this.promptCache.findExact(rec.tokens, rec.ns);
134
+ if (!entry) return null;
135
+ return {
136
+ tokens: [...entry.tokens],
137
+ caches: this.cloneCaches(entry.caches),
138
+ ns: rec.ns,
139
+ };
140
+ });
141
+ } catch {
142
+ return "failed";
143
+ }
144
+ if (!snap)
145
+ return this.isAlreadyDurable(rec.tokens, rec.ns) ? "stored" : "missing";
146
+ return await this.spillQueue.enqueue(snap) ? "stored" : "failed";
147
+ }
148
+
149
+ async #flushInner(started: number): Promise<DurabilityFlushResult> {
150
+ for (const timer of this.#timers.values()) clearTimeout(timer);
151
+ this.#timers.clear();
152
+
153
+ let flushedSnapshots = 0;
154
+ const missing = new Map<string, DirtySnapshot>();
155
+ const droppedBefore = this.spillQueue.droppedCount;
156
+ const failedBefore = this.spillQueue.failedCount;
157
+
158
+ if (this.#attempts.size > 0)
159
+ await Promise.allSettled([...this.#attempts.values()]);
160
+ await this.spillQueue.drain();
161
+
162
+ // Flush one entry at a time. This prevents the queue cap from dropping a
163
+ // boundary snapshot while a large final snapshot is already in flight.
164
+ // Attempt each record version once. Missing state stays dirty, so a
165
+ // second flush cannot report success without a real durable snapshot.
166
+ const attempted = new Set<DirtySnapshot>();
167
+ while (this.#dirty.size > 0) {
168
+ const records = [...this.#dirty.entries()].filter(([, rec]) => !attempted.has(rec));
169
+ if (records.length === 0) break;
170
+ for (const [key, rec] of records) {
171
+ if (this.#dirty.get(key) !== rec) continue;
172
+ attempted.add(rec);
173
+ const outcome = await this.#attempt(key, true);
174
+ await this.spillQueue.drain();
175
+ if (outcome === "stored") {
176
+ flushedSnapshots++;
177
+ } else if (outcome === "missing") {
178
+ missing.set(key, rec);
179
+ }
180
+ }
181
+ }
182
+
183
+ // A later trimmable snapshot may now cover a superseded RAM ancestor.
184
+ // Reconcile against committed SSD coverage after all writes settle;
185
+ // never clear a missing prefix merely because another write succeeded.
186
+ for (const [key, rec] of missing) {
187
+ if (!this.isAlreadyDurable(rec.tokens, rec.ns)) continue;
188
+ if (this.#dirty.get(key) === rec) this.#dirty.delete(key);
189
+ missing.delete(key);
190
+ flushedSnapshots++;
191
+ }
192
+
193
+ const stats = this.stats;
194
+ return {
195
+ ...stats,
196
+ durable:
197
+ stats.pendingSnapshots === 0 &&
198
+ stats.pendingSpills === 0 &&
199
+ stats.droppedSpills === droppedBefore &&
200
+ stats.failedSpills === failedBefore &&
201
+ missing.size === 0,
202
+ flushedSnapshots,
203
+ missingSnapshots: missing.size,
204
+ elapsedMs: performance.now() - started,
205
+ };
206
+ }
207
+ }
package/src/tool-call.ts CHANGED
@@ -161,7 +161,7 @@ export function parseToolCalls(text: string): ParsedToolCall[] {
161
161
 
162
162
  const TOOL_CALL_BLOCK_RE = /<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/gi;
163
163
  const XML_FUNCTION_EQUALS_RE = /^\s*<function=([^>\s]+)>\s*([\s\S]*?)\s*<\/function>\s*$/i;
164
- const XML_PARAMETER_EQUALS_RE = /<parameter=([^>\s]+)>\s*([\s\S]*?)\s*<\/parameter>/gi;
164
+ const XML_PARAMETER_EQUALS_RE = /<parameter=([^>\s]+)>([\s\S]*?)<\/parameter>/gi;
165
165
  const GLM52_ARG_KEY_RE = /<arg_key>/i;
166
166
  const GLM52_ARG_PAIR_RE = /<arg_key>\s*([\s\S]*?)\s*<\/arg_key>\s*<arg_value>\s*([\s\S]*?)\s*<\/arg_value>/gi;
167
167
  const XML_FUNCTION_ATTR_RE = /<function\s+name=["']([^"']+)["']\s*>\s*([\s\S]*?)\s*<\/function>/gi;
@@ -208,11 +208,9 @@ function toolParameterSchema(
208
208
  return null;
209
209
  }
210
210
 
211
- /** Oracle `_decode_tool_parameter_value`: string-typed params stay raw
212
- * text (a path like "2025" must not become a number); everything else
213
- * is JSON-decoded when possible. */
214
- function decodeToolValue(value: string, schema: Record<string, unknown> | null): unknown {
215
- const text = decodeXml(value);
211
+ /** Decode a value after its caller removes protocol-specific delimiters.
212
+ * String-typed params stay raw; everything else is JSON-decoded when possible. */
213
+ function decodeToolValue(text: string, schema: Record<string, unknown> | null): unknown {
216
214
  if (!text) return "";
217
215
  const types = schemaTypeNames(schema);
218
216
  if (types.size && [...types].every((t) => t === "string" || t === "null")) return text;
@@ -546,23 +544,59 @@ function parseJsonToolCall(block: string): ParsedToolCall | null {
546
544
  return repairs.length ? { ...call, repaired: true, repairs } : call;
547
545
  }
548
546
 
549
- function parseXmlEqualsToolCall(block: string, tools: ToolSpec[]): ParsedToolCall | null {
547
+ function parseXmlEqualsToolCall(block: string, tools: ToolSpec[], strict = false): ParsedToolCall | null {
550
548
  const match = XML_FUNCTION_EQUALS_RE.exec(block);
551
549
  XML_FUNCTION_EQUALS_RE.lastIndex = 0;
552
550
  if (!match) return null;
551
+ if (strict && (match.index !== 0 || match[0].length !== block.length)) return null;
553
552
  const name = match[1]!.trim();
554
553
  const body = match[2]!;
555
554
  const args: Record<string, unknown> = {};
555
+ let cursor = 0;
556
556
  for (const param of body.matchAll(XML_PARAMETER_EQUALS_RE)) {
557
+ if (strict && body.slice(cursor, param.index).trim()) return null;
557
558
  const key = param[1]!.trim();
558
559
  if (!key) throw new Error(`tool '${name}' contains an empty parameter name`);
559
- args[key] = decodeToolValue(param[2]!, toolParameterSchema(tools, name, key));
560
+ if (strict && Object.hasOwn(args, key)) return null;
561
+ // Qwen's format carries raw text, not XML entity-encoded text. Match
562
+ // mlx_lm.tool_parsers.qwen3_coder: remove one framing newline at each
563
+ // end, preserving indentation, literal entities and JSON string escapes.
564
+ const value = param[2]!.replace(/^\n|\n$/g, "");
565
+ args[key] = decodeToolValue(value, toolParameterSchema(tools, name, key));
566
+ cursor = param.index + param[0].length;
560
567
  }
561
568
  XML_PARAMETER_EQUALS_RE.lastIndex = 0;
569
+ if (strict && body.slice(cursor).trim()) return null;
562
570
  return { name, arguments: args };
563
571
  }
564
572
 
565
- function parseGlm52ToolCall(block: string, tools: ToolSpec[]): ParsedToolCall | null {
573
+ /** One complete envelope, with full consumption and no syntax repair. Used
574
+ * to establish structural fill boundaries, never to verify model predictions. */
575
+ export function parseStrictToolCall(text: string, tools: ToolSpec[]): ParsedToolCall | null {
576
+ const open = "<tool_call>", close = "</tool_call>";
577
+ if (!text.startsWith(open) || !text.endsWith(close)) return null;
578
+ const body = text.slice(open.length, -close.length).trim();
579
+ if (body.includes(open) || body.includes(close)) return null;
580
+ try {
581
+ let call: ParsedToolCall | null;
582
+ if (body.startsWith("{")) {
583
+ // JSON.parse must succeed before the existing payload interpretation.
584
+ JSON.parse(body);
585
+ call = parseJsonToolCall(body);
586
+ } else {
587
+ call = parseXmlEqualsToolCall(body, tools, true) ?? parseGlm52ToolCall(body, tools, true);
588
+ }
589
+ if (!call || call.repaired || !tools.some(tool => toolSpecName(tool) === call.name)) return null;
590
+ return call;
591
+ } catch { return null; }
592
+ }
593
+
594
+ /** A textual argument delimiter is complete only after its closing bracket. */
595
+ export function hasCompleteToolValueDelimiter(text: string): boolean {
596
+ return /^(?:<\/parameter>|<\/arg_value>)/.test(text.trimStart());
597
+ }
598
+
599
+ function parseGlm52ToolCall(block: string, tools: ToolSpec[], strict = false): ParsedToolCall | null {
566
600
  const firstArg = block.search(GLM52_ARG_KEY_RE);
567
601
  const name = (firstArg < 0 ? block : block.slice(0, firstArg)).trim();
568
602
  if (!name || !/^[^\s<>]+$/.test(name)) return null;
@@ -576,7 +610,8 @@ function parseGlm52ToolCall(block: string, tools: ToolSpec[]): ParsedToolCall |
576
610
  if (body.slice(cursor, pair.index).trim()) return null;
577
611
  const key = pair[1]!.trim();
578
612
  if (!key) throw new Error(`tool '${name}' contains an empty argument name`);
579
- args[key] = decodeToolValue(pair[2]!, toolParameterSchema(tools, name, key));
613
+ if (strict && Object.hasOwn(args, key)) return null;
614
+ args[key] = decodeToolValue(decodeXml(pair[2]!), toolParameterSchema(tools, name, key));
580
615
  cursor = pair.index + pair[0].length;
581
616
  matched = true;
582
617
  }
@@ -594,7 +629,7 @@ function parseXmlAttrToolCalls(text: string, tools: ToolSpec[]): ParsedToolCall[
594
629
  for (const param of body.matchAll(XML_PARAM_ATTR_RE)) {
595
630
  const key = param[1]!.trim();
596
631
  if (!key) throw new Error(`tool '${name}' contains an empty parameter name`);
597
- args[key] = decodeToolValue(param[2]!, toolParameterSchema(tools, name, key));
632
+ args[key] = decodeToolValue(decodeXml(param[2]!), toolParameterSchema(tools, name, key));
598
633
  }
599
634
  XML_PARAM_ATTR_RE.lastIndex = 0;
600
635
  calls.push({ name, arguments: args });
@@ -616,9 +651,11 @@ export function parseGeneratedToolCalls(text: string, tools: ToolSpec[]): Parsed
616
651
  const calls: ParsedToolCall[] = [];
617
652
  for (const block of text.matchAll(TOOL_CALL_BLOCK_RE)) {
618
653
  const body = block[1]!.trim();
619
- const parsed = parseJsonToolCall(body) ??
620
- parseXmlEqualsToolCall(body, tools) ??
621
- parseGlm52ToolCall(body, tools);
654
+ // A valid Qwen parameter may itself contain JSON. Dispatch by the outer
655
+ // format before JSON repair can mistake that value for the tool envelope.
656
+ const parsed = /^<function=/i.test(body)
657
+ ? parseXmlEqualsToolCall(body, tools)
658
+ : parseJsonToolCall(body) ?? parseGlm52ToolCall(body, tools);
622
659
  if (!parsed) throw new Error("unsupported tool_call payload format");
623
660
  calls.push(parsed);
624
661
  }
@@ -37,6 +37,7 @@ import { MlxArray } from "../mlx/array";
37
37
  import { Dtype } from "../mlx/ffi";
38
38
  import * as ops from "../mlx/ops";
39
39
  import { STEEL_QMM_HEADER } from "./steel-qmm-header";
40
+ import { runtimeValue } from "../runtime-config";
40
41
 
41
42
  /** The quantized head pieces the kernel needs (packed weight + per-group affine
42
43
  * scale/bias + bits/group_size). Mirrors HeadQuant in loss.ts. */
@@ -392,7 +393,7 @@ function fwdSgKernel(): MetalKernel {
392
393
  inputNames: ["h", "wp", "scales", "biases", "targets", "shape", "capv"],
393
394
  outputNames: ["blockMax_out", "blockSum_out", "blockTgt_out"],
394
395
  source: FWD_SG_SOURCE,
395
- header: `#include <metal_simdgroup_matrix>\n#define TG ${TG}\n#define BB ${SG_BB}\n#define BV ${SG_BV}\n#define BD ${SG_BD}\n#define BDX ${SG_BD + 4}\n#define BVX ${SG_BV + 4}\n#define CCE_SG_SKIPDQ ${process.env.MLX_BUN_CCE_SG_SKIPDQ === "1" ? 1 : 0}\n`,
396
+ header: `#include <metal_simdgroup_matrix>\n#define TG ${TG}\n#define BB ${SG_BB}\n#define BV ${SG_BV}\n#define BD ${SG_BD}\n#define BDX ${SG_BD + 4}\n#define BVX ${SG_BV + 4}\n#define CCE_SG_SKIPDQ ${runtimeValue("MLX_BUN_CCE_SG_SKIPDQ") === "1" ? 1 : 0}\n`,
396
397
  ensureRowContiguous: true,
397
398
  });
398
399
  }
@@ -404,7 +405,7 @@ function fwdSgKernel(): MetalKernel {
404
405
  // compute-bound regime. Removing the staging redundancy (one thread per packed
405
406
  // word; scale/bias once per group) cut it 1629→848 ms, beating the scalar
406
407
  // kernel's 1097 ms. Fall back to scalar (MLX_BUN_CCE_SCALAR=1) when H % SG_BD != 0.
407
- const USE_SIMDGROUP_FWD = process.env.MLX_BUN_CCE_SCALAR !== "1" && process.env.MLX_BUN_CCE_LANE !== "1";
408
+ const USE_SIMDGROUP_FWD = runtimeValue("MLX_BUN_CCE_SCALAR") !== "1" && runtimeValue("MLX_BUN_CCE_LANE") !== "1";
408
409
 
409
410
  // Backward: dh[t,d] = Σ_v coeff_v · dequant(W_v)[d], coeff_v = g_t·(onehot_v −
410
411
  // softmax_v)·sech²_v, softmax_v = exp(softcap(logit_v) − lse_t). Two-phase per
@@ -549,7 +550,7 @@ function bwdKernel(daccN: number): MetalKernel {
549
550
  inputNames: ["h", "wp", "scales", "biases", "targets", "lse", "gv", "shape", "capv"],
550
551
  outputNames: ["dh"],
551
552
  source: BWD_SOURCE,
552
- header: `#define TG ${TG}\n#define BLOCK_B ${BLOCK_B_BWD}\n#define DACC_N ${daccN}\n#define CCE_BWD_SKIP_P2DQ ${process.env.MLX_BUN_CCE_BWD_SKIP_P2DQ === "1" ? 1 : 0}\n#define CCE_BWD_SKIP_P2 ${process.env.MLX_BUN_CCE_BWD_SKIP_P2 === "1" ? 1 : 0}\n`,
553
+ header: `#define TG ${TG}\n#define BLOCK_B ${BLOCK_B_BWD}\n#define DACC_N ${daccN}\n#define CCE_BWD_SKIP_P2DQ ${runtimeValue("MLX_BUN_CCE_BWD_SKIP_P2DQ") === "1" ? 1 : 0}\n#define CCE_BWD_SKIP_P2 ${runtimeValue("MLX_BUN_CCE_BWD_SKIP_P2") === "1" ? 1 : 0}\n`,
553
554
  ensureRowContiguous: true,
554
555
  atomicOutputs: true, // dh is summed across the NBLK vocab-block programs
555
556
  });
@@ -727,7 +728,7 @@ const BWD_SG_SOURCE = String.raw`
727
728
  // (flipped 2026-07-02 — the kernel-perf-review backlog #1 flip). The old
728
729
  // 0.66%→2.7% dh-accuracy scare was on RANDOM targets (flat softmax, the filter's
729
730
  // worst case — see PLAN.md "filter-on-real-data"); the real-data gate
730
- // (scripts/experiments/flash-cce-filter-realdata.ts) measured, on REAL chunk-ORPO
731
+ // (flash-cce-filter-realdata.ts (deleted 2026-08-23; git history)) measured, on REAL chunk-ORPO
731
732
  // data (M1 Max, 2026-07-02): CPM5 eps 1e-5 = 0.343% dh for 1.41×; e4b = 0.158% dh
732
733
  // for 1.70× — and the teacher-forced full-logits fidelity shows the filter's added
733
734
  // error is far below the flash-vs-full fp-reassociation scatter that already
@@ -735,7 +736,7 @@ const BWD_SG_SOURCE = String.raw`
735
736
  // (The ~3.5× figure floating around older notes was the retired SG kernel, not
736
737
  // steel.) MLX_BUN_CCE_BWD_FILTER_EPS=0 restores exact gradients — eps "0"
737
738
  // compiles the filter OUT (#if CCE_BWD_FILTER 0), the identical pre-flip kernel.
738
- const BWD_FILTER_EPS = process.env.MLX_BUN_CCE_BWD_FILTER_EPS ?? "1e-5";
739
+ const BWD_FILTER_EPS = runtimeValue("MLX_BUN_CCE_BWD_FILTER_EPS") ?? "1e-5";
739
740
  // blockMax vocab-block early-exit eps (skip whole cold (token-block,vocab-block) programs
740
741
  // — attacks BOTH phases). DEFAULT ON at 1e-5 (flipped 2026-07-02). It is ~LOSSLESS when
741
742
  // it fires (only skips blocks whose max prob < eps AND which lack the target → ~0
@@ -744,7 +745,7 @@ const BWD_FILTER_EPS = process.env.MLX_BUN_CCE_BWD_FILTER_EPS ?? "1e-5";
744
745
  // at ≤0.004% dh; COMBINED with the coeff filter at 1e-5/1e-5 the backward is 1.71×
745
746
  // (CPM5) / 3.16× (e4b) vs exact. Fires only when the caller supplies blockMax (the
746
747
  // production head does); MLX_BUN_CCE_BWD_BLOCK_EPS=0 disables (compiled out).
747
- const BWD_BLOCK_EPS = process.env.MLX_BUN_CCE_BWD_BLOCK_EPS ?? "1e-5";
748
+ const BWD_BLOCK_EPS = runtimeValue("MLX_BUN_CCE_BWD_BLOCK_EPS") ?? "1e-5";
748
749
  const _bwdSgKernels = new Map<string, MetalKernel>();
749
750
  function bwdSgKernel(dimTiles: number, filterEps: string, blockEps: string): MetalKernel {
750
751
  const filterOn = Number.parseFloat(filterEps) > 0 ? 1 : 0;
@@ -780,7 +781,7 @@ function bwdSgKernel(dimTiles: number, filterEps: string, blockEps: string): Met
780
781
  // accumulated into D's frags LANE-LOCALLY (frag += frag — no get_coord math, the
781
782
  // per-lane element mapping is identical between temp and D). dh atomic-added
782
783
  // across the NBLK vocab-block programs. Validated dh ~1e-4 rel vs autograd.
783
- // (Design + parity: scripts/experiments/steel-bwd-htile-test.ts.)
784
+ // (Design + parity: steel-bwd-htile-test.ts (deleted 2026-08-23; git history).)
784
785
  // PHASE-2 DEQUANT: the Wd[32,HTw] staging uses MLX's vectorized+fused
785
786
  // QuantizedBlockLoader (the SAME loader the forward uses), NOT a manual scalar
786
787
  // dequant. The wrinkle: phase-2's GEMM contracts over vocab (K=32) and outputs H
@@ -1061,9 +1062,9 @@ export function flashCceForward(
1061
1062
  // tiling is clean (H,V,blockV all % 32). MLX_BUN_CCE_NOSTEEL=1 falls back to the
1062
1063
  // old kernels (simdgroup / scalar / lane).
1063
1064
  const blockV0 = Math.ceil(V / NBLK);
1064
- const useSteel = process.env.MLX_BUN_CCE_NOSTEEL !== "1" && H % 32 === 0 && V % 32 === 0 && blockV0 % 32 === 0;
1065
+ const useSteel = runtimeValue("MLX_BUN_CCE_NOSTEEL") !== "1" && H % 32 === 0 && V % 32 === 0 && blockV0 % 32 === 0;
1065
1066
  const useSimd = !useSteel && USE_SIMDGROUP_FWD && H % SG_BD === 0;
1066
- if (!useSteel && process.env.MLX_BUN_CCE_NOSTEEL !== "1") {
1067
+ if (!useSteel && runtimeValue("MLX_BUN_CCE_NOSTEEL") !== "1") {
1067
1068
  // NOTE blockV0 = ceil(V/NBLK) can violate %32 even when V%32==0 (e.g. V=50304)
1068
1069
  const fails = [
1069
1070
  H % 32 !== 0 ? `H%32!=0 (H=${H})` : null,
@@ -1072,7 +1073,7 @@ export function flashCceForward(
1072
1073
  ].filter(Boolean).join(", ");
1073
1074
  warnSteelFallback("forward", fails, useSimd ? "simdgroup kernel" : "scalar/lane kernel");
1074
1075
  }
1075
- const useLane = !useSteel && !useSimd && process.env.MLX_BUN_CCE_LANE === "1" && WPR % 32 === 0;
1076
+ const useLane = !useSteel && !useSimd && runtimeValue("MLX_BUN_CCE_LANE") === "1" && WPR % 32 === 0;
1076
1077
  const kernel = useSteel ? fwdSteelKernel(GS, head.bits) : useSimd ? fwdSgKernel() : useLane ? fwdLaneKernel() : fwdKernel();
1077
1078
  const tokTile = useSteel ? STEEL_BM : useSimd ? SG_BB : BLOCK_B;
1078
1079
  // steel + simdgroup read h as [M, H] (M on axis 0); lane + scalar read h TRANSPOSED
@@ -1178,11 +1179,11 @@ export function flashCceBackward(
1178
1179
  const blockV0 = Math.ceil(V / NBLK);
1179
1180
  // HTw == group_size for the phase-2 QuantizedBlockLoader (one group per H-tile); needs
1180
1181
  // H % GS == 0 (always, GR*GS=H) and GS % 32 == 0 (TNv = GS/32 frags per H-tile integral).
1181
- const useSteel = process.env.MLX_BUN_CCE_BWD_NOSTEEL !== "1" && H % 32 === 0 && V % 32 === 0 && blockV0 % 32 === 0 && H % GS === 0 && GS % 32 === 0;
1182
+ const useSteel = runtimeValue("MLX_BUN_CCE_BWD_NOSTEEL") !== "1" && H % 32 === 0 && V % 32 === 0 && blockV0 % 32 === 0 && H % GS === 0 && GS % 32 === 0;
1182
1183
  // simdgroup_matrix phase-2 GEMM is the next tier (5512→3913 ms, 1.41×) when the H
1183
1184
  // tiling is clean; MLX_BUN_CCE_BWD_SCALAR=1 forces the float4 scalar kernel.
1184
- const useSg = !useSteel && process.env.MLX_BUN_CCE_BWD_SCALAR !== "1" && H % (SGN * 8) === 0 && H % SG_BD === 0;
1185
- if (!useSteel && process.env.MLX_BUN_CCE_BWD_NOSTEEL !== "1") {
1185
+ const useSg = !useSteel && runtimeValue("MLX_BUN_CCE_BWD_SCALAR") !== "1" && H % (SGN * 8) === 0 && H % SG_BD === 0;
1186
+ if (!useSteel && runtimeValue("MLX_BUN_CCE_BWD_NOSTEEL") !== "1") {
1186
1187
  // NOTE blockV0 = ceil(V/NBLK) can violate %32 even when V%32==0 (e.g. V=50304)
1187
1188
  const fails = [
1188
1189
  H % 32 !== 0 ? `H%32!=0 (H=${H})` : null,