@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,3 @@
1
+ export * from "./preprocess";
2
+ export * from "./prompt";
3
+ export * from "./video-frames";
@@ -0,0 +1,247 @@
1
+ // Gemma-4 unified image preprocessing — port of optiq's
2
+ // vlm/gemma4_unified/image_processing.py (vendored from mlx-vlm, BSD-3).
3
+ //
4
+ // decode → aspect-ratio-preserving resize (PIL-style convolution
5
+ // resampling, bicubic kernel with antialias-scaled support) → rescale
6
+ // 1/255 → patchify into 48×48 patch vectors with 2D grid positions →
7
+ // pad to the 280 soft-token budget.
8
+ //
9
+ // Decode strategy (Bun ≥1.4.0): PNG goes straight through fast-png
10
+ // (exact, lossless — the parity-golden path). Everything else (JPEG,
11
+ // HEIC, AVIF, WebP, TIFF, GIF, BMP) is transcoded to lossless PNG by
12
+ // Bun.Image — native OS codecs (ImageIO on macOS), EXIF auto-orient,
13
+ // off-thread — then decoded by fast-png. Bun.Image has no raw-pixel
14
+ // terminal, so the PNG bridge is the supported path to pixels.
15
+ //
16
+ // Resize note: Bun.Image.resize is NOT used — its kernels (lanczos3,
17
+ // cubic≠PIL-bicubic) don't match PIL's antialiased bicubic, which the
18
+ // vision tower was trained behind. PIL's exact float pipeline isn't
19
+ // bit-reproducible here either, so vision logits can differ slightly
20
+ // from the python stack on images that need resizing. Images already
21
+ // sized to multiples of 48 (≤2520 patches) skip the resize entirely and
22
+ // are bit-identical through preprocessing.
23
+
24
+ import { decode as decodePng } from "fast-png";
25
+
26
+ export const PATCH_SIZE = 16;
27
+ export const POOLING_KERNEL_SIZE = 3;
28
+ export const MODEL_PATCH_SIZE = 48; // patch_size * pooling_kernel_size
29
+ export const NUM_SOFT_TOKENS = 280;
30
+
31
+ export interface RGBImage {
32
+ width: number;
33
+ height: number;
34
+ /** Row-major RGB, 3 bytes/px. */
35
+ data: Uint8Array;
36
+ }
37
+
38
+ export interface PreprocessedImage {
39
+ /** [numSoftTokens, 6912] f32 patch vectors (padded). */
40
+ patches: Float32Array;
41
+ /** [numSoftTokens, 2] (x, y) grid positions; -1 padding. */
42
+ positions: Int32Array;
43
+ /** Real (unpadded) soft-token count. */
44
+ softTokens: number;
45
+ }
46
+
47
+ function pngToRgb(bytes: Uint8Array): RGBImage {
48
+ const png = decodePng(bytes);
49
+ const ch = png.channels;
50
+ const out = new Uint8Array(png.width * png.height * 3);
51
+ const src = png.data as Uint8Array;
52
+ const n = png.width * png.height;
53
+ if (png.depth === 16) {
54
+ const src16 = png.data as Uint16Array;
55
+ for (let i = 0; i < n; i++)
56
+ for (let c = 0; c < 3; c++)
57
+ out[i * 3 + c] = src16[i * ch + Math.min(c, ch - 1)]! >> 8;
58
+ } else {
59
+ for (let i = 0; i < n; i++)
60
+ for (let c = 0; c < 3; c++)
61
+ out[i * 3 + c] = src[i * ch + Math.min(c, ch - 1)]!;
62
+ }
63
+ return { width: png.width, height: png.height, data: out };
64
+ }
65
+
66
+ export async function decodeImage(bytes: Uint8Array): Promise<RGBImage> {
67
+ // PNG: direct, exact (the parity-golden path)
68
+ if (bytes.length > 8 && bytes[0] === 0x89 && bytes[1] === 0x50)
69
+ return pngToRgb(bytes);
70
+ // everything else: native transcode to lossless PNG (HEIC/AVIF/WebP/
71
+ // JPEG/TIFF/GIF/BMP — whatever the OS codecs decode), EXIF auto-orient
72
+ try {
73
+ const png = await new Bun.Image(bytes).png({ compressionLevel: 1 }).bytes();
74
+ return pngToRgb(png);
75
+ } catch (e) {
76
+ const code = (e as { code?: string }).code;
77
+ throw new Error(
78
+ code === "ERR_IMAGE_UNKNOWN_FORMAT"
79
+ ? "unsupported image format"
80
+ : `image decode failed: ${(e as Error).message}`,
81
+ );
82
+ }
83
+ }
84
+
85
+ // --- PIL-style convolution resize (bicubic, a = -0.5) ---------------------
86
+
87
+ function bicubic(x: number): number {
88
+ const a = -0.5;
89
+ x = Math.abs(x);
90
+ if (x < 1) return ((a + 2) * x - (a + 3)) * x * x + 1;
91
+ if (x < 2) return (((x - 5) * x + 8) * x - 4) * a;
92
+ return 0;
93
+ }
94
+
95
+ interface ResampleCoeffs {
96
+ bounds: Int32Array; // [outSize × 2]: (start, count)
97
+ weights: Float64Array; // [outSize × kmax]
98
+ kmax: number;
99
+ }
100
+
101
+ /** Port of PIL's precompute_coeffs (antialias: support scales with ratio). */
102
+ function precomputeCoeffs(inSize: number, outSize: number): ResampleCoeffs {
103
+ const SUPPORT = 2.0;
104
+ const scale = inSize / outSize;
105
+ const filterscale = Math.max(scale, 1.0);
106
+ const support = SUPPORT * filterscale;
107
+ const kmax = Math.ceil(support) * 2 + 1;
108
+ const bounds = new Int32Array(outSize * 2);
109
+ const weights = new Float64Array(outSize * kmax);
110
+
111
+ for (let xx = 0; xx < outSize; xx++) {
112
+ const center = (xx + 0.5) * scale;
113
+ let xmin = Math.floor(center - support);
114
+ if (xmin < 0) xmin = 0;
115
+ let xmax = Math.ceil(center + support);
116
+ if (xmax > inSize) xmax = inSize;
117
+ const count = xmax - xmin;
118
+ let sum = 0;
119
+ for (let k = 0; k < count; k++) {
120
+ const w = bicubic((xmin + k - center + 0.5) / filterscale);
121
+ weights[xx * kmax + k] = w;
122
+ sum += w;
123
+ }
124
+ if (sum !== 0)
125
+ for (let k = 0; k < count; k++) weights[xx * kmax + k]! /= sum;
126
+ bounds[xx * 2] = xmin;
127
+ bounds[xx * 2 + 1] = count;
128
+ }
129
+ return { bounds, weights, kmax };
130
+ }
131
+
132
+ /** Separable resize on float RGB planes (clamped back to [0,255] like PIL). */
133
+ export function resizeBicubic(img: RGBImage, outW: number, outH: number): RGBImage {
134
+ const { width: inW, height: inH, data } = img;
135
+ // horizontal pass → [inH, outW]
136
+ const hCoef = precomputeCoeffs(inW, outW);
137
+ const tmp = new Float64Array(inH * outW * 3);
138
+ for (let y = 0; y < inH; y++) {
139
+ for (let x = 0; x < outW; x++) {
140
+ const xmin = hCoef.bounds[x * 2]!;
141
+ const count = hCoef.bounds[x * 2 + 1]!;
142
+ let r = 0, g = 0, b = 0;
143
+ for (let k = 0; k < count; k++) {
144
+ const w = hCoef.weights[x * hCoef.kmax + k]!;
145
+ const src = (y * inW + xmin + k) * 3;
146
+ r += data[src]! * w;
147
+ g += data[src + 1]! * w;
148
+ b += data[src + 2]! * w;
149
+ }
150
+ const dst = (y * outW + x) * 3;
151
+ tmp[dst] = r;
152
+ tmp[dst + 1] = g;
153
+ tmp[dst + 2] = b;
154
+ }
155
+ }
156
+ // vertical pass → [outH, outW]
157
+ const vCoef = precomputeCoeffs(inH, outH);
158
+ const out = new Uint8Array(outH * outW * 3);
159
+ for (let y = 0; y < outH; y++) {
160
+ const ymin = vCoef.bounds[y * 2]!;
161
+ const count = vCoef.bounds[y * 2 + 1]!;
162
+ for (let x = 0; x < outW; x++) {
163
+ let r = 0, g = 0, b = 0;
164
+ for (let k = 0; k < count; k++) {
165
+ const w = vCoef.weights[y * vCoef.kmax + k]!;
166
+ const src = ((ymin + k) * outW + x) * 3;
167
+ r += tmp[src]! * w;
168
+ g += tmp[src + 1]! * w;
169
+ b += tmp[src + 2]! * w;
170
+ }
171
+ const dst = (y * outW + x) * 3;
172
+ out[dst] = Math.min(255, Math.max(0, Math.round(r)));
173
+ out[dst + 1] = Math.min(255, Math.max(0, Math.round(g)));
174
+ out[dst + 2] = Math.min(255, Math.max(0, Math.round(b)));
175
+ }
176
+ }
177
+ return { width: outW, height: outH, data: out };
178
+ }
179
+
180
+ /** Port of _aspect_ratio_preserving_resize. */
181
+ export function targetSize(
182
+ width: number, height: number,
183
+ patchSize = PATCH_SIZE, poolingKernel = POOLING_KERNEL_SIZE,
184
+ ): { width: number; height: number } {
185
+ const maxPatches = NUM_SOFT_TOKENS * poolingKernel ** 2;
186
+ const targetPx = maxPatches * patchSize ** 2;
187
+ const factor = Math.sqrt(targetPx / (height * width));
188
+ const sideMult = poolingKernel * patchSize; // 48
189
+
190
+ let tH = Math.floor((factor * height) / sideMult) * sideMult;
191
+ let tW = Math.floor((factor * width) / sideMult) * sideMult;
192
+ if (tH === 0 && tW === 0) throw new Error("image resizes to 0×0");
193
+
194
+ const maxSide = Math.floor(maxPatches / poolingKernel ** 2) * sideMult;
195
+ if (tH === 0) {
196
+ tH = sideMult;
197
+ tW = Math.min(Math.floor(width / height) * sideMult, maxSide);
198
+ } else if (tW === 0) {
199
+ tW = sideMult;
200
+ tH = Math.min(Math.floor(height / width) * sideMult, maxSide);
201
+ }
202
+ return { width: tW, height: tH };
203
+ }
204
+
205
+ /** Full pipeline: bytes → padded patch vectors + grid positions. */
206
+ export async function preprocessImage(bytes: Uint8Array): Promise<PreprocessedImage> {
207
+ let img = await decodeImage(bytes);
208
+ const t = targetSize(img.width, img.height);
209
+ if (t.width !== img.width || t.height !== img.height)
210
+ img = resizeBicubic(img, t.width, t.height);
211
+
212
+ const { width, height, data } = img;
213
+ const p = MODEL_PATCH_SIZE;
214
+ const pH = Math.floor(height / p);
215
+ const pW = Math.floor(width / p);
216
+ const n = pH * pW;
217
+ if (n === 0) throw new Error("image smaller than one 48×48 patch");
218
+ if (n > NUM_SOFT_TOKENS)
219
+ throw new Error(`image yields ${n} patches > budget ${NUM_SOFT_TOKENS}`);
220
+
221
+ const patchDim = p * p * 3;
222
+ const patches = new Float32Array(NUM_SOFT_TOKENS * patchDim);
223
+ const positions = new Int32Array(NUM_SOFT_TOKENS * 2).fill(-1);
224
+
225
+ // patch vector layout matches the python reshape/transpose:
226
+ // [pH, pW, p, p, C] flattened — i.e. for patch (py, px):
227
+ // index = ((dy * p) + dx) * 3 + c over [C=channel-last]... python does
228
+ // transpose(1,3,2,4,0) from [C, pH, p, pW, p] → [pH, pW, p, p, C]
229
+ for (let py = 0; py < pH; py++) {
230
+ for (let px = 0; px < pW; px++) {
231
+ const base = (py * pW + px) * patchDim;
232
+ for (let dy = 0; dy < p; dy++) {
233
+ for (let dx = 0; dx < p; dx++) {
234
+ const src = ((py * p + dy) * width + px * p + dx) * 3;
235
+ const dst = base + (dy * p + dx) * 3;
236
+ // python rescales then patchifies in CHW; layout [p, p, C]
237
+ patches[dst] = data[src]! / 255;
238
+ patches[dst + 1] = data[src + 1]! / 255;
239
+ patches[dst + 2] = data[src + 2]! / 255;
240
+ }
241
+ }
242
+ positions[(py * pW + px) * 2] = px; // x
243
+ positions[(py * pW + px) * 2 + 1] = py; // y
244
+ }
245
+ }
246
+ return { patches, positions, softTokens: n };
247
+ }
@@ -0,0 +1,398 @@
1
+ // Multimodal prompt assembly: OpenAI image/audio content parts → spliced
2
+ // token ids + merged input embeddings + attention/zeroing masks.
3
+ //
4
+ // The chat template renders one <|image|> token (258880) per image part and
5
+ // one <|audio|> token (258881) per audio part; we expand each in document
6
+ // order — <boi> + image_token × softTokens + <eoi> (matching optiq's vision
7
+ // frontend) and <boa> + audio_token × n + <eoa> with
8
+ // n = min(ceil(duration_ms/40), 750) (processing_gemma4.py) — embed the full
9
+ // sequence, then overwrite the soft-token rows with the towers' features.
10
+ //
11
+ // Attention semantics (`02d723a:docs/design/generic-model-support.md` §6.6, resolved vs the oracle):
12
+ // - image runs attend bidirectionally among themselves (bidirMask), BUT
13
+ // any audio token in the prompt disables the vision overlay entirely —
14
+ // mixed image+audio prompts run fully causal (bidirMask = null);
15
+ // - audio runs are strictly causal, always;
16
+ // - per-layer-input id zeroing (e2b/e4b) applies to the UNION of
17
+ // multimodal soft tokens (image | audio) — multimodalMask, decoupled
18
+ // from the bidirectional mask.
19
+
20
+ import { MlxArray } from "@mlx-bun/mlx/array";
21
+ import { Dtype } from "@mlx-bun/mlx/ffi";
22
+ import * as ops from "@mlx-bun/mlx/ops";
23
+ import type { AudioEncoder,TextEmbeddingModel,VisionEncoder } from "../../contracts/mlx/media";
24
+ import type { PromptNativeWork } from "../../contracts/portable/prompt";
25
+ import type { EncoderCache } from "../../state/encoder-cache";
26
+ import type { ChatMessage,ChatTemplate,ToolDefinition } from "../chat-template";
27
+ import type { LoadedTokenizer } from "../tokenizer";
28
+
29
+ import { audioSoftTokenCount,decodeAudio } from "../audio/decode";
30
+ import { extractMelFeatures } from "../audio/features";
31
+ import { fetchMediaBytes,videoMediaFetchPolicy } from "../media-fetch";
32
+
33
+ export type { VisionEncoder } from "../../contracts/mlx/media";
34
+
35
+ export interface VisionPrompt {
36
+ ids: number[];
37
+ prefixIdentity?: string;
38
+ /** Unscaled merged embeddings [1, L, hidden] — caller disposes. */
39
+ embeddings: MlxArray;
40
+ /** bool [L] image-token mask — caller disposes. */
41
+ imageMask: MlxArray;
42
+ }
43
+
44
+ export interface MultimodalPrompt {
45
+ ids: number[];
46
+ /** Includes every media item and the exact token prefix through the last one. */
47
+ prefixIdentity?: string;
48
+ /** Unscaled merged embeddings [1, L, hidden] — caller disposes. */
49
+ embeddings: MlxArray;
50
+ /** bool [L] image-token mask for the bidirectional attention overlay.
51
+ * null when the prompt contains ANY audio (§3.3 Q1: mixed image+audio
52
+ * runs fully causal) or no images. Caller disposes when non-null. */
53
+ bidirMask: MlxArray | null;
54
+ /** bool [L] union multimodal soft-token mask (image | audio) for
55
+ * per-layer-input id zeroing (§3.3 Q2). Caller disposes. */
56
+ multimodalMask: MlxArray;
57
+ }
58
+
59
+ export interface VisionTokenIds {
60
+ imageTokenId: number;
61
+ boiTokenId: number;
62
+ eoiTokenId: number;
63
+ }
64
+
65
+ export interface AudioTokenIds {
66
+ audioTokenId: number;
67
+ boaTokenId: number;
68
+ eoaTokenId: number;
69
+ }
70
+
71
+ /** The towers a multimodal prompt may need; pass only what the request uses
72
+ * (vision-only and audio-only fall out as special cases). */
73
+ export interface MultimodalTowers<P extends { softTokens: number }> {
74
+ vision?: { tower: VisionEncoder<P>; tokenIds: VisionTokenIds; cache?: EncoderCache };
75
+ audio?: { tower: AudioEncoder; tokenIds: AudioTokenIds; cache?: EncoderCache };
76
+ }
77
+
78
+ /** Extract image bytes from OpenAI-style content parts, rewriting the
79
+ * parts to the template's {type:"image"} form. */
80
+ export async function extractImages(
81
+ messages: ChatMessage[],
82
+ ): Promise<{ messages: ChatMessage[]; images: Uint8Array[] }> {
83
+ const images: Uint8Array[] = [];
84
+ const out: ChatMessage[] = [];
85
+ for (const m of messages) {
86
+ if (!Array.isArray(m.content)) {
87
+ out.push(m);
88
+ continue;
89
+ }
90
+ const parts: Array<Record<string, unknown>> = [];
91
+ for (const part of m.content) {
92
+ if (part.type === "image_url") {
93
+ const url = (part.image_url as { url: string } | undefined)?.url
94
+ ?? (part.image_url as unknown as string);
95
+ if (typeof url !== "string") throw new Error("image_url part missing url");
96
+ images.push(await fetchMediaBytes(url, "image"));
97
+ parts.push({ type: "image" });
98
+ } else if (part.type === "image") {
99
+ // already template-form: expect base64 `data` or pass-through marker
100
+ if (typeof part.data === "string") {
101
+ images.push(Uint8Array.from(Buffer.from(part.data, "base64")));
102
+ parts.push({ type: "image" });
103
+ } else {
104
+ throw new Error("image part requires base64 `data`");
105
+ }
106
+ } else {
107
+ parts.push(part);
108
+ }
109
+ }
110
+ out.push({ ...m, content: parts });
111
+ }
112
+ return { messages: out, images };
113
+ }
114
+
115
+ /** Extract video bytes from content parts, rewriting to the template's
116
+ * {type:"video"} form. Accepts {type:"video_url", video_url:{url}} (data:
117
+ * or http(s), same SSRF guard as images with the larger video body cap)
118
+ * and {type:"video"} with base64 `data`. Decode to frames happens later
119
+ * (packages/inference/src/input/vision/video-frames.ts — the AVFoundation sidecar). */
120
+ export async function extractVideos(
121
+ messages: ChatMessage[],
122
+ ): Promise<{ messages: ChatMessage[]; videos: Uint8Array[] }> {
123
+ const videos: Uint8Array[] = [];
124
+ const out: ChatMessage[] = [];
125
+ for (const m of messages) {
126
+ if (!Array.isArray(m.content)) {
127
+ out.push(m);
128
+ continue;
129
+ }
130
+ const parts: Array<Record<string, unknown>> = [];
131
+ for (const part of m.content) {
132
+ if (part.type === "video_url") {
133
+ const url = (part.video_url as { url: string } | undefined)?.url
134
+ ?? (part.video_url as unknown as string);
135
+ if (typeof url !== "string") throw new Error("video_url part missing url");
136
+ videos.push(await fetchMediaBytes(url, "video"));
137
+ parts.push({ type: "video" });
138
+ } else if (part.type === "video") {
139
+ if (typeof part.data === "string") {
140
+ // Same cap as fetched/data:-URL video bodies — the inline base64
141
+ // form must not be the uncapped route.
142
+ const cap = videoMediaFetchPolicy().maxBytes;
143
+ if (part.data.length * 0.75 > cap)
144
+ throw new Error(
145
+ `video part exceeds the ${Math.round(cap / 1024 / 1024)} MB cap`,
146
+ );
147
+ videos.push(Uint8Array.from(Buffer.from(part.data, "base64")));
148
+ parts.push({ type: "video" });
149
+ } else {
150
+ throw new Error("video part requires base64 `data`");
151
+ }
152
+ } else {
153
+ parts.push(part);
154
+ }
155
+ }
156
+ out.push({ ...m, content: parts });
157
+ }
158
+ return { messages: out, videos };
159
+ }
160
+
161
+ /** Extract audio bytes from OpenAI-style content parts, rewriting the parts
162
+ * to the template's {type:"audio"} form. Accepts the OpenAI-canonical
163
+ * {type:"input_audio", input_audio:{data:<b64>, format:...}} plus optiq's
164
+ * {type:"audio"} (base64 `data`) and {type:"audio_url"} aliases. */
165
+ export async function extractAudio(
166
+ messages: ChatMessage[],
167
+ ): Promise<{ messages: ChatMessage[]; audio: Uint8Array[] }> {
168
+ const audio: Uint8Array[] = [];
169
+ const out: ChatMessage[] = [];
170
+ for (const m of messages) {
171
+ if (!Array.isArray(m.content)) {
172
+ out.push(m);
173
+ continue;
174
+ }
175
+ const parts: Array<Record<string, unknown>> = [];
176
+ for (const part of m.content) {
177
+ if (part.type === "input_audio") {
178
+ const data = (part.input_audio as { data?: string } | undefined)?.data;
179
+ if (typeof data !== "string")
180
+ throw new Error("input_audio part requires base64 `input_audio.data`");
181
+ audio.push(Uint8Array.from(Buffer.from(data, "base64")));
182
+ parts.push({ type: "audio" });
183
+ } else if (part.type === "audio") {
184
+ if (typeof part.data === "string") {
185
+ audio.push(Uint8Array.from(Buffer.from(part.data, "base64")));
186
+ parts.push({ type: "audio" });
187
+ } else {
188
+ throw new Error("audio part requires base64 `data`");
189
+ }
190
+ } else if (part.type === "audio_url") {
191
+ const url = (part.audio_url as { url: string } | undefined)?.url
192
+ ?? (part.audio_url as unknown as string);
193
+ if (typeof url !== "string") throw new Error("audio_url part missing url");
194
+ audio.push(await fetchMediaBytes(url, "audio"));
195
+ parts.push({ type: "audio" });
196
+ } else {
197
+ parts.push(part);
198
+ }
199
+ }
200
+ out.push({ ...m, content: parts });
201
+ }
202
+ return { messages: out, audio };
203
+ }
204
+
205
+ /** Decoded + featurized audio clip, ready to splice. */
206
+ interface PreparedAudio {
207
+ mel: Float32Array;
208
+ frames: number;
209
+ softTokens: number;
210
+ }
211
+
212
+ export async function buildMultimodalPrompt<P extends { softTokens: number }>(
213
+ model: TextEmbeddingModel,
214
+ towers: MultimodalTowers<P>,
215
+ tokenizer: LoadedTokenizer,
216
+ template: ChatTemplate,
217
+ messages: ChatMessage[],
218
+ images: Uint8Array[],
219
+ audio: Uint8Array[],
220
+ tools: ToolDefinition[] | null = null,
221
+ nativeWork: PromptNativeWork = (work) => work(),
222
+ ): Promise<MultimodalPrompt> {
223
+ if (images.length > 0 && !towers.vision)
224
+ throw new Error("images present but no vision tower configured");
225
+ if (audio.length > 0 && !towers.audio)
226
+ throw new Error("audio present but no audio tower configured");
227
+
228
+ const rendered = template.render(messages, { tools });
229
+ let ids = tokenizer.encode(rendered);
230
+ if (ids[0] === ids[1] && ids[0] === tokenizer.bosTokenId) ids = ids.slice(1);
231
+
232
+ // preprocess every clip/image up front (splice lengths depend on them);
233
+ // audio soft-token counts come from the DECODED samples, never metadata.
234
+ const pre = await Promise.all(
235
+ images.map((bytes) => towers.vision!.tower.preprocess(bytes)),
236
+ );
237
+ const clips: PreparedAudio[] = audio.map((bytes) => {
238
+ const samples = decodeAudio(bytes);
239
+ const softTokens = audioSoftTokenCount(samples.length);
240
+ const m = extractMelFeatures(samples);
241
+ return { mel: m.features, frames: m.frames, softTokens };
242
+ });
243
+
244
+ const vIds = towers.vision?.tokenIds;
245
+ const aIds = towers.audio?.tokenIds;
246
+
247
+ // expand each <|image|> into <boi> + image_token×soft + <eoi> and each
248
+ // <|audio|> into <boa> + audio_token×n + <eoa>, in document order
249
+ const spliced: number[] = [];
250
+ const runs: { start: number; length: number; kind: "image" | "audio"; index: number }[] = [];
251
+ let imgIdx = 0;
252
+ let audIdx = 0;
253
+ for (const id of ids) {
254
+ if (vIds && id === vIds.imageTokenId) {
255
+ const p = pre[imgIdx];
256
+ if (!p) throw new Error("more <|image|> markers than images");
257
+ spliced.push(vIds.boiTokenId);
258
+ runs.push({ start: spliced.length, length: p.softTokens, kind: "image", index: imgIdx });
259
+ for (let k = 0; k < p.softTokens; k++) spliced.push(vIds.imageTokenId);
260
+ spliced.push(vIds.eoiTokenId);
261
+ imgIdx++;
262
+ } else if (aIds && id === aIds.audioTokenId) {
263
+ const clip = clips[audIdx];
264
+ if (!clip) throw new Error("more <|audio|> markers than audio clips");
265
+ spliced.push(aIds.boaTokenId);
266
+ runs.push({ start: spliced.length, length: clip.softTokens, kind: "audio", index: audIdx });
267
+ for (let k = 0; k < clip.softTokens; k++) spliced.push(aIds.audioTokenId);
268
+ spliced.push(aIds.eoaTokenId);
269
+ audIdx++;
270
+ } else {
271
+ spliced.push(id);
272
+ }
273
+ }
274
+ if (imgIdx !== images.length)
275
+ throw new Error(`${images.length} images but ${imgIdx} <|image|> markers`);
276
+ if (audIdx !== audio.length)
277
+ throw new Error(`${audio.length} audio clips but ${audIdx} <|audio|> markers`);
278
+
279
+ // Exact encoded objects contain no request positions or template state.
280
+ // CPU preprocessing and SSD reads run before acquiring native execution.
281
+ const keys = runs.map(run => towers[run.kind === "image" ? "vision" : "audio"]?.cache
282
+ ? new Bun.CryptoHasher("sha256").update(`gemma-${run.kind}-input-v1`)
283
+ .update(run.kind === "image" ? images[run.index]! : audio[run.index]!).digest("hex")
284
+ : "");
285
+ const lastMedia = runs.at(-1);
286
+ // A matching namespace guarantees any reused prefix includes all media.
287
+ // The audio-presence bit also identifies Gemma's causal/bidirectional policy.
288
+ const prefixIdentity = lastMedia && keys.every(Boolean)
289
+ ? new Bun.CryptoHasher("sha256").update("gemma-prepared-prefix-v1")
290
+ .update(JSON.stringify({ audio: audio.length > 0,
291
+ tokens: spliced.slice(0, lastMedia.start + lastMedia.length),
292
+ media: runs.map((run, i) => [run.kind, keys[i],
293
+ (run.kind === "image" ? towers.vision : towers.audio)!.cache!.identity]),
294
+ })).digest("hex") : undefined;
295
+ const reused: (MlxArray | null)[] = [];
296
+ try {
297
+ for (const [index, run] of runs.entries()) {
298
+ const cache = run.kind === "image" ? towers.vision?.cache : towers.audio?.cache;
299
+ reused.push(cache ? await cache.take(keys[index]!) : null);
300
+ }
301
+ return await nativeWork(async () => {
302
+ // embed text tokens, then overwrite soft-token rows with tower features
303
+ using idsArr = ops.fromInt32(spliced, [1, spliced.length]);
304
+ let embeds = model.embed.encode(idsArr);
305
+ try {
306
+ const hidden = embeds.shape[2]!;
307
+ for (const [runIndex, run] of runs.entries()) {
308
+ let cast: MlxArray;
309
+ if (run.kind === "image") {
310
+ // vision towers return pre-divided by embed_scale (their oracle's
311
+ // frontend convention) — cast to the embedding dtype and splice.
312
+ const cached = reused[runIndex];
313
+ using feats = cached ?? towers.vision!.tower.features(pre[run.index]!); // [1, soft, hidden]
314
+ reused[runIndex] = null;
315
+ if (!cached) towers.vision!.cache?.put(keys[runIndex]!, feats);
316
+ cast = feats.astype(embeds.dtype);
317
+ } else {
318
+ // audio mirrors `02d723a:scripts/oracle/gen-e4b-audio-golden.py` EXACTLY:
319
+ // features.astype(embeds.dtype) / embed_scale
320
+ // — raw f32 embed_audio output, cast to bf16 FIRST, then divided by a
321
+ // weak (dtype-following, i.e. bf16) embed_scale scalar. The order is
322
+ // load-bearing for the bit-exact greedy gate (see AudioTower.features).
323
+ const clip = clips[run.index]!;
324
+ const cached = reused[runIndex];
325
+ using raw = cached ?? towers.audio!.tower.features(clip.mel, clip.frames, false); // [1, n, hidden] f32
326
+ reused[runIndex] = null;
327
+ if (raw.shape[1] !== clip.softTokens) {
328
+ const n = raw.shape[1];
329
+ throw new Error(`audio tower produced ${n} frames but splice expects ${clip.softTokens}`);
330
+ }
331
+ if (!cached) towers.audio!.cache?.put(keys[runIndex]!, raw);
332
+ using bf = raw.astype(embeds.dtype);
333
+ using scale = ops.scalarLike(towers.audio!.tower.embedScale, bf);
334
+ cast = ops.div(bf, scale);
335
+ }
336
+ const { start, length } = run;
337
+ try {
338
+ const updated = ops.sliceUpdate(embeds, cast, [0, start, 0], [1, start + length, hidden]);
339
+ embeds.dispose();
340
+ embeds = updated;
341
+ } finally { cast.dispose(); }
342
+ }
343
+
344
+ // masks over the spliced sequence: union multimodal soft tokens (per-layer
345
+ // id zeroing) always; image-only bidirectional overlay only when NO audio
346
+ // is present (§3.3 Q1).
347
+ const mmInts = new Int32Array(spliced.length);
348
+ for (let i = 0; i < spliced.length; i++) {
349
+ const id = spliced[i]!;
350
+ mmInts[i] = (vIds && id === vIds.imageTokenId) || (aIds && id === aIds.audioTokenId) ? 1 : 0;
351
+ }
352
+ const mmI32 = MlxArray.fromInt32(mmInts, [spliced.length]);
353
+ const multimodalMask = mmI32.astype(Dtype.bool);
354
+ mmI32.dispose();
355
+
356
+ let bidirMask: MlxArray | null = null;
357
+ if (images.length > 0 && audio.length === 0) {
358
+ const imgInts = new Int32Array(spliced.length);
359
+ for (let i = 0; i < spliced.length; i++)
360
+ imgInts[i] = spliced[i] === vIds!.imageTokenId ? 1 : 0;
361
+ const imgI32 = MlxArray.fromInt32(imgInts, [spliced.length]);
362
+ bidirMask = imgI32.astype(Dtype.bool);
363
+ imgI32.dispose();
364
+ }
365
+
366
+ return { ids: spliced, embeddings: embeds, bidirMask, multimodalMask, prefixIdentity };
367
+ } catch (error) { embeds.dispose(); throw error; }
368
+ });
369
+ } finally { for (const tensor of reused) tensor?.dispose(); }
370
+ }
371
+
372
+ /** Vision-only prompt (back-compat wrapper over buildMultimodalPrompt —
373
+ * behavior byte-identical to the pre-audio builder: the returned imageMask
374
+ * serves as BOTH the bidirectional-attention mask and the per-layer-input
375
+ * zeroing mask, which forwardEmbeddings derives from `bidir` when no
376
+ * separate multimodal mask is passed). */
377
+ export async function buildVisionPrompt<P extends { softTokens: number }>(
378
+ model: TextEmbeddingModel,
379
+ tower: VisionEncoder<P>,
380
+ tokenizer: LoadedTokenizer,
381
+ template: ChatTemplate,
382
+ messages: ChatMessage[],
383
+ images: Uint8Array[],
384
+ tokenIds: VisionTokenIds,
385
+ tools: ToolDefinition[] | null = null,
386
+ nativeWork: PromptNativeWork = (work) => work(),
387
+ cache?: EncoderCache,
388
+ ): Promise<VisionPrompt> {
389
+ const mp = await buildMultimodalPrompt(
390
+ model, { vision: { tower, tokenIds, cache } }, tokenizer, template,
391
+ messages, images, [], tools, nativeWork,
392
+ );
393
+ // no audio was passed, so the union mask IS the image mask — reuse it for
394
+ // the zero-image edge (the original builder returned an all-false mask).
395
+ const imageMask = mp.bidirMask ?? mp.multimodalMask;
396
+ if (mp.bidirMask) mp.multimodalMask.dispose();
397
+ return { ids: mp.ids, embeddings: mp.embeddings, imageMask, prefixIdentity: mp.prefixIdentity };
398
+ }