@mlx-bun/inference 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (467) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1475 -2
  3. package/THIRD_PARTY_NOTICES.md +3404 -0
  4. package/dist/native/libmlx_bun_ane.dylib +0 -0
  5. package/dist/native/libmlx_bun_expert_io.dylib +0 -0
  6. package/dist/native/mlx-bun-frame-extract +0 -0
  7. package/examples/fit-model.ts +25 -0
  8. package/examples/qwen3-forward.ts +42 -0
  9. package/examples/qwen3-generate.ts +28 -0
  10. package/examples/trellis-expand.ts +23 -0
  11. package/package.json +129 -4
  12. package/src/adapters/available.ts +110 -0
  13. package/src/adapters/index.ts +3 -0
  14. package/src/adapters/manager.ts +316 -0
  15. package/src/artifacts/auxiliary-files.ts +45 -0
  16. package/src/artifacts/config-dialects.ts +94 -0
  17. package/src/artifacts/config.ts +341 -0
  18. package/src/artifacts/draft-vocabulary.ts +30 -0
  19. package/src/artifacts/expert-offload-build.ts +75 -0
  20. package/src/artifacts/expert-offload.ts +89 -0
  21. package/src/artifacts/experts/index.ts +3 -0
  22. package/src/artifacts/experts/io.ts +386 -0
  23. package/src/artifacts/experts/residency.ts +1134 -0
  24. package/src/artifacts/experts/usage.ts +415 -0
  25. package/src/artifacts/fingerprint.ts +44 -0
  26. package/src/artifacts/glm52/container.ts +305 -0
  27. package/src/artifacts/glm52/expert-layout.ts +249 -0
  28. package/src/artifacts/glm52/index.ts +7 -0
  29. package/src/artifacts/glm52/kernel-layout.ts +124 -0
  30. package/src/artifacts/glm52/layout.ts +152 -0
  31. package/src/artifacts/glm52/memory.ts +500 -0
  32. package/src/artifacts/glm52/resident-weights.ts +135 -0
  33. package/src/artifacts/glm52/weights.ts +503 -0
  34. package/src/artifacts/glm52-config.ts +262 -0
  35. package/src/artifacts/identity.ts +94 -0
  36. package/src/artifacts/index.ts +15 -0
  37. package/src/artifacts/mmap.ts +90 -0
  38. package/src/artifacts/safetensors-writer.ts +249 -0
  39. package/src/artifacts/safetensors.ts +164 -0
  40. package/src/artifacts/turboquant-fold.ts +42 -0
  41. package/src/artifacts/weight-audit.ts +43 -0
  42. package/src/artifacts/weights.ts +255 -0
  43. package/src/contracts/index.ts +2 -0
  44. package/src/contracts/mlx/attention.ts +8 -0
  45. package/src/contracts/mlx/cache.ts +510 -0
  46. package/src/contracts/mlx/checkpoint.ts +10 -0
  47. package/src/contracts/mlx/draft-checkpoint.ts +14 -0
  48. package/src/contracts/mlx/draft-projection.ts +27 -0
  49. package/src/contracts/mlx/draft-target.ts +84 -0
  50. package/src/contracts/mlx/drafter.ts +176 -0
  51. package/src/contracts/mlx/forward-work.ts +23 -0
  52. package/src/contracts/mlx/glm52-weights.ts +25 -0
  53. package/src/contracts/mlx/graph.ts +309 -0
  54. package/src/contracts/mlx/head.ts +6 -0
  55. package/src/contracts/mlx/index.ts +15 -0
  56. package/src/contracts/mlx/kv-codec.ts +9 -0
  57. package/src/contracts/mlx/media.ts +113 -0
  58. package/src/contracts/mlx/positions.ts +16 -0
  59. package/src/contracts/mlx/row-table.ts +2 -0
  60. package/src/contracts/mlx/trainable.ts +103 -0
  61. package/src/contracts/mlx/whisper.ts +96 -0
  62. package/src/contracts/portable/composition.ts +106 -0
  63. package/src/contracts/portable/denoising.ts +17 -0
  64. package/src/contracts/portable/execution.ts +79 -0
  65. package/src/contracts/portable/generation.ts +103 -0
  66. package/src/contracts/portable/graph.ts +80 -0
  67. package/src/contracts/portable/index.ts +15 -0
  68. package/src/contracts/portable/memory-plan.ts +26 -0
  69. package/src/contracts/portable/object-cache.ts +8 -0
  70. package/src/contracts/portable/prefill.ts +63 -0
  71. package/src/contracts/portable/prefix-cache.ts +33 -0
  72. package/src/contracts/portable/prompt.ts +2 -0
  73. package/src/contracts/portable/resources.ts +25 -0
  74. package/src/contracts/portable/rollback.ts +7 -0
  75. package/src/contracts/portable/sampling.ts +44 -0
  76. package/src/contracts/portable/scheduling.ts +54 -0
  77. package/src/contracts/portable/token-delivery.ts +4 -0
  78. package/src/contracts/portable/tsconfig.json +14 -0
  79. package/src/embeddings/index.ts +1 -0
  80. package/src/embeddings/text.ts +85 -0
  81. package/src/execution/admission.ts +59 -0
  82. package/src/execution/batch-group.ts +1240 -0
  83. package/src/execution/batch-types.ts +225 -0
  84. package/src/execution/cancellation.ts +38 -0
  85. package/src/execution/continuation-request.ts +45 -0
  86. package/src/execution/continuation-types.ts +10 -0
  87. package/src/execution/coordinator.ts +70 -0
  88. package/src/execution/denoising-group.ts +180 -0
  89. package/src/execution/engine.ts +56 -0
  90. package/src/execution/fill-group.ts +505 -0
  91. package/src/execution/fit.ts +184 -0
  92. package/src/execution/gateway-binding.ts +178 -0
  93. package/src/execution/grammar-group.ts +224 -0
  94. package/src/execution/index.ts +18 -0
  95. package/src/execution/methods.ts +75 -0
  96. package/src/execution/mixed-iteration.ts +96 -0
  97. package/src/execution/plan.ts +65 -0
  98. package/src/execution/prefill-cohort.ts +100 -0
  99. package/src/execution/prefill-rows.ts +263 -0
  100. package/src/execution/request-policy.ts +19 -0
  101. package/src/execution/row-sampling.ts +49 -0
  102. package/src/execution/scheduler.ts +92 -0
  103. package/src/execution/session.ts +247 -0
  104. package/src/execution/speculative-group.ts +797 -0
  105. package/src/execution/tasks.ts +44 -0
  106. package/src/generation/autoregressive.ts +840 -0
  107. package/src/generation/bindings/autoregressive.ts +104 -0
  108. package/src/generation/bindings/denoising.ts +60 -0
  109. package/src/generation/bindings/prefill-policy.ts +36 -0
  110. package/src/generation/checkpoint-identity.ts +60 -0
  111. package/src/generation/denoising.ts +85 -0
  112. package/src/generation/diffusion.ts +583 -0
  113. package/src/generation/fill/echo-index.ts +188 -0
  114. package/src/generation/fill/index.ts +3 -0
  115. package/src/generation/fill/proposal.ts +69 -0
  116. package/src/generation/fill/schema-rows.ts +506 -0
  117. package/src/generation/fill/session.ts +464 -0
  118. package/src/generation/fill/tool-boundary.ts +103 -0
  119. package/src/generation/fill-append.ts +58 -0
  120. package/src/generation/generate.ts +44 -0
  121. package/src/generation/grammar-step.ts +39 -0
  122. package/src/generation/index.ts +14 -0
  123. package/src/generation/phase-dispatch.ts +115 -0
  124. package/src/generation/prefill.ts +33 -0
  125. package/src/generation/result.ts +49 -0
  126. package/src/generation/scopes.ts +157 -0
  127. package/src/generation/speculative/acceptance.ts +41 -0
  128. package/src/generation/speculative/adaptive-draft-gate.ts +166 -0
  129. package/src/generation/speculative/bindings/assistant-rows.ts +102 -0
  130. package/src/generation/speculative/bindings/autoregressive-rows.ts +124 -0
  131. package/src/generation/speculative/bindings/binding.ts +106 -0
  132. package/src/generation/speculative/bindings/deepspec-rows.ts +33 -0
  133. package/src/generation/speculative/bindings/draft-target.ts +10 -0
  134. package/src/generation/speculative/bindings/glm52-mtp-rows.ts +128 -0
  135. package/src/generation/speculative/bindings/projected-context-rows.ts +78 -0
  136. package/src/generation/speculative/bindings/projected-draft-rows.ts +95 -0
  137. package/src/generation/speculative/bindings/qwen-mtp-rows.ts +336 -0
  138. package/src/generation/speculative/bindings/two-model-rows.ts +63 -0
  139. package/src/generation/speculative/cache-identity.ts +16 -0
  140. package/src/generation/speculative/draft-kind.ts +149 -0
  141. package/src/generation/speculative/draft-policy.ts +41 -0
  142. package/src/generation/speculative/draft-registry.ts +118 -0
  143. package/src/generation/speculative/draft-vocab.ts +110 -0
  144. package/src/generation/speculative/index.ts +17 -0
  145. package/src/generation/speculative/output.ts +49 -0
  146. package/src/generation/speculative/round.ts +307 -0
  147. package/src/generation/speculative/source.ts +144 -0
  148. package/src/generation/speculative/sources/assistant-source.ts +64 -0
  149. package/src/generation/speculative/sources/deepspec-source.ts +70 -0
  150. package/src/generation/speculative/sources/dflash-source.ts +84 -0
  151. package/src/generation/speculative/sources/dflash2-source.ts +86 -0
  152. package/src/generation/speculative/sources/glm52-mtp-source.ts +66 -0
  153. package/src/generation/speculative/sources/ngram-source.ts +165 -0
  154. package/src/generation/speculative/sources/qwen-mtp-source.ts +154 -0
  155. package/src/generation/speculative/sources/two-model.ts +75 -0
  156. package/src/generation/types.ts +194 -0
  157. package/src/index.ts +20 -0
  158. package/src/input/audio/audiotoolbox.ts +112 -0
  159. package/src/input/audio/decode.ts +133 -0
  160. package/src/input/audio/features.ts +202 -0
  161. package/src/input/audio/index.ts +6 -0
  162. package/src/input/audio/transcode.ts +67 -0
  163. package/src/input/audio/whisper-mel.ts +185 -0
  164. package/src/input/audio/whisper-tokenizer.ts +238 -0
  165. package/src/input/chat-template.ts +389 -0
  166. package/src/input/index.ts +6 -0
  167. package/src/input/media-fetch.ts +439 -0
  168. package/src/input/packed-mixed.ts +43 -0
  169. package/src/input/special-tokens.ts +75 -0
  170. package/src/input/token-groups.ts +49 -0
  171. package/src/input/tokenizer.ts +336 -0
  172. package/src/input/tool-call.ts +689 -0
  173. package/src/input/vision/index.ts +3 -0
  174. package/src/input/vision/preprocess.ts +247 -0
  175. package/src/input/vision/prompt.ts +398 -0
  176. package/src/input/vision/qwen3vl-preprocess.ts +409 -0
  177. package/src/input/vision/qwen3vl-prompt.ts +237 -0
  178. package/src/input/vision/video-frames.ts +100 -0
  179. package/src/kernels/ane/fill.ts +101 -0
  180. package/src/kernels/ane/linear.ts +416 -0
  181. package/src/kernels/attention/flash.ts +694 -0
  182. package/src/kernels/attention/kvq4-decode.ts +155 -0
  183. package/src/kernels/attention/masks.ts +40 -0
  184. package/src/kernels/attention/multi-query.ts +193 -0
  185. package/src/kernels/attention/paged.ts +130 -0
  186. package/src/kernels/delta/gated.ts +306 -0
  187. package/src/kernels/delta/index.ts +3 -0
  188. package/src/kernels/delta/silu-rms-heads.ts +69 -0
  189. package/src/kernels/delta/state.ts +74 -0
  190. package/src/kernels/gated-rms-norm.ts +64 -0
  191. package/src/kernels/glm52/layout.ts +37 -0
  192. package/src/kernels/glm52/streamed-experts.ts +360 -0
  193. package/src/kernels/logits.ts +38 -0
  194. package/src/kernels/quantization/affine-row.ts +154 -0
  195. package/src/kernels/quantization/affine-rows.ts +36 -0
  196. package/src/kernels/quantization/affine-split-mma.ts +144 -0
  197. package/src/kernels/quantization/affine3-mma.ts +117 -0
  198. package/src/kernels/quantization/affine3-rows.ts +103 -0
  199. package/src/kernels/quantization/affine4-mma.ts +113 -0
  200. package/src/kernels/quantization/affine4-rows.ts +102 -0
  201. package/src/kernels/residual-rms-norm.ts +65 -0
  202. package/src/kernels/sampling/index.ts +2 -0
  203. package/src/kernels/sampling/normalized-argmax.ts +116 -0
  204. package/src/kernels/sampling/token-bitmask.ts +52 -0
  205. package/src/kernels/softcap.ts +35 -0
  206. package/src/kernels/trellis/codebook.ts +123 -0
  207. package/src/kernels/trellis/down-codebook.ts +184 -0
  208. package/src/kernels/trellis/down-factored.ts +180 -0
  209. package/src/kernels/trellis/down-k3-interleaved-factored.ts +146 -0
  210. package/src/kernels/trellis/down-mma-wide.ts +195 -0
  211. package/src/kernels/trellis/down-mma.ts +145 -0
  212. package/src/kernels/trellis/expand.ts +42 -0
  213. package/src/kernels/trellis/gate-up-codebook-mma.ts +141 -0
  214. package/src/kernels/trellis/gate-up-codebook.ts +137 -0
  215. package/src/kernels/trellis/gate-up-factored.ts +197 -0
  216. package/src/kernels/trellis/gate-up-mma-wide.ts +147 -0
  217. package/src/kernels/trellis/gate-up-mma.ts +148 -0
  218. package/src/kernels/trellis/gate-up.ts +248 -0
  219. package/src/kernels/trellis/geometry.ts +22 -0
  220. package/src/kernels/trellis/index.ts +103 -0
  221. package/src/kernels/trellis/launch.ts +12 -0
  222. package/src/kernels/trellis/mixed-gate-up-factored.ts +119 -0
  223. package/src/kernels/trellis/mixed-gate-up.ts +146 -0
  224. package/src/kernels/trellis/qualified.ts +34 -0
  225. package/src/kernels/trellis/reduce.ts +150 -0
  226. package/src/kernels/trellis/scatter.ts +407 -0
  227. package/src/kernels/trellis/splitk-prefill.ts +152 -0
  228. package/src/kernels/trellis/tiled-prefill.ts +131 -0
  229. package/src/kernels/trellis/vector-expand.ts +58 -0
  230. package/src/kernels/trellis/wide-prefill.ts +89 -0
  231. package/src/kernels/turboquant/decode.ts +82 -0
  232. package/src/kernels/turboquant/index.ts +5 -0
  233. package/src/kernels/turboquant/inverse.ts +122 -0
  234. package/src/kernels/turboquant/ops.ts +860 -0
  235. package/src/kernels/turboquant/signs.ts +130 -0
  236. package/src/kernels/turboquant/tables.ts +124 -0
  237. package/src/layers/affine-verify-linear.ts +75 -0
  238. package/src/layers/ane-prefill-split.ts +283 -0
  239. package/src/layers/compiled-normalization.ts +41 -0
  240. package/src/layers/delta-heads.ts +23 -0
  241. package/src/layers/dense-embedding.ts +29 -0
  242. package/src/layers/dense-linear.ts +32 -0
  243. package/src/layers/dequantized-linear.ts +72 -0
  244. package/src/layers/folded-attention.ts +42 -0
  245. package/src/layers/geglu.ts +130 -0
  246. package/src/layers/helpers.ts +7 -0
  247. package/src/layers/index.ts +36 -0
  248. package/src/layers/kv4-decode-attention.ts +15 -0
  249. package/src/layers/layer-norm.ts +24 -0
  250. package/src/layers/loading.ts +92 -0
  251. package/src/layers/lora.ts +150 -0
  252. package/src/layers/normalization.ts +9 -0
  253. package/src/layers/prefix-layout.ts +33 -0
  254. package/src/layers/quantized-append-attention.ts +32 -0
  255. package/src/layers/quantized-attention.ts +353 -0
  256. package/src/layers/quantized-embedding.ts +39 -0
  257. package/src/layers/quantized-linear.ts +137 -0
  258. package/src/layers/quantized-switch-linear.ts +37 -0
  259. package/src/layers/qwen-append.ts +12 -0
  260. package/src/layers/qwen-mrope.ts +238 -0
  261. package/src/layers/rope-2d.ts +110 -0
  262. package/src/layers/rope.ts +389 -0
  263. package/src/layers/swiglu.ts +23 -0
  264. package/src/layers/trellis-codebook-down.ts +52 -0
  265. package/src/layers/trellis-codebook-gate-up.ts +42 -0
  266. package/src/layers/trellis-down.ts +117 -0
  267. package/src/layers/trellis-gate-up.ts +71 -0
  268. package/src/layers/trellis-linear.ts +211 -0
  269. package/src/layers/trellis-prompt.ts +102 -0
  270. package/src/layers/trellis-reduce-gate-up.ts +73 -0
  271. package/src/layers/trellis-scatter-down.ts +98 -0
  272. package/src/models/ane-install.ts +74 -0
  273. package/src/models/audio/conformer.ts +656 -0
  274. package/src/models/cache-composition.ts +44 -0
  275. package/src/models/capabilities.ts +89 -0
  276. package/src/models/chat-template.ts +10 -0
  277. package/src/models/composition.ts +107 -0
  278. package/src/models/diffusion-gemma/experts.ts +59 -0
  279. package/src/models/diffusion-gemma/family.ts +21 -0
  280. package/src/models/diffusion-gemma/model.ts +900 -0
  281. package/src/models/diffusion-gemma/vision.ts +549 -0
  282. package/src/models/drafter-loaders.ts +49 -0
  283. package/src/models/drafters.ts +39 -0
  284. package/src/models/factory.ts +351 -0
  285. package/src/models/families.ts +70 -0
  286. package/src/models/family.ts +125 -0
  287. package/src/models/gemma4/assistant-target.ts +46 -0
  288. package/src/models/gemma4/assistant.ts +327 -0
  289. package/src/models/gemma4/blocks.ts +755 -0
  290. package/src/models/gemma4/compiled-decode.ts +452 -0
  291. package/src/models/gemma4/family.ts +39 -0
  292. package/src/models/gemma4/gemma4-12b.ts +677 -0
  293. package/src/models/gemma4/gemma4-26b.ts +544 -0
  294. package/src/models/gemma4/gemma4-e4b.ts +599 -0
  295. package/src/models/gemma4/graphs.ts +147 -0
  296. package/src/models/gemma4/media.ts +88 -0
  297. package/src/models/gemma4/model.ts +860 -0
  298. package/src/models/gemma4/serving-blocks.ts +72 -0
  299. package/src/models/gemma4/trainable.ts +186 -0
  300. package/src/models/gemma4/written-blocks.ts +232 -0
  301. package/src/models/gemma4/written-forward.ts +159 -0
  302. package/src/models/gemma4/written-input.ts +29 -0
  303. package/src/models/glm52/atlas.ts +507 -0
  304. package/src/models/glm52/coupling.ts +388 -0
  305. package/src/models/glm52/dsa.ts +601 -0
  306. package/src/models/glm52/family.ts +20 -0
  307. package/src/models/glm52/mla.ts +812 -0
  308. package/src/models/glm52/model.ts +1127 -0
  309. package/src/models/glm52/moe.ts +223 -0
  310. package/src/models/glm52/mtp.ts +60 -0
  311. package/src/models/glm52/pilot.ts +537 -0
  312. package/src/models/glm52/reference.ts +445 -0
  313. package/src/models/glm52/residency.ts +557 -0
  314. package/src/models/glm52/streamed-experts.ts +447 -0
  315. package/src/models/graph.ts +59 -0
  316. package/src/models/implementation.ts +230 -0
  317. package/src/models/index.ts +15 -0
  318. package/src/models/media-input.ts +15 -0
  319. package/src/models/memory-plan.ts +39 -0
  320. package/src/models/minicpm5/family.ts +29 -0
  321. package/src/models/minicpm5/model.ts +394 -0
  322. package/src/models/minicpm5/trainable.ts +90 -0
  323. package/src/models/profile.ts +337 -0
  324. package/src/models/qwen/blocks.ts +658 -0
  325. package/src/models/qwen/checkpoint.ts +190 -0
  326. package/src/models/qwen/families.ts +57 -0
  327. package/src/models/qwen/media-input.ts +161 -0
  328. package/src/models/qwen/mtp.ts +101 -0
  329. package/src/models/qwen/qwen3-moe.ts +355 -0
  330. package/src/models/qwen/qwen3.ts +323 -0
  331. package/src/models/qwen/qwen38-27b-trellis-m1max.ts +1532 -0
  332. package/src/models/qwen/qwen38-27b-trellis-m4pro.ts +1092 -0
  333. package/src/models/qwen/qwen3_5.ts +433 -0
  334. package/src/models/qwen/written-layers.ts +285 -0
  335. package/src/models/row-table.ts +1 -0
  336. package/src/models/runtime.ts +73 -0
  337. package/src/models/speculative/deepspec-artifact.ts +10 -0
  338. package/src/models/speculative/deepspec.ts +744 -0
  339. package/src/models/speculative/dflash.ts +553 -0
  340. package/src/models/speculative/dflash2-artifact.ts +9 -0
  341. package/src/models/speculative/dflash2-operations.ts +1 -0
  342. package/src/models/speculative/dflash2.ts +576 -0
  343. package/src/models/speculative/loader.ts +29 -0
  344. package/src/models/support.ts +79 -0
  345. package/src/models/universal/archs.ts +365 -0
  346. package/src/models/universal/blocks.ts +479 -0
  347. package/src/models/universal/dense.ts +255 -0
  348. package/src/models/universal/family.ts +17 -0
  349. package/src/models/vision/qwen3vl.ts +566 -0
  350. package/src/models/vision/siglip.ts +520 -0
  351. package/src/models/vision/unified.ts +176 -0
  352. package/src/models/whisper/family.ts +19 -0
  353. package/src/models/whisper/fast.ts +734 -0
  354. package/src/models/whisper/model.ts +430 -0
  355. package/src/runtime/compiled-trace.ts +16 -0
  356. package/src/runtime/config.ts +77 -0
  357. package/src/runtime/expert-trace.ts +71 -0
  358. package/src/runtime/graph-forward.ts +65 -0
  359. package/src/runtime/integers.ts +6 -0
  360. package/src/runtime/native.ts +16 -0
  361. package/src/runtime/resources.ts +53 -0
  362. package/src/runtime/row-operations.ts +42 -0
  363. package/src/runtime/trace.ts +161 -0
  364. package/src/sampling/curve.ts +122 -0
  365. package/src/sampling/draft.ts +55 -0
  366. package/src/sampling/extras.ts +45 -0
  367. package/src/sampling/filters.ts +134 -0
  368. package/src/sampling/grammar.ts +630 -0
  369. package/src/sampling/hlg.ts +290 -0
  370. package/src/sampling/index.ts +9 -0
  371. package/src/sampling/processors.ts +142 -0
  372. package/src/sampling/sampler.ts +120 -0
  373. package/src/sampling/step.ts +343 -0
  374. package/src/sampling/types.ts +161 -0
  375. package/src/sampling/window-filters.ts +46 -0
  376. package/src/scoring/decisions.ts +96 -0
  377. package/src/scoring/full-sequence.ts +103 -0
  378. package/src/scoring/index.ts +9 -0
  379. package/src/scoring/kl.ts +29 -0
  380. package/src/scoring/perplexity.ts +165 -0
  381. package/src/scoring/response-logps.ts +65 -0
  382. package/src/state/affine-attention.ts +180 -0
  383. package/src/state/attachment.ts +68 -0
  384. package/src/state/attention-read.ts +215 -0
  385. package/src/state/batched-kv.ts +294 -0
  386. package/src/state/batched-mask.ts +445 -0
  387. package/src/state/batched-quant.ts +389 -0
  388. package/src/state/batched-quantized-kv.ts +209 -0
  389. package/src/state/batched-rotating-quant.ts +457 -0
  390. package/src/state/batched-rotating-state.ts +149 -0
  391. package/src/state/batched-rotating.ts +430 -0
  392. package/src/state/batched-row-storage.ts +179 -0
  393. package/src/state/batched-ssm.ts +54 -0
  394. package/src/state/batched-turboquant-kv.ts +144 -0
  395. package/src/state/bf16-first-kv.ts +663 -0
  396. package/src/state/cache-service.ts +592 -0
  397. package/src/state/capabilities.ts +65 -0
  398. package/src/state/checkpoint.ts +46 -0
  399. package/src/state/continuation-persistence.ts +126 -0
  400. package/src/state/decode-step-inputs.ts +31 -0
  401. package/src/state/decoded-kv-donor.ts +12 -0
  402. package/src/state/delayed-quantized-kv.ts +141 -0
  403. package/src/state/delayed-rotating-quantized-kv.ts +178 -0
  404. package/src/state/delayed-turboquant-kv.ts +123 -0
  405. package/src/state/durability.ts +214 -0
  406. package/src/state/encoder-cache.ts +26 -0
  407. package/src/state/full-kv-row-append.ts +33 -0
  408. package/src/state/full-kv-row-donor.ts +54 -0
  409. package/src/state/full-prefill-padding.ts +56 -0
  410. package/src/state/full-prefill-row.ts +112 -0
  411. package/src/state/full-transitioning-kv-rows.ts +70 -0
  412. package/src/state/glm52-cache.ts +783 -0
  413. package/src/state/grouped-heads-kv4-cache.ts +26 -0
  414. package/src/state/index.ts +38 -0
  415. package/src/state/kv-attention-view.ts +77 -0
  416. package/src/state/kv-budget.ts +14 -0
  417. package/src/state/kv-scheme.ts +315 -0
  418. package/src/state/kv-storage.ts +57 -0
  419. package/src/state/kv-tensor-rows.ts +212 -0
  420. package/src/state/kv.ts +324 -0
  421. package/src/state/kv4-head256-cache.ts +46 -0
  422. package/src/state/latent-legos.ts +895 -0
  423. package/src/state/layout.ts +277 -0
  424. package/src/state/leases.ts +26 -0
  425. package/src/state/paged/cache.ts +689 -0
  426. package/src/state/paged/codec.ts +36 -0
  427. package/src/state/paged/rows.ts +223 -0
  428. package/src/state/persistence-types.ts +173 -0
  429. package/src/state/persistence-worker.ts +83 -0
  430. package/src/state/persistence.ts +1036 -0
  431. package/src/state/persistence.worker.d.ts +3 -0
  432. package/src/state/persistence.worker.js +264 -0
  433. package/src/state/prefix-cache.ts +685 -0
  434. package/src/state/quantized-kv.ts +369 -0
  435. package/src/state/quantized-tensor.ts +26 -0
  436. package/src/state/request-policy.ts +48 -0
  437. package/src/state/retention-policy.ts +37 -0
  438. package/src/state/rollback.ts +131 -0
  439. package/src/state/rotating-kv-donor.ts +35 -0
  440. package/src/state/rotating-kv-layout.ts +341 -0
  441. package/src/state/rotating-kv.ts +529 -0
  442. package/src/state/rotating-quantized-kv.ts +616 -0
  443. package/src/state/rotating-row-transaction.ts +35 -0
  444. package/src/state/rows.ts +104 -0
  445. package/src/state/softcap-attention.ts +118 -0
  446. package/src/state/speculative/glm52-mtp-state.ts +38 -0
  447. package/src/state/speculative/qwen-mtp-state.ts +34 -0
  448. package/src/state/speculative-rotating-kv.ts +113 -0
  449. package/src/state/ssd-cache.ts +434 -0
  450. package/src/state/ssm-prefill-padding.ts +39 -0
  451. package/src/state/ssm.ts +422 -0
  452. package/src/state/target-layout.ts +7 -0
  453. package/src/state/tiered-prefix-cache.ts +45 -0
  454. package/src/state/training-cache.ts +300 -0
  455. package/src/state/transitioning-kv-rows.ts +267 -0
  456. package/src/state/turboquant-attention.ts +97 -0
  457. package/src/state/turboquant-codec.ts +90 -0
  458. package/src/state/turboquant-kv.ts +442 -0
  459. package/src/state/views.ts +26 -0
  460. package/src/transcription/index.ts +3 -0
  461. package/src/transcription/silero-vad.ts +379 -0
  462. package/src/transcription/whisper/decode.ts +773 -0
  463. package/src/transcription/whisper/format.ts +74 -0
  464. package/src/transcription/whisper/index.ts +4 -0
  465. package/src/transcription/whisper/timing.ts +274 -0
  466. package/src/transcription/whisper/transcribe.ts +394 -0
  467. package/src/transcription/whisper/types.ts +22 -0
@@ -0,0 +1,630 @@
1
+ // Grammar-constrained decoding via @mlc-ai/web-xgrammar (WASM).
2
+ //
3
+ // Serves the structured-output API surface: response_format (json_object /
4
+ // json_schema), guided_grammar (EBNF/LARK), guided_regex, guided_choice,
5
+ // and the structured_outputs alias — full parity with oMLX/vLLM. From our
6
+ // side it's all one shape: compile something → GrammarMatcher → per-step
7
+ // bitmask → mask invalid logits to -inf before the sampler. The only
8
+ // difference between the modes is which xgrammar compile method we call.
9
+ //
10
+ // Fidelity tier: L2-class (oMLX is the oracle — same model+prompt+schema+seed
11
+ // must produce the same token stream). mlx-lm has no grammar support, so this
12
+ // is purely an oMLX-parity feature, not an L1 contract. The bitmask does not
13
+ // change the numerics of VALID tokens (it only sets invalid ones to -inf), so
14
+ // it touches neither L1 nor L2 decode numerics.
15
+ //
16
+ // Integration: a GrammarController is a stateful logits masker. Because
17
+ // xgrammar's getNextTokenBitmask() is async (WASM) and acceptToken() needs
18
+ // the token id as a JS number, the controller does NOT ride the sync
19
+ // LogitsProcessor array (which operates on device-side token arrays with no
20
+ // readback). Instead generate() drives it explicitly: after each token is
21
+ // sampled, call accept(tokenId) (sync matcher advance + fires async mask
22
+ // precompute that overlaps the next GPU forward), then await ready() before
23
+ // the next sampleStep; applyMask() applies the ready bitmask synchronously.
24
+ // See the grammar branch in generate()'s decode loop.
25
+
26
+ import xgrammar, {
27
+ type TokenizerInfo as XTokenizerInfo,
28
+ type GrammarCompiler as XGrammarCompiler,
29
+ type CompiledGrammar as XCompiledGrammar,
30
+ type GrammarMatcher as XGrammarMatcher,
31
+ type Grammar as XGrammar,
32
+ } from "@mlc-ai/web-xgrammar";
33
+ import { readFileSync } from "node:fs";
34
+ import { MlxArray } from "@mlx-bun/mlx/array";
35
+ import * as ops from "@mlx-bun/mlx/ops";
36
+ import { applyTokenBitmask } from "../kernels/sampling/token-bitmask";
37
+ import { runtimeValue } from "../runtime/config";
38
+ import type { LoadedTokenizer } from "../input/tokenizer";
39
+
40
+ /** The request shape — the union of every structured-output field name across
41
+ * the OpenAI/oMLX/vLLM API surfaces. BOTH spellings are accepted: the wire
42
+ * protocol is snake_case (the server passes the raw request body — this was
43
+ * a live bug until 2026-07-03: only camelCase was read, so response_format
44
+ * over HTTP silently no-op'd; caught by the feature-matrix benchmark's
45
+ * conformance gate); library callers use camelCase. */
46
+ export interface GrammarRequest {
47
+ /** OpenAI response_format. {type:"json_object"} | {type:"json_schema",
48
+ * json_schema:{name,schema,strict?}} | {type:"text"} (text = no-op). */
49
+ responseFormat?: unknown;
50
+ response_format?: unknown;
51
+ /** vLLM/oMLX: raw EBNF/LARK grammar string. */
52
+ guidedGrammar?: string;
53
+ guided_grammar?: string;
54
+ /** vLLM/oMLX: regex. Compiled via `root ::= <regex>` EBNF embedding, so
55
+ * only the regex∩EBNF subset works (the F5 gap — real regex→EBNF or
56
+ * structural-tag RegexFormat is the tracked fix, `02d723a:docs/reference/server-api.md`). */
57
+ guidedRegex?: string;
58
+ guided_regex?: string;
59
+ /** vLLM/oMLX: restrict output to one of these strings (enum). */
60
+ guidedChoice?: string[];
61
+ guided_choice?: string[];
62
+ /** vLLM/oMLX: a JSON schema object directly (alias for response_format's
63
+ * json_schema.schema). */
64
+ structuredOutputs?: unknown;
65
+ structured_outputs?: unknown;
66
+ }
67
+
68
+ /** A compiled grammar ready to mask logits. Null when the request asks for no
69
+ * constraint (text, or none of the fields set). `warning` is set when the
70
+ * requested grammar could not be compiled and the caller should degrade to
71
+ * prompt injection (mirrors oMLX's _compile_grammar_for_request returning None
72
+ * + a Warning header). */
73
+ export interface CompiledGrammarResult {
74
+ controller: GrammarController;
75
+ /** Schema/grammar for the degrade-path system-prompt injection (oMLX parity). */
76
+ degradeHint: string | null;
77
+ }
78
+
79
+ /** xgrammar TokenizerInfo is expensive to build (it decodes every vocab id).
80
+ * Cache one per tokenizer.json path — it's vocab-structural, not per-request. */
81
+ /** Module-level serializer for ALL xgrammar WASM calls. The WASM instance is
82
+ * single-threaded; CONCURRENT async calls corrupt emscripten's binding
83
+ * layer (BindingError: Expected null or instance of VectorInt, got an
84
+ * instance of VectorInt — reproduced in `02d723a:tests/parity/grammar.test.ts` B1 test, which
85
+ * fires 4 overlapping compiles + fills). Under `--batch N` the scheduler
86
+ * fires N accept()s (each kicks an async fill) then awaits ready() on all;
87
+ * and the server compiles grammar for concurrent requests. Without
88
+ * serialization the overlapping WASM calls crash. This queue keeps the
89
+ * async-overlap API (fills still overlap GPU compute — the queue only
90
+ * serializes the CPU-side WASM calls, each 0.004–0.19 ms) while ensuring
91
+ * only ONE xgrammar call is in flight at a time. Both the compile path
92
+ * (compileGrammarRequest) and the fill path (prime/accept) ride it. */
93
+ let wasmChain: Promise<unknown> = Promise.resolve();
94
+ function wasmQueue<T>(fn: () => Promise<T>): Promise<T> {
95
+ const run = wasmChain.then(fn, fn); // run regardless of prior success/failure
96
+ wasmChain = run.then(
97
+ () => undefined,
98
+ () => undefined,
99
+ );
100
+ return run;
101
+ }
102
+
103
+ /** F4: ONE TokenizerInfo + ONE compiler per tokenizer.json path, cached for
104
+ * the process lifetime. xgrammar's GrammarCompiler carries an internal
105
+ * compilation cache (cacheEnabled defaults true), so agentic clients
106
+ * replaying the same JSON schema hit a cached grammar instead of paying a
107
+ * full schema→grammar compile per request. Both are vocab-structural.
108
+ * Controllers get ownsCompiler=false and never dispose it.
109
+ * The entry holds a PROMISE, installed synchronously before any await —
110
+ * concurrent compiles after a path change share ONE build (single-flight)
111
+ * instead of racing to rebuild and double-dispose the previous pair. */
112
+ let cachedTokenizerInfo: {
113
+ path: string;
114
+ pair: Promise<{ info: XTokenizerInfo; compiler: XGrammarCompiler }>;
115
+ } | null = null;
116
+
117
+ /** Extract the ordered vocab array + detect xgrammar vocab type from
118
+ * tokenizer.json (the same data packages/inference/src/input/tokenizer.ts already loads).
119
+ * - decoder.type == "ByteLevel" → byte_level (Llama3, Qwen)
120
+ * - decoder is Sequence w/ ByteFallback → byte_fallback (Gemma/SP)
121
+ * - otherwise → raw
122
+ * prependSpace is true for byte_level (ByteLevel pretokenizers add a leading
123
+ * space). Validated in xgrammar-spike.ts (deleted 2026-08-23; git history) across Gemma
124
+ * (byte_fallback, 256k), Llama-3 / Qwen2.5 (byte_level, 128k/151k). */
125
+ function loadVocab(tokenizerJsonPath: string): {
126
+ vocab: string[];
127
+ vocabType: string;
128
+ prependSpace: boolean;
129
+ } {
130
+ const t = JSON.parse(readFileSync(tokenizerJsonPath, "utf8"));
131
+ const v = t.model?.vocab;
132
+ if (!v || typeof v !== "object")
133
+ throw new Error("tokenizer.json has no model.vocab");
134
+ const entries = Object.entries(v as Record<string, number>);
135
+ const size = entries.length;
136
+ const vocab = new Array<string>(size);
137
+ for (const [tok, id] of entries) vocab[id] = tok;
138
+
139
+ let vocabType = "raw";
140
+ const dec = t.decoder;
141
+ if (dec?.type === "ByteLevel") vocabType = "byte_level";
142
+ else if (dec?.type === "Sequence" && Array.isArray(dec.decoders)) {
143
+ if (dec.decoders.some((d: { type?: string }) => d?.type === "ByteFallback"))
144
+ vocabType = "byte_fallback";
145
+ } else if (dec?.type === "ByteFallback") {
146
+ vocabType = "byte_fallback";
147
+ }
148
+ return { vocab, vocabType, prependSpace: vocabType === "byte_level" };
149
+ }
150
+
151
+ /** Get (cached) xgrammar TokenizerInfo + its process-lifetime compiler for a
152
+ * tokenizer.json path. On a path change (model switch) the previous pair is
153
+ * disposed exactly once, by the single flight that replaced it. */
154
+ function getTokenizerInfo(
155
+ tokenizerJsonPath: string,
156
+ ): Promise<{ info: XTokenizerInfo; compiler: XGrammarCompiler }> {
157
+ if (cachedTokenizerInfo?.path === tokenizerJsonPath) return cachedTokenizerInfo.pair;
158
+ const prev = cachedTokenizerInfo;
159
+ const pair = (async () => {
160
+ const { vocab, vocabType, prependSpace } = loadVocab(tokenizerJsonPath);
161
+ const info = await wasmQueue(() =>
162
+ xgrammar.TokenizerInfo.createTokenizerInfo(vocab, vocabType, prependSpace, vocab.length),
163
+ );
164
+ const compiler = await wasmQueue(() => xgrammar.GrammarCompiler.createGrammarCompiler(info));
165
+ if (prev)
166
+ prev.pair.then(
167
+ (p) => {
168
+ p.compiler.dispose();
169
+ p.info.dispose();
170
+ },
171
+ () => undefined,
172
+ );
173
+ return { info, compiler };
174
+ })();
175
+ // Install synchronously — concurrent callers join this flight. A failed
176
+ // flight uncaches itself (callers still see the rejection) so a transient
177
+ // error doesn't poison the path forever.
178
+ cachedTokenizerInfo = { path: tokenizerJsonPath, pair };
179
+ pair.catch(() => {
180
+ if (cachedTokenizerInfo?.pair === pair) cachedTokenizerInfo = null;
181
+ });
182
+ return pair;
183
+ }
184
+
185
+ /** The model's logit width: config.vocab_size, which may exceed the ids the
186
+ * matcher knows (embedding padding, added tokens outside model.vocab).
187
+ * Falls back to the tokenizer length. */
188
+ function logitWidth(tokenizer: LoadedTokenizer, configVocabSize?: number): number {
189
+ return configVocabSize ?? tokenizer.vocabSize ?? 0;
190
+ }
191
+
192
+ /** Keep XGrammar's verdict for every id its matcher knows and reject every
193
+ * other id. The matcher's bitmask has ceil(known / 32) words, and XGrammar can
194
+ * set the unused high bits of the final word when `known` is not a multiple
195
+ * of 32: a 4-id vocabulary with guided_choice yes/no allows ids 4-31, and
196
+ * MiniCPM5's 130072 model.vocab ids allow added tokens 130072-130079. Ids
197
+ * past the matcher also include config padding and added tokens it was not
198
+ * built with. The result spans the logit width with those bits clear; both
199
+ * mask paths also reject ids past the words they are given. */
200
+ function grammarOwnedBits(bits: Int32Array, known: number, logits: number): Int32Array {
201
+ const owned = new Int32Array(Math.ceil(logits / 32));
202
+ const whole = known >>> 5, tail = known & 31;
203
+ owned.set(bits.subarray(0, Math.min(whole, owned.length)));
204
+ if (tail !== 0 && whole < owned.length) owned[whole] = (bits[whole] ?? 0) & ~(-1 << tail);
205
+ return owned;
206
+ }
207
+
208
+ /** Stateful grammar masker. Driven explicitly by generate()'s decode loop
209
+ * (see module doc): accept(tokenId) advances the matcher + fires async mask
210
+ * precompute; ready() awaits it; applyMask(logits) sets invalid ids to -inf. */
211
+ export class GrammarController {
212
+ private readonly matcher: XGrammarMatcher;
213
+ private readonly compiled: XCompiledGrammar;
214
+ private readonly compiler: XGrammarCompiler;
215
+ private readonly ownsCompiler: boolean;
216
+ /** `matcher`: ids XGrammar's TokenizerInfo was built with, the only ids the
217
+ * grammar decides. `logits`: the model's logit width. */
218
+ private readonly vocab: { readonly matcher: number; readonly logits: number };
219
+ /** Raw-encode hook for jump-forward (encode WITHOUT special tokens); null
220
+ * disables jumpForward(). */
221
+ private readonly encodeRaw: ((text: string) => number[]) | null;
222
+ /** Grammar-owned bits (grammarOwnedBits), read by both mask paths. */
223
+ private readyMask: Int32Array;
224
+ private pending: Promise<void> | null;
225
+ private terminated = false;
226
+ private disposed = false;
227
+ private readonly metalMask = runtimeValue("MLX_BUN_TOKEN_MASK") === "metal";
228
+ /** Tokens emitted via jumpForward() this generation — telemetry/test hook. */
229
+ jumpedTokens = 0;
230
+
231
+ constructor(
232
+ matcher: XGrammarMatcher,
233
+ compiled: XCompiledGrammar,
234
+ compiler: XGrammarCompiler,
235
+ ownsCompiler: boolean,
236
+ vocab: { readonly matcher: number; readonly logits: number },
237
+ encodeRaw?: (text: string) => number[],
238
+ ) {
239
+ this.matcher = matcher;
240
+ this.compiled = compiled;
241
+ this.compiler = compiler;
242
+ this.ownsCompiler = ownsCompiler;
243
+ this.vocab = vocab;
244
+ this.encodeRaw = encodeRaw ?? null;
245
+ const all = new Int32Array(Math.ceil(vocab.matcher / 32)).fill(-1);
246
+ this.readyMask = grammarOwnedBits(all, vocab.matcher, vocab.logits);
247
+ this.pending = null;
248
+ }
249
+
250
+ /** Normalize each XGrammar mask once, before either mask path reads it. */
251
+ private setReadyMask(bits: Int32Array): void {
252
+ this.readyMask = grammarOwnedBits(bits, this.vocab.matcher, this.vocab.logits);
253
+ }
254
+
255
+ /** Precompute the step-0 mask. Must be awaited before the first applyMask(). */
256
+ async prime(): Promise<void> {
257
+ this.setReadyMask(await wasmQueue(() => this.matcher.getNextTokenBitmask()));
258
+ }
259
+
260
+ /** Apply the ready bitmask to logits [1, V]. Invalid token ids → -inf.
261
+ * Sync — the mask is already materialized. Returns a NEW array (caller
262
+ * disposes the input). Mirrors oMLX's apply_token_bitmask_mlx. Both paths
263
+ * read the grammar-owned readyMask, so ids the matcher does not know are
264
+ * never valid. */
265
+ applyMask(logits: MlxArray): MlxArray {
266
+ if (this.metalMask) return applyTokenBitmask(logits, this.readyMask);
267
+ // Build a -inf/0 additive float mask on device from the int32 bitmask.
268
+ // Each int32 bit b (little-endian within the word) gates token
269
+ // word*32 + b. Set = valid (xgrammar convention). Adding -inf → -inf,
270
+ // adding 0 → noop.
271
+ const V = logits.shape[logits.shape.length - 1]!;
272
+ const maskArr = new Float32Array(V);
273
+ for (let id = 0; id < V; id++) {
274
+ const word = id >>> 5;
275
+ const bit = id & 31;
276
+ // F3: readyMask already clears ids the matcher does not know, up to
277
+ // the configured logit width. Logits wider than that are never valid
278
+ // either; mask them explicitly rather than relying on undefined>>>bit
279
+ // coercing to 0.
280
+ const w = word < this.readyMask.length ? this.readyMask[word]! : 0;
281
+ const valid = (w >>> bit) & 1;
282
+ maskArr[id] = valid ? 0 : -Infinity;
283
+ }
284
+ const raw = MlxArray.fromFloat32(maskArr, [1, V]);
285
+ const mask = raw.astype(logits.dtype);
286
+ raw.dispose();
287
+ const out = ops.add(logits, mask);
288
+ mask.dispose();
289
+ return out;
290
+ }
291
+
292
+ /** Advance the matcher by the just-sampled token and fire async mask
293
+ * precompute for the NEXT step (overlaps the GPU forward). Call AFTER the
294
+ * token is read back to JS. No-op once terminated. */
295
+ accept(tokenId: number): void {
296
+ if (this.terminated) return;
297
+ const ok = this.matcher.acceptToken(tokenId);
298
+ if (!ok) {
299
+ // The sampler only ever picks valid tokens (we masked the rest), so an
300
+ // accept failure means the vocab/mask drifted from the model's actual
301
+ // tokenization — not a logic bug in the walk. Log + keep going; the
302
+ // output may be malformed but generation continues rather than dying.
303
+ console.warn(`grammar: matcher rejected token ${tokenId}`);
304
+ }
305
+ if (this.matcher.isTerminated()) {
306
+ this.terminated = true;
307
+ return;
308
+ }
309
+ this.pending = this.fireFill();
310
+ }
311
+
312
+ /** Queue the async bitmask fill. The closure re-checks `disposed` at RUN
313
+ * time: a controller disposed while its fill is still queued (an exception
314
+ * between accept/jumpForward and ready()) must not call into the deleted
315
+ * WASM matcher — that BindingError poisons the module-wide wasmChain for
316
+ * every later grammar request (found by `02d723a:tests/parity/grammar-jump.test.ts`). */
317
+ private fireFill(): Promise<void> {
318
+ return wasmQueue(() =>
319
+ this.disposed
320
+ ? Promise.resolve(null)
321
+ : this.matcher.getNextTokenBitmask(),
322
+ ).then((m) => {
323
+ if (m) this.setReadyMask(m);
324
+ });
325
+ }
326
+
327
+ /** Await the mask precompute fired by accept(). Should be called before the
328
+ * next applyMask(); usually resolves immediately (overlapped the forward). */
329
+ async ready(): Promise<void> {
330
+ if (this.pending) {
331
+ await this.pending;
332
+ this.pending = null;
333
+ }
334
+ }
335
+
336
+ get isTerminated(): boolean {
337
+ return this.terminated;
338
+ }
339
+
340
+ /** Propose the forced string without advancing the matcher. Tokenization
341
+ * can differ at its first boundary; verification and the normal grammar
342
+ * sampler accept or reject the candidates before anything is committed. */
343
+ async proposeTokens(maxTokens: number): Promise<number[]> {
344
+ await this.ready();
345
+ if (this.terminated || !this.encodeRaw) return [];
346
+ return wasmQueue(async () => {
347
+ if (this.disposed) return [];
348
+ const text = this.matcher.findJumpForwardString();
349
+ return text ? this.encodeRaw!(text).slice(0, maxTokens) : [];
350
+ });
351
+ }
352
+
353
+ /** Jump-forward decoding (SGLang's technique via xgrammar's
354
+ * findJumpForwardString; opt-in — the composition's grammarJump): when the grammar
355
+ * admits exactly one continuation string (JSON keys/punctuation, enum
356
+ * bodies — longest with `any_whitespace:false` compact schemas), return its
357
+ * token ids so the decode loop can carry them into the KV with ONE
358
+ * multi-token forward instead of one masked forward per token.
359
+ *
360
+ * Contract: MUTATES the matcher for every id it returns (and only those) —
361
+ * the caller MUST emit exactly the returned ids, then await ready() before
362
+ * the next applyMask. Call only after ready() has resolved (no WASM fill in
363
+ * flight — the matcher calls here are sync). Returns null (matcher
364
+ * untouched) when: no hook, terminated, the jump retokenizes to <2 ids
365
+ * (a 1-token jump saves no forward), maxIds < 2, or the first id is
366
+ * rejected. If a LATER id is rejected the accepted prefix is returned —
367
+ * those ids already advanced the matcher, so they must be emitted; the
368
+ * loop resumes masked decoding after them (no rollback needed by
369
+ * construction).
370
+ *
371
+ * Fidelity note (why opt-in, no oracle): encode(jumpStr) standalone can
372
+ * tokenize the forced span differently than the model would have sampled
373
+ * it. The emitted STRING is identical (token strings concatenate to the
374
+ * jump string) and grammar validity is preserved (every id is
375
+ * matcher-accepted), but the token STREAM — and, conditioning on it,
376
+ * content generated AFTER the forced span — may differ from an unjumped
377
+ * run. SentencePiece-family tokenizers whose raw encode prepends a space
378
+ * simply never jump (the first id gets rejected) — the guard degrades to
379
+ * normal decoding, never to invalid output. */
380
+ jumpForward(maxIds: number): number[] | null {
381
+ if (this.terminated || !this.encodeRaw || maxIds < 2) return null;
382
+ const str = this.matcher.findJumpForwardString();
383
+ if (!str || str.length < 2) return null;
384
+ const ids = this.encodeRaw(str);
385
+ if (ids.length < 2) return null;
386
+ const take = Math.min(ids.length, maxIds);
387
+ if (take < 2) return null;
388
+ const accepted: number[] = [];
389
+ for (let i = 0; i < take; i++) {
390
+ const id = ids[i]!;
391
+ if (!this.matcher.acceptToken(id)) break; // keep the accepted prefix
392
+ accepted.push(id);
393
+ if (this.matcher.isTerminated()) {
394
+ this.terminated = true;
395
+ break;
396
+ }
397
+ }
398
+ if (accepted.length === 0) return null; // matcher untouched
399
+ this.jumpedTokens += accepted.length;
400
+ if (!this.terminated) this.pending = this.fireFill();
401
+ return accepted;
402
+ }
403
+
404
+ dispose(): void {
405
+ // Idempotent: generate()/the gateway dispose in finally, and the server's
406
+ // early-reject paths dispose pre-run — a throw inside run can reach both.
407
+ if (this.disposed) return;
408
+ this.disposed = true; // queued fills become no-ops (see fireFill)
409
+ this.matcher.dispose();
410
+ this.compiled.dispose();
411
+ if (this.ownsCompiler) this.compiler.dispose();
412
+ }
413
+ }
414
+
415
+ /** Normalize the request fields + compile. Returns null when no constraint is
416
+ * requested (text / none set). On compile failure returns
417
+ * `{ controller: null, degradeHint }` — callers inject a "respond in valid
418
+ * JSON" system prompt + emit a Warning header (oMLX parity, never 500). */
419
+ export async function compileGrammarRequest(
420
+ req: GrammarRequest,
421
+ tokenizer: LoadedTokenizer,
422
+ configVocabSize?: number,
423
+ ): Promise<{ controller: GrammarController | null; degradeHint: string | null } | null> {
424
+ const tokenizerJsonPath = tokenizer.tokenizerJsonPath;
425
+ if (!tokenizerJsonPath)
426
+ throw new Error("grammar: tokenizer.tokenizerJsonPath is required (set by loadTokenizer)");
427
+
428
+ const resolved = resolveGrammarRequest(req);
429
+ if (!resolved) return null;
430
+
431
+ // Each xgrammar WASM call rides wasmQueue individually (NOT a body-level
432
+ // wrap — prime() calls wasmQueue internally, so a body-level wrap would
433
+ // deadlock reentrantly). Individual calls serialize without nesting.
434
+ const { info, compiler } = await getTokenizerInfo(tokenizerJsonPath);
435
+ let compiled: XCompiledGrammar | null = null;
436
+ let degradeHint: string | null = null;
437
+
438
+ try {
439
+ switch (resolved.kind) {
440
+ case "json_object":
441
+ compiled = await wasmQueue(() => compiler.compileBuiltinJSONGrammar());
442
+ break;
443
+ case "json_schema": {
444
+ const s = resolved.schema;
445
+ compiled = await wasmQueue(() =>
446
+ compiler.compileJSONSchema(
447
+ typeof s === "string" ? s : JSON.stringify(s),
448
+ resolved.anyWhitespace ?? true,
449
+ resolved.indent,
450
+ resolved.separators,
451
+ resolved.strict ?? true,
452
+ ),
453
+ );
454
+ break;
455
+ }
456
+ case "grammar": {
457
+ const g = await wasmQueue(() => xgrammar.Grammar.fromEBNF(resolved.ebnf, resolved.rootRule));
458
+ compiled = await wasmQueue(() => compiler.compileGrammar(g));
459
+ g.dispose();
460
+ break;
461
+ }
462
+ case "regex": {
463
+ const ebnf = `root ::= ${resolved.regex}`;
464
+ const g = await wasmQueue(() => xgrammar.Grammar.fromEBNF(ebnf));
465
+ compiled = await wasmQueue(() => compiler.compileGrammar(g));
466
+ g.dispose();
467
+ break;
468
+ }
469
+ case "choice": {
470
+ // F6: escape everything an EBNF string literal can't hold bare.
471
+ // \n\r\t have named escapes; any other control char has no EBNF
472
+ // spelling — throw into the degrade path (prompt injection +
473
+ // Warning header), never a 500.
474
+ const esc = (c: string) => {
475
+ const body = c
476
+ .replace(/\\/g, "\\\\")
477
+ .replace(/"/g, '\\"')
478
+ .replace(/\n/g, "\\n")
479
+ .replace(/\r/g, "\\r")
480
+ .replace(/\t/g, "\\t");
481
+ // eslint-disable-next-line no-control-regex
482
+ if (/[\x00-\x1f]/.test(body))
483
+ throw new Error("guided_choice: unsupported control character in choice");
484
+ return `"${body}"`;
485
+ };
486
+ const opts = resolved.choices.map(esc).join(" | ");
487
+ const g = await wasmQueue(() => xgrammar.Grammar.fromEBNF(`root ::= ${opts}`));
488
+ compiled = await wasmQueue(() => compiler.compileGrammar(g));
489
+ g.dispose();
490
+ break;
491
+ }
492
+ }
493
+ } catch (e) {
494
+ degradeHint = resolved.degradeDescription ?? String(e);
495
+ compiled = null;
496
+ }
497
+
498
+ if (!compiled) {
499
+ if (!degradeHint) degradeHint = resolved.degradeDescription ?? "grammar compile failed";
500
+ // The compiler is the cached per-TokenizerInfo instance (F4) — a failed
501
+ // compile must NOT dispose it; the WASM abort is catchable and the
502
+ // compiler state survives (verified in `02d723a:tests/parity/grammar.test.ts`).
503
+ // Return the hint (NOT bare null): it drives the degrade path's
504
+ // system-prompt injection + Warning header. Dropping it here served
505
+ // unconstrained output with no Warning at all (found 2026-07-07 sweep).
506
+ return { controller: null, degradeHint };
507
+ }
508
+
509
+ // terminateWithoutStopToken=true: the matcher terminates as soon as the
510
+ // grammar is SATISFIED (e.g. the closing `}` of a complete JSON), not only
511
+ // when the model emits a stop token. This is what oMLX does — generation
512
+ // halts once the structured output is complete, so the sampler never faces
513
+ // an all-rejected (all--inf) mask after the grammar closes (which would
514
+ // make greedy argmax return a garbage token id like 0 that the matcher
515
+ // then rejects, looping until max_tokens).
516
+ const matcher = await wasmQueue(() =>
517
+ xgrammar.GrammarMatcher.createGrammarMatcher(compiled!, undefined, true),
518
+ );
519
+ // The matcher decides only the ids its TokenizerInfo was built with
520
+ // (model.vocab); the logits can be wider.
521
+ const known = info.getVocabSize();
522
+ const controller = new GrammarController(
523
+ matcher, compiled, compiler, false,
524
+ { matcher: known, logits: logitWidth(tokenizer, configVocabSize) || known },
525
+ // Raw encode (no BOS/specials) for jump-forward retokenization.
526
+ (s) => tokenizer.encode(s, false),
527
+ );
528
+ await controller.prime();
529
+ return { controller, degradeHint };
530
+ }
531
+
532
+ /** Internal: normalize the union of request fields into one compile target. */
533
+ function resolveGrammarRequest(req: GrammarRequest):
534
+ | (
535
+ | { kind: "json_object"; degradeDescription: string }
536
+ | {
537
+ kind: "json_schema";
538
+ schema: unknown;
539
+ strict?: boolean;
540
+ anyWhitespace?: boolean;
541
+ indent?: number;
542
+ separators?: [string, string];
543
+ degradeDescription: string;
544
+ }
545
+ | { kind: "grammar"; ebnf: string; rootRule?: string; degradeDescription: string }
546
+ | { kind: "regex"; regex: string; degradeDescription: string }
547
+ | { kind: "choice"; choices: string[]; degradeDescription: string }
548
+ )
549
+ | null {
550
+ // Normalize the two spellings first (wire = snake_case, library = camel).
551
+ const guidedGrammar = req.guidedGrammar ?? req.guided_grammar;
552
+ const guidedRegex = req.guidedRegex ?? req.guided_regex;
553
+ const guidedChoice = req.guidedChoice ?? req.guided_choice;
554
+ const structuredOutputs = req.structuredOutputs ?? req.structured_outputs;
555
+ const responseFormat = req.responseFormat ?? req.response_format;
556
+
557
+ // Precedence mirrors oMLX's _effective_guided_grammar / _compile_grammar:
558
+ // explicit grammar > response_format json_schema > json_object > regex >
559
+ // choice > structured_outputs. text / unset → null (no constraint).
560
+ if (guidedGrammar) {
561
+ return {
562
+ kind: "grammar",
563
+ ebnf: guidedGrammar,
564
+ degradeDescription: "guided_grammar",
565
+ };
566
+ }
567
+
568
+ const rf = responseFormat;
569
+ if (rf && typeof rf === "object") {
570
+ const type = (rf as { type?: string }).type;
571
+ if (type === "json_schema") {
572
+ const js = (rf as {
573
+ json_schema?: {
574
+ schema?: unknown;
575
+ strict?: boolean;
576
+ any_whitespace?: boolean;
577
+ anyWhitespace?: boolean;
578
+ };
579
+ }).json_schema;
580
+ const schema = js?.schema ?? structuredOutputs;
581
+ if (schema) {
582
+ return {
583
+ kind: "json_schema",
584
+ schema,
585
+ strict: js?.strict,
586
+ // Whitespace-stall escape hatch (`02d723a:docs/reference/server-api.md` known gaps):
587
+ // any_whitespace=false compiles the schema with xgrammar's compact
588
+ // separators — no whitespace choice points, so a whitespace-
589
+ // degenerate base model can't greedily tab-loop to max_tokens.
590
+ // Default stays true (oMLX parity: model-chosen formatting).
591
+ anyWhitespace: js?.any_whitespace ?? js?.anyWhitespace,
592
+ degradeDescription: "response_format json_schema",
593
+ };
594
+ }
595
+ } else if (type === "json_object") {
596
+ return { kind: "json_object", degradeDescription: "response_format json_object" };
597
+ }
598
+ // type === "text" → no constraint (OpenAI: text = free-form)
599
+ }
600
+
601
+ if (structuredOutputs && typeof structuredOutputs === "object") {
602
+ // structured_outputs as a bare schema object (oMLX alias)
603
+ return {
604
+ kind: "json_schema",
605
+ schema: structuredOutputs,
606
+ degradeDescription: "structured_outputs",
607
+ };
608
+ }
609
+
610
+ if (guidedRegex) {
611
+ return { kind: "regex", regex: guidedRegex, degradeDescription: "guided_regex" };
612
+ }
613
+
614
+ if (guidedChoice && guidedChoice.length > 0) {
615
+ return {
616
+ kind: "choice",
617
+ choices: guidedChoice,
618
+ degradeDescription: "guided_choice",
619
+ };
620
+ }
621
+
622
+ return null;
623
+ }
624
+
625
+ /** Kill switch: MLX_BUN_GRAMMAR=0 disables grammar compilation entirely
626
+ * (requests degrade to prompt injection). Matches the project's envelope-gated
627
+ * kill-switch discipline. */
628
+ export function grammarEnabled(): boolean {
629
+ return runtimeValue("MLX_BUN_GRAMMAR") !== "0";
630
+ }