mlx-bun 0.0.13 → 0.2.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 (224) hide show
  1. package/README.md +31 -21
  2. package/bin/mlx-bun.mjs +1 -1
  3. package/docs/{design → archive}/hlg-sampling.md +3 -3
  4. package/docs/{investigations → archive/investigations}/hlg-sampling-investigation.md +1 -1
  5. package/docs/archive/investigations/pareto-specialized-runtime-findings.md +92 -0
  6. package/docs/{planning → archive/planning}/chat-ui-vision.md +1 -1
  7. package/docs/{planning → archive/planning}/release-notes-v0.0.9.md +1 -1
  8. package/docs/{planning → archive/planning}/website-readme-pass-plan.md +1 -1
  9. package/docs/design/colibri-glm52-port.md +1 -1
  10. package/docs/design/compat-cli-surface-design.md +1 -1
  11. package/docs/design/decode-speed-program.md +1 -1
  12. package/docs/design/docs-reorg-plan.md +1 -1
  13. package/docs/design/dspark-serving-program.md +1 -1
  14. package/docs/design/dspark-speculative-decoding.md +53 -3
  15. package/docs/design/grammar-spec-batching-integration.md +1 -1
  16. package/docs/design/mlx-lm-serving-execution-seam.md +81 -0
  17. package/docs/design/orpo-dynamic-lambda.md +1 -1
  18. package/docs/design/orpo-future-enhancements.md +1 -1
  19. package/docs/design/orpo-training.md +2 -2
  20. package/docs/design/parity-tier-dag.md +1 -1
  21. package/docs/design/repo-cleanup-plan.md +46 -4
  22. package/docs/design/segmented-backward-training.md +2 -2
  23. package/docs/design/ssd-kv-cold-tier.md +127 -0
  24. package/docs/design/turboquant-weights.md +467 -0
  25. package/docs/design/w4a16-compute-precision-spike.md +1 -1
  26. package/docs/design/web-chat-redesign.md +5 -5
  27. package/docs/planning/chunk-task-roadmap.md +1 -1
  28. package/docs/planning/curve-sampler-research-plan.md +4 -4
  29. package/docs/planning/memory-docs-and-dag-plan.md +1 -1
  30. package/docs/planning/release-notes-v0.2.0.md +49 -0
  31. package/docs/planning/video-series.md +421 -0
  32. package/docs/reference/cli.md +15 -4
  33. package/docs/reference/distribution.md +1 -0
  34. package/docs/reference/features-matrix.md +3 -2
  35. package/docs/reference/library-api.md +41 -1
  36. package/docs/reference/models.md +33 -0
  37. package/docs/reference/server-api.md +69 -14
  38. package/docs/reference/server-config.md +57 -33
  39. package/package.json +2 -2
  40. package/src/anthropic.ts +92 -3
  41. package/src/cli.ts +131 -36
  42. package/src/eval/kl.ts +27 -20
  43. package/src/eval/runner.ts +23 -4
  44. package/src/eval/tasks/bfcl.ts +2 -1
  45. package/src/eval/tasks/gsm8k.ts +2 -1
  46. package/src/eval/tasks/hashhop.ts +2 -1
  47. package/src/eval/tasks/humaneval.ts +5 -4
  48. package/src/eval/tasks/ifeval.ts +2 -1
  49. package/src/eval/tasks/mmlu.ts +2 -1
  50. package/src/expert-io.ts +3 -1
  51. package/src/expert-offload.ts +3 -1
  52. package/src/expert-trace.ts +3 -1
  53. package/src/fit.ts +24 -43
  54. package/src/flags.ts +7 -9
  55. package/src/generate.ts +107 -92
  56. package/src/grammar.ts +2 -1
  57. package/src/harness-pi.ts +1 -1
  58. package/src/index.ts +20 -0
  59. package/src/jobs/job-entry.ts +3 -2
  60. package/src/kv-scheme.ts +199 -0
  61. package/src/kv-store.ts +370 -205
  62. package/src/media-fetch.ts +19 -3
  63. package/src/memory/model.ts +2 -1
  64. package/src/memory/vault.ts +2 -1
  65. package/src/memory-plan.ts +18 -0
  66. package/src/mlx/ffi.ts +8 -11
  67. package/src/mlx/ops.ts +31 -6
  68. package/src/model/batched-mask.ts +1 -1
  69. package/src/model/batched-quant.ts +1 -1
  70. package/src/model/batched-rotating-quant.ts +43 -97
  71. package/src/model/batched-rotating-state.ts +73 -0
  72. package/src/model/batched-rotating.ts +56 -105
  73. package/src/model/batched-row-storage.ts +127 -0
  74. package/src/model/compiled-decode.ts +3 -2
  75. package/src/model/factory.ts +44 -49
  76. package/src/model/gemma4-base.ts +163 -10
  77. package/src/model/generated/gemma4-12b.ts +50 -146
  78. package/src/model/generated/gemma4-26b.ts +32 -92
  79. package/src/model/generated/gemma4-e4b.ts +26 -74
  80. package/src/model/glm52-cache.ts +19 -9
  81. package/src/model/glm52-memory.ts +28 -1
  82. package/src/model/minicpm5.ts +2 -1
  83. package/src/model/profile.ts +433 -0
  84. package/src/model/qwen3-delta.ts +98 -2
  85. package/src/model/qwen3-mrope.ts +252 -0
  86. package/src/model/qwen3_5.ts +217 -25
  87. package/src/native/frame_extract.swift +140 -0
  88. package/src/native-pack.ts +13 -6
  89. package/src/pi-web.ts +6 -5
  90. package/src/prompt-cache.ts +2 -2
  91. package/src/quantize/atomic-output.ts +38 -0
  92. package/src/quantize/calibration.ts +2 -1
  93. package/src/quantize/config-writer.ts +6 -0
  94. package/src/quantize/index.ts +28 -1
  95. package/src/quantize/job.ts +11 -0
  96. package/src/quantize/quantizer.ts +265 -100
  97. package/src/quantize/rotate.ts +603 -0
  98. package/src/quantize/safetensors-writer.ts +79 -1
  99. package/src/quantize/sensitivity.ts +41 -59
  100. package/src/quantize/weight-transform.ts +292 -0
  101. package/src/responses.ts +102 -2
  102. package/src/runtime-config.ts +67 -0
  103. package/src/sampler.ts +272 -0
  104. package/src/serve/admin-routes.ts +184 -0
  105. package/src/serve/aux-routes.ts +132 -0
  106. package/src/serve/batch-scheduler.ts +200 -87
  107. package/src/serve/completion-executor.ts +346 -0
  108. package/src/serve/completion-sink.ts +157 -0
  109. package/src/serve/discovery-routes.ts +205 -0
  110. package/src/serve/generation-gateway.ts +116 -109
  111. package/src/serve/kv-budget.ts +14 -0
  112. package/src/serve/lab-routes.ts +338 -0
  113. package/src/serve/model-admin-routes.ts +153 -0
  114. package/src/serve/prompt-response-trace.ts +147 -0
  115. package/src/serve/request-plan.ts +158 -0
  116. package/src/serve/static-routes.ts +101 -0
  117. package/src/server.ts +704 -1400
  118. package/src/spec/dflash-source.ts +2 -1
  119. package/src/spec/dspark/module-dflash.ts +1 -1
  120. package/src/spec/glm52-mtp-source.ts +8 -0
  121. package/src/spec/qwen-mtp-source.ts +431 -0
  122. package/src/spec/serve-loop.ts +36 -68
  123. package/src/ssd-cache.ts +17 -14
  124. package/src/ssd-durability.ts +195 -0
  125. package/src/train/flash-cce.ts +12 -11
  126. package/src/train/forward.ts +32 -1
  127. package/src/train/loss.ts +2 -1
  128. package/src/train/segmented.ts +4 -3
  129. package/src/train/trainer.ts +3 -2
  130. package/src/vision/preprocess.ts +1 -1
  131. package/src/vision/prompt.ts +47 -1
  132. package/src/vision/qwen3vl-preprocess.ts +409 -0
  133. package/src/vision/qwen3vl-prompt.ts +196 -0
  134. package/src/vision/qwen3vl-tower.ts +513 -0
  135. package/src/vision/video-frames.ts +148 -0
  136. package/src/web/app.js +10 -3
  137. package/src/web/src/chat.ts +12 -5
  138. package/src/weights.ts +81 -26
  139. /package/docs/{investigations → archive/investigations}/chunk-finetune-experiment.md +0 -0
  140. /package/docs/{investigations → archive/investigations}/colibri-oracle-pin.md +0 -0
  141. /package/docs/{investigations → archive/investigations}/curve-bisector-routing.md +0 -0
  142. /package/docs/{investigations → archive/investigations}/curve-runs/bisector-route.json +0 -0
  143. /package/docs/{investigations → archive/investigations}/curve-runs/bisector-trace.json +0 -0
  144. /package/docs/{investigations → archive/investigations}/curve-runs/curve-terrain.json +0 -0
  145. /package/docs/{investigations → archive/investigations}/curve-runs/curve-terrain.svg +0 -0
  146. /package/docs/{investigations → archive/investigations}/curve-terrain.html +0 -0
  147. /package/docs/{investigations → archive/investigations}/decode-roofline-lookagain.md +0 -0
  148. /package/docs/{investigations → archive/investigations}/dspark-handoff.md +0 -0
  149. /package/docs/{investigations → archive/investigations}/expert-offload-single-user-moe.md +0 -0
  150. /package/docs/{investigations → archive/investigations}/hlg-ab.html +0 -0
  151. /package/docs/{investigations → archive/investigations}/hlg-explorer.html +0 -0
  152. /package/docs/{investigations → archive/investigations}/hlg-figs/cliff.svg +0 -0
  153. /package/docs/{investigations → archive/investigations}/hlg-figs/cross.svg +0 -0
  154. /package/docs/{investigations → archive/investigations}/hlg-figs/edges.svg +0 -0
  155. /package/docs/{investigations → archive/investigations}/hlg-figs/k20-grids.svg +0 -0
  156. /package/docs/{investigations → archive/investigations}/hlg-figs/k20-peaks.svg +0 -0
  157. /package/docs/{investigations → archive/investigations}/hlg-figs/lw-curve.svg +0 -0
  158. /package/docs/{investigations → archive/investigations}/hlg-figs/terrain.svg +0 -0
  159. /package/docs/{investigations → archive/investigations}/hlg-report.html +0 -0
  160. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-ab.json +0 -0
  161. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-blind-prompts.md +0 -0
  162. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-blind.json +0 -0
  163. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-cliff.json +0 -0
  164. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-cross.json +0 -0
  165. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-edge.json +0 -0
  166. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-eval1.json +0 -0
  167. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-frontier.json +0 -0
  168. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-grid.json +0 -0
  169. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-grid2.json +0 -0
  170. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-lw-coarse.json +0 -0
  171. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-map.json +0 -0
  172. /package/docs/{investigations → archive/investigations}/hlg-runs/run0-original.log +0 -0
  173. /package/docs/{investigations → archive/investigations}/hlg-runs/runA-recommended.log +0 -0
  174. /package/docs/{investigations → archive/investigations}/hlg-runs/runAA-cliff-cpm5.log +0 -0
  175. /package/docs/{investigations → archive/investigations}/hlg-runs/runAB-grid2-e4b.log +0 -0
  176. /package/docs/{investigations → archive/investigations}/hlg-runs/runAC-peak-e4b-partial.log +0 -0
  177. /package/docs/{investigations → archive/investigations}/hlg-runs/runAD-ab-samples.log +0 -0
  178. /package/docs/{investigations → archive/investigations}/hlg-runs/runAE-edge-e4b.log +0 -0
  179. /package/docs/{investigations → archive/investigations}/hlg-runs/runAF-logit-calibration.log +0 -0
  180. /package/docs/{investigations → archive/investigations}/hlg-runs/runAG-eval-config2.log +0 -0
  181. /package/docs/{investigations → archive/investigations}/hlg-runs/runB-gain-variants.log +0 -0
  182. /package/docs/{investigations → archive/investigations}/hlg-runs/runC-pivots.log +0 -0
  183. /package/docs/{investigations → archive/investigations}/hlg-runs/runD-oetf.log +0 -0
  184. /package/docs/{investigations → archive/investigations}/hlg-runs/runE-eotf.log +0 -0
  185. /package/docs/{investigations → archive/investigations}/hlg-runs/runF-shaper-working.log +0 -0
  186. /package/docs/{investigations → archive/investigations}/hlg-runs/runG-shaper-sweep.log +0 -0
  187. /package/docs/{investigations → archive/investigations}/hlg-runs/runH-wsweep.log +0 -0
  188. /package/docs/{investigations → archive/investigations}/hlg-runs/runI-asweep.log +0 -0
  189. /package/docs/{investigations → archive/investigations}/hlg-runs/runJ-knobmap.log +0 -0
  190. /package/docs/{investigations → archive/investigations}/hlg-runs/runK-frontier.log +0 -0
  191. /package/docs/{investigations → archive/investigations}/hlg-runs/runL-widemap.log +0 -0
  192. /package/docs/{investigations → archive/investigations}/hlg-runs/runM-frontier-emb.log +0 -0
  193. /package/docs/{investigations → archive/investigations}/hlg-runs/runN-verify.log +0 -0
  194. /package/docs/{investigations → archive/investigations}/hlg-runs/runO-frontier-seed5000.log +0 -0
  195. /package/docs/{investigations → archive/investigations}/hlg-runs/runP-grid-AxSm.log +0 -0
  196. /package/docs/{investigations → archive/investigations}/hlg-runs/runQ-grid-tgW.log +0 -0
  197. /package/docs/{investigations → archive/investigations}/hlg-runs/runR-blind-transcript.log +0 -0
  198. /package/docs/{investigations → archive/investigations}/hlg-runs/runS-cross-coherence.log +0 -0
  199. /package/docs/{investigations → archive/investigations}/hlg-runs/runT-cross-cpm5.log +0 -0
  200. /package/docs/{investigations → archive/investigations}/hlg-runs/runU-cross-12b.log +0 -0
  201. /package/docs/{investigations → archive/investigations}/hlg-runs/runV-lw-e4b.log +0 -0
  202. /package/docs/{investigations → archive/investigations}/hlg-runs/runW-lw-12b.log +0 -0
  203. /package/docs/{investigations → archive/investigations}/hlg-runs/runX-lw-cpm5.log +0 -0
  204. /package/docs/{investigations → archive/investigations}/hlg-runs/runY-cliff-e4b.log +0 -0
  205. /package/docs/{investigations → archive/investigations}/hlg-runs/runZ-cliff-12b.log +0 -0
  206. /package/docs/{investigations → archive/investigations}/indexshare-performance-spike.md +0 -0
  207. /package/docs/{investigations → archive/investigations}/kernel-perf-review-2026-07.md +0 -0
  208. /package/docs/{investigations → archive/investigations}/lab-build-journal.md +0 -0
  209. /package/docs/{investigations → archive/investigations}/openwiki-evaluation.md +0 -0
  210. /package/docs/{investigations → archive/investigations}/orpo-base-uf-experiment-and-directions.md +0 -0
  211. /package/docs/{investigations → archive/investigations}/orpo-flash-cce-pin-leak.md +0 -0
  212. /package/docs/{investigations → archive/investigations}/orpo-uf-testing-handoff.md +0 -0
  213. /package/docs/{investigations → archive/investigations}/pi-builtin-investigation.md +0 -0
  214. /package/docs/{investigations → archive/investigations}/segmented-backward-handoff.md +0 -0
  215. /package/docs/{investigations → archive/investigations}/starter-model-port-handoff.md +0 -0
  216. /package/docs/{investigations → archive/investigations}/steel-flash-cce-handoff.md +0 -0
  217. /package/docs/{investigations → archive/investigations}/trainer-validation-experiment.md +0 -0
  218. /package/docs/{planning → archive/planning}/chat-ui-vision-memory-first.md +0 -0
  219. /package/docs/{planning → archive/planning}/journal.md +0 -0
  220. /package/docs/{planning → archive/planning}/release-notes-v0.0.10.md +0 -0
  221. /package/docs/{planning → archive/planning}/release-notes-v0.0.11.md +0 -0
  222. /package/docs/{planning → archive/planning}/release-notes-v0.0.12.md +0 -0
  223. /package/docs/{planning → archive/planning}/release-notes-v0.0.13.md +0 -0
  224. /package/docs/{planning → archive/planning}/web-ui-pass-plan.md +0 -0
@@ -12,6 +12,7 @@
12
12
  import { MlxArray } from "../../mlx/array";
13
13
  import * as ops from "../../mlx/ops";
14
14
  import {
15
+ cacheSignature,
15
16
  disposing,
16
17
  fusedSdpaRuntimeOk,
17
18
  KVCache,
@@ -529,83 +530,34 @@ function sharerFullQuantG64B4VPliScal(layer: DecoderLayer, x: MlxArray, mask: Ma
529
530
  }
530
531
 
531
532
  export class GeneratedGemma4 extends Gemma4Model {
532
- /** Per-layer cache classes must match what this file was generated
533
- * for; anything else runs the monolith path. */
533
+ /** Per-layer cache signatures must match what this file was generated for. */
534
534
  #matches(cache: Cache[]): boolean {
535
535
  return (
536
536
  cache.length === 24 &&
537
- cache[0] instanceof RotatingQuantizedKVCache &&
538
- cache[0].bits === 4 &&
539
- cache[0].groupSize === 64 &&
540
- cache[1] instanceof RotatingQuantizedKVCache &&
541
- cache[1].bits === 4 &&
542
- cache[1].groupSize === 64 &&
543
- cache[2] instanceof RotatingQuantizedKVCache &&
544
- cache[2].bits === 4 &&
545
- cache[2].groupSize === 64 &&
546
- cache[3] instanceof RotatingQuantizedKVCache &&
547
- cache[3].bits === 8 &&
548
- cache[3].groupSize === 64 &&
549
- cache[4] instanceof RotatingQuantizedKVCache &&
550
- cache[4].bits === 8 &&
551
- cache[4].groupSize === 64 &&
552
- cache[5] instanceof QuantizedKVCache &&
553
- cache[5].bits === 4 &&
554
- cache[5].groupSize === 64 &&
555
- cache[6] instanceof RotatingQuantizedKVCache &&
556
- cache[6].bits === 4 &&
557
- cache[6].groupSize === 64 &&
558
- cache[7] instanceof RotatingQuantizedKVCache &&
559
- cache[7].bits === 4 &&
560
- cache[7].groupSize === 64 &&
561
- cache[8] instanceof RotatingQuantizedKVCache &&
562
- cache[8].bits === 4 &&
563
- cache[8].groupSize === 64 &&
564
- cache[9] instanceof RotatingQuantizedKVCache &&
565
- cache[9].bits === 8 &&
566
- cache[9].groupSize === 64 &&
567
- cache[10] instanceof RotatingQuantizedKVCache &&
568
- cache[10].bits === 8 &&
569
- cache[10].groupSize === 64 &&
570
- cache[11] instanceof QuantizedKVCache &&
571
- cache[11].bits === 8 &&
572
- cache[11].groupSize === 64 &&
573
- cache[12] instanceof RotatingQuantizedKVCache &&
574
- cache[12].bits === 4 &&
575
- cache[12].groupSize === 64 &&
576
- cache[13] instanceof RotatingQuantizedKVCache &&
577
- cache[13].bits === 4 &&
578
- cache[13].groupSize === 64 &&
579
- cache[14] instanceof RotatingQuantizedKVCache &&
580
- cache[14].bits === 8 &&
581
- cache[14].groupSize === 64 &&
582
- cache[15] instanceof RotatingQuantizedKVCache &&
583
- cache[15].bits === 4 &&
584
- cache[15].groupSize === 64 &&
585
- cache[16] instanceof RotatingQuantizedKVCache &&
586
- cache[16].bits === 4 &&
587
- cache[16].groupSize === 64 &&
588
- cache[17] instanceof QuantizedKVCache &&
589
- cache[17].bits === 4 &&
590
- cache[17].groupSize === 64 &&
591
- cache[18] instanceof RotatingQuantizedKVCache &&
592
- cache[18].bits === 4 &&
593
- cache[18].groupSize === 64 &&
594
- cache[19] instanceof RotatingQuantizedKVCache &&
595
- cache[19].bits === 4 &&
596
- cache[19].groupSize === 64 &&
597
- cache[20] instanceof RotatingQuantizedKVCache &&
598
- cache[20].bits === 4 &&
599
- cache[20].groupSize === 64 &&
600
- cache[21] instanceof RotatingQuantizedKVCache &&
601
- cache[21].bits === 4 &&
602
- cache[21].groupSize === 64 &&
603
- cache[22] instanceof RotatingQuantizedKVCache &&
604
- cache[22].bits === 4 &&
605
- cache[22].groupSize === 64 &&
606
- cache[23] instanceof QuantizedKVCache &&
607
- cache[23].bits === 4 &&
608
- cache[23].groupSize === 64
537
+ cacheSignature(cache[0]) === "kv:rotating-quant:4:64" &&
538
+ cacheSignature(cache[1]) === "kv:rotating-quant:4:64" &&
539
+ cacheSignature(cache[2]) === "kv:rotating-quant:4:64" &&
540
+ cacheSignature(cache[3]) === "kv:rotating-quant:8:64" &&
541
+ cacheSignature(cache[4]) === "kv:rotating-quant:8:64" &&
542
+ cacheSignature(cache[5]) === "kv:quant:4:64" &&
543
+ cacheSignature(cache[6]) === "kv:rotating-quant:4:64" &&
544
+ cacheSignature(cache[7]) === "kv:rotating-quant:4:64" &&
545
+ cacheSignature(cache[8]) === "kv:rotating-quant:4:64" &&
546
+ cacheSignature(cache[9]) === "kv:rotating-quant:8:64" &&
547
+ cacheSignature(cache[10]) === "kv:rotating-quant:8:64" &&
548
+ cacheSignature(cache[11]) === "kv:quant:8:64" &&
549
+ cacheSignature(cache[12]) === "kv:rotating-quant:4:64" &&
550
+ cacheSignature(cache[13]) === "kv:rotating-quant:4:64" &&
551
+ cacheSignature(cache[14]) === "kv:rotating-quant:8:64" &&
552
+ cacheSignature(cache[15]) === "kv:rotating-quant:4:64" &&
553
+ cacheSignature(cache[16]) === "kv:rotating-quant:4:64" &&
554
+ cacheSignature(cache[17]) === "kv:quant:4:64" &&
555
+ cacheSignature(cache[18]) === "kv:rotating-quant:4:64" &&
556
+ cacheSignature(cache[19]) === "kv:rotating-quant:4:64" &&
557
+ cacheSignature(cache[20]) === "kv:rotating-quant:4:64" &&
558
+ cacheSignature(cache[21]) === "kv:rotating-quant:4:64" &&
559
+ cacheSignature(cache[22]) === "kv:rotating-quant:4:64" &&
560
+ cacheSignature(cache[23]) === "kv:quant:4:64"
609
561
  );
610
562
  }
611
563
 
@@ -88,7 +88,7 @@ function trimToTokens(array: MlxArray, tokens: number): MlxArray {
88
88
  const stop = [...array.shape];
89
89
  stop[1] = tokens;
90
90
  const view = array.slice(start, stop);
91
- const exact = ops.contiguous(view);
91
+ const exact = ops.copyOf(view); // TRUE copy: see 2026-08-20 contiguous-view pin class
92
92
  view.dispose();
93
93
  return exact;
94
94
  }
@@ -225,6 +225,15 @@ export class Glm52Cache implements BatchableCache {
225
225
  throw new Error("native MTP cache cannot contain target DSA state");
226
226
  }
227
227
 
228
+ signature(): string {
229
+ if (this.role === "mtp") return "kv:mla:mtp";
230
+ return this.dsa ? "kv:mla:target:dsa" : "kv:mla:target";
231
+ }
232
+
233
+ bytesPerToken(): number {
234
+ return (this.kvLoraRank + this.ropeHeadDim + (this.dsa?.headDim ?? 0)) * 4;
235
+ }
236
+
228
237
  /** Append one equal-width row block without expanding it to per-head K/V. */
229
238
  append(latent: MlxArray, rope: MlxArray, dsa: MlxArray | null = null): void {
230
239
  const latentShape = latent.shape;
@@ -620,15 +629,16 @@ export class Glm52Cache implements BatchableCache {
620
629
  const nextWidth = this.offset - removablePad;
621
630
  const indices = ops.fromInt32([...keep], [keep.length]);
622
631
  const filterFamily = (array: MlxArray): MlxArray => {
623
- const selected = ops.takeAxis(array, indices, 0);
624
- if (removablePad === 0) return selected;
625
- const view = selected.slice(
626
- [0, removablePad, 0],
627
- [keep.length, this.offset, array.shape[2]!],
628
- );
629
- const exact = ops.contiguous(view);
632
+ const trimmed = removablePad === 0
633
+ ? array
634
+ : array.slice(
635
+ [0, removablePad, 0],
636
+ [this.batchSize!, this.offset, array.shape[2]!],
637
+ );
638
+ const selected = ops.takeAxis(trimmed, indices, 0);
639
+ const exact = ops.copyOf(selected); // TRUE copy: see 2026-08-20 contiguous-view pin class
630
640
  selected.dispose();
631
- view.dispose();
641
+ if (trimmed !== array) trimmed.dispose();
632
642
  return exact;
633
643
  };
634
644
 
@@ -4,6 +4,7 @@ import {
4
4
  type Glm52Config,
5
5
  } from "./glm52-config";
6
6
  import { ColibriGlm52Container } from "./glm52-container";
7
+ import type { MemoryPlan } from "../memory-plan";
7
8
 
8
9
  export const GLM52_G5_GIB = 1024 ** 3;
9
10
  export const GLM52_G5_DEFAULT_MACHINE_BYTES = 32 * GLM52_G5_GIB;
@@ -73,8 +74,9 @@ export interface Glm52MemoryLineItems {
73
74
  readonly safetyMarginBytes: number;
74
75
  }
75
76
 
76
- export interface Glm52MemoryPlan {
77
+ export interface Glm52MemoryPlan extends MemoryPlan {
77
78
  readonly schemaVersion: 1;
79
+ readonly strategy: "glm52-colibri";
78
80
  readonly preset: "g5-32gb-quality";
79
81
  readonly machineBytes: number;
80
82
  readonly processLimitBytes: number;
@@ -374,11 +376,14 @@ export function planGlm52Memory(
374
376
 
375
377
  return {
376
378
  schemaVersion: 1,
379
+ strategy: "glm52-colibri",
380
+ fits: true,
377
381
  preset: "g5-32gb-quality",
378
382
  machineBytes,
379
383
  processLimitBytes,
380
384
  osReserveBytes,
381
385
  contextTokens,
386
+ maxSafeContext: contextTokens,
382
387
  maxGenerationTokens,
383
388
  batchSize,
384
389
  enableMtp,
@@ -392,6 +397,28 @@ export function planGlm52Memory(
392
397
  mtpResidentSlots,
393
398
  mtpTotalSlots,
394
399
  lineItems,
400
+ weightsBytes: sum("GLM planned weights", [
401
+ lineItems.residentWeightsBytes,
402
+ lineItems.mainExpertSlabBytes,
403
+ lineItems.mtpExpertSlabBytes,
404
+ ]),
405
+ kvBytes: sum("GLM planned KV", [
406
+ lineItems.targetKvBytes,
407
+ lineItems.mtpKvBytes,
408
+ ]),
409
+ transientBytes: sum("GLM planned transient", [
410
+ lineItems.reconstructedKvTransientBytes,
411
+ lineItems.verifyBatchTransientBytes,
412
+ ]),
413
+ reserveBytes: sum("GLM planned reserves", [
414
+ lineItems.allocatorReserveBytes,
415
+ lineItems.bunNativeReserveBytes,
416
+ lineItems.safetyMarginBytes,
417
+ ]),
418
+ totalBytes: plannedProcessBytes,
419
+ usableBytes: processLimitBytes,
420
+ predictedDecodeTps: null,
421
+ allocatorLimitBytes: lineItems.allocatorReserveBytes,
395
422
  runtimeReserveBytes,
396
423
  plannedProcessBytes,
397
424
  processHeadroomBytes: processLimitBytes - plannedProcessBytes,
@@ -24,6 +24,7 @@ import {
24
24
  type Mask,
25
25
  } from "./gemma4-base";
26
26
  import { CompiledFunction } from "../mlx/compile";
27
+ import { runtimeValue } from "../runtime-config";
27
28
 
28
29
  /** Verbatim port of mlx_lm/models/activations.py:
29
30
  * `@partial(mx.compile, shapeless=True) def swiglu(gate, x): return nn.silu(gate) * x`
@@ -207,7 +208,7 @@ export class LlamaMLP {
207
208
  // Plain sigmoid+mul+mul otherwise (M>1 prefill/training, or inside a compile
208
209
  // trace) — identical math either way. MLX_BUN_COMPILED_SWIGLU=0 disables.
209
210
  let hidden: MlxArray;
210
- if (M === 1 && !isCompiledTrace() && process.env.MLX_BUN_COMPILED_SWIGLU !== "0") {
211
+ if (M === 1 && !isCompiledTrace() && runtimeValue("MLX_BUN_COMPILED_SWIGLU") !== "0") {
211
212
  hidden = compiledSwiglu(gate, up);
212
213
  gate.dispose();
213
214
  up.dispose();
@@ -0,0 +1,433 @@
1
+ import type { ModelConfig } from "../config";
2
+ import { configFingerprint } from "./fingerprint";
3
+ import {
4
+ isDiffusionGemmaConfig,
5
+ isGlm52Config,
6
+ isMiniCPM5Config,
7
+ isQwen35Config,
8
+ isQwen3Config,
9
+ isQwen3MoeConfig,
10
+ } from "./support";
11
+ import { GENERATED } from "./generated";
12
+ import { GENERIC_MODEL_TYPES, genericArgsFor, remapModelType } from "./universal/archs";
13
+
14
+ export const ENGINE_CAPABILITIES = Object.freeze([
15
+ "autoregressive",
16
+ "colibri-container",
17
+ "diffusion",
18
+ "diffusion-gemma-graph",
19
+ "gemma4-graph",
20
+ "generated-graph",
21
+ "glm5.2-graph",
22
+ "minicpm5-graph",
23
+ "mixed-precision-kv",
24
+ "native-mtp",
25
+ "qwen3-graph",
26
+ "qwen3-moe-graph",
27
+ "qwen3.5-graph",
28
+ "recurrent-state",
29
+ "safetensors",
30
+ "streamed-experts",
31
+ "universal-dense-graph",
32
+ "vision-sidecar",
33
+ ] as const);
34
+
35
+ export type EngineCapability = typeof ENGINE_CAPABILITIES[number];
36
+ export type FidelityTier = "l1" | "l2" | "l3";
37
+
38
+ export type FidelityTarget =
39
+ | Readonly<{ tier: "l1"; oracle: "mlx-lm"; claim: "bit-exact" }>
40
+ | Readonly<{ tier: "l2"; oracle: "mlx-optiq"; claim: "bit-exact" }>
41
+ | Readonly<{ tier: "l3"; oracle: null; claim: "measured" }>;
42
+
43
+ export type ModelLoader = "safetensors" | "colibri";
44
+ export type ModelGraph =
45
+ | "gemma4"
46
+ | "minicpm5"
47
+ | "qwen3.5"
48
+ | "qwen3"
49
+ | "qwen3-moe"
50
+ | "diffusion-gemma"
51
+ | "glm5.2"
52
+ | "universal-dense";
53
+ export type GenerationLoop = "autoregressive" | "diffusion";
54
+ export type ModelSpecialization = "artifact" | "dedicated" | "generated" | "generic";
55
+
56
+ export interface ModelExecutionComposition {
57
+ readonly loader: ModelLoader;
58
+ readonly graph: ModelGraph;
59
+ readonly loop: GenerationLoop;
60
+ readonly specialization: ModelSpecialization;
61
+ }
62
+
63
+ /** A profile declares construction only. Request methods such as MTP, KV
64
+ * schemes, adapters, grammar, and sampling are resolved independently and
65
+ * cannot be rewritten by profile selection. */
66
+ export interface ModelProfile {
67
+ readonly id: string;
68
+ /** Exact external identity. Family profiles omit this field. */
69
+ readonly artifactFingerprint?: string;
70
+ /** Structural guard for an exact artifact declaration. */
71
+ readonly configFingerprint?: string;
72
+ readonly fidelity: FidelityTarget;
73
+ readonly requiredCapabilities: readonly EngineCapability[];
74
+ readonly execution: ModelExecutionComposition;
75
+ }
76
+
77
+ export interface ArtifactModelProfile extends ModelProfile {
78
+ readonly artifactFingerprint: string;
79
+ readonly configFingerprint: string;
80
+ }
81
+
82
+ export interface ModelArtifactIdentity {
83
+ readonly fingerprint: string | null;
84
+ readonly configFingerprint: string;
85
+ }
86
+
87
+ export interface ResolvedModelProfile {
88
+ readonly profile: ModelProfile;
89
+ readonly artifact: ModelArtifactIdentity;
90
+ readonly exactArtifact: boolean;
91
+ }
92
+
93
+ export interface ResolveModelProfileOptions {
94
+ /** Explicit identity for a non-Hugging-Face artifact. The caller owns its
95
+ * provenance; mlx-bun never mistakes a local path for a content identity. */
96
+ readonly artifactFingerprint?: string | null;
97
+ /** Additional exact declarations composed with the shipped profiles. */
98
+ readonly artifactProfiles?: readonly ArtifactModelProfile[];
99
+ readonly engineCapabilities?: readonly EngineCapability[];
100
+ }
101
+
102
+ const L1: FidelityTarget = Object.freeze({
103
+ tier: "l1", oracle: "mlx-lm", claim: "bit-exact",
104
+ });
105
+ const L2: FidelityTarget = Object.freeze({
106
+ tier: "l2", oracle: "mlx-optiq", claim: "bit-exact",
107
+ });
108
+ const L3: FidelityTarget = Object.freeze({
109
+ tier: "l3", oracle: null, claim: "measured",
110
+ });
111
+
112
+ function freezeProfile<T extends ModelProfile>(profile: T): T {
113
+ Object.freeze(profile.fidelity);
114
+ Object.freeze(profile.requiredCapabilities);
115
+ Object.freeze(profile.execution);
116
+ return Object.freeze(profile);
117
+ }
118
+
119
+ function snapshotProfile(profile: ModelProfile): ModelProfile {
120
+ return freezeProfile({
121
+ ...profile,
122
+ fidelity: { ...profile.fidelity },
123
+ requiredCapabilities: [...profile.requiredCapabilities],
124
+ execution: { ...profile.execution },
125
+ });
126
+ }
127
+
128
+ /** Exact artifact declarations already backed by this repository's parity or
129
+ * measured evidence. A new revision gets the family profile until it earns a
130
+ * declaration of its own. */
131
+ export const BUILTIN_ARTIFACT_PROFILES: readonly ArtifactModelProfile[] = Object.freeze([
132
+ freezeProfile({
133
+ id: "qwen3.8-27b-optiq-4bit",
134
+ artifactFingerprint:
135
+ "hf:mlx-community/Qwen3.8-27B-OptiQ-4bit@b04599de95d7a9bfbd7f208d347c0f10d9432a42",
136
+ configFingerprint: "50117975cb405944",
137
+ fidelity: L1,
138
+ requiredCapabilities: Object.freeze([
139
+ "safetensors", "autoregressive", "qwen3.5-graph", "recurrent-state", "vision-sidecar",
140
+ ]),
141
+ execution: Object.freeze({
142
+ loader: "safetensors",
143
+ graph: "qwen3.5",
144
+ loop: "autoregressive",
145
+ specialization: "artifact",
146
+ }),
147
+ }),
148
+ freezeProfile({
149
+ id: "glm5.2-colibri-int4-int8-mtp",
150
+ artifactFingerprint:
151
+ "hf:mateogrgic/GLM-5.2-colibri-int4-with-int8-mtp@3cc8db99b1b13fc79325d987ba3c1c430766b3b8",
152
+ configFingerprint: "8b49d0941e18f1b9",
153
+ fidelity: L3,
154
+ requiredCapabilities: Object.freeze([
155
+ "colibri-container", "autoregressive", "glm5.2-graph", "streamed-experts",
156
+ ]),
157
+ execution: Object.freeze({
158
+ loader: "colibri",
159
+ graph: "glm5.2",
160
+ loop: "autoregressive",
161
+ specialization: "artifact",
162
+ }),
163
+ }),
164
+ ]);
165
+
166
+ const FAMILY_PROFILES = {
167
+ gemma4Generated: freezeProfile({
168
+ id: "gemma4-generated",
169
+ fidelity: L1,
170
+ requiredCapabilities: ["safetensors", "autoregressive", "gemma4-graph", "generated-graph"],
171
+ execution: {
172
+ loader: "safetensors", graph: "gemma4", loop: "autoregressive", specialization: "generated",
173
+ },
174
+ }),
175
+ gemma4: freezeProfile({
176
+ id: "gemma4-dedicated",
177
+ fidelity: L1,
178
+ requiredCapabilities: ["safetensors", "autoregressive", "gemma4-graph"],
179
+ execution: {
180
+ loader: "safetensors", graph: "gemma4", loop: "autoregressive", specialization: "dedicated",
181
+ },
182
+ }),
183
+ minicpm5: freezeProfile({
184
+ id: "minicpm5-dedicated",
185
+ fidelity: L1,
186
+ requiredCapabilities: ["safetensors", "autoregressive", "minicpm5-graph"],
187
+ execution: {
188
+ loader: "safetensors", graph: "minicpm5", loop: "autoregressive", specialization: "dedicated",
189
+ },
190
+ }),
191
+ qwen35: freezeProfile({
192
+ id: "qwen3.5-dedicated",
193
+ fidelity: L1,
194
+ requiredCapabilities: ["safetensors", "autoregressive", "qwen3.5-graph", "recurrent-state"],
195
+ execution: {
196
+ loader: "safetensors", graph: "qwen3.5", loop: "autoregressive", specialization: "dedicated",
197
+ },
198
+ }),
199
+ qwen3: freezeProfile({
200
+ id: "qwen3-dedicated",
201
+ fidelity: L1,
202
+ requiredCapabilities: ["safetensors", "autoregressive", "qwen3-graph"],
203
+ execution: {
204
+ loader: "safetensors", graph: "qwen3", loop: "autoregressive", specialization: "dedicated",
205
+ },
206
+ }),
207
+ qwen3Moe: freezeProfile({
208
+ id: "qwen3-moe-dedicated",
209
+ fidelity: L1,
210
+ requiredCapabilities: ["safetensors", "autoregressive", "qwen3-moe-graph"],
211
+ execution: {
212
+ loader: "safetensors", graph: "qwen3-moe", loop: "autoregressive", specialization: "dedicated",
213
+ },
214
+ }),
215
+ diffusionGemma: freezeProfile({
216
+ id: "diffusion-gemma-dedicated",
217
+ fidelity: L2,
218
+ requiredCapabilities: ["safetensors", "diffusion", "diffusion-gemma-graph"],
219
+ execution: {
220
+ loader: "safetensors", graph: "diffusion-gemma", loop: "diffusion", specialization: "dedicated",
221
+ },
222
+ }),
223
+ glm52: freezeProfile({
224
+ id: "glm5.2-colibri",
225
+ fidelity: L3,
226
+ requiredCapabilities: ["colibri-container", "autoregressive", "glm5.2-graph", "streamed-experts"],
227
+ execution: {
228
+ loader: "colibri", graph: "glm5.2", loop: "autoregressive", specialization: "dedicated",
229
+ },
230
+ }),
231
+ universal: freezeProfile({
232
+ id: "universal-dense",
233
+ fidelity: L1,
234
+ requiredCapabilities: ["safetensors", "autoregressive", "universal-dense-graph"],
235
+ execution: {
236
+ loader: "safetensors", graph: "universal-dense", loop: "autoregressive", specialization: "generic",
237
+ },
238
+ }),
239
+ } satisfies Record<string, ModelProfile>;
240
+
241
+ /** Return a stable external fingerprint only when the directory is an exact HF
242
+ * snapshot revision. Relocation does not change the identity. Arbitrary local
243
+ * directories and mutable aliases intentionally return null. */
244
+ export function externalArtifactFingerprint(modelDir: string): string | null {
245
+ const match = modelDir.replaceAll("\\", "/").match(
246
+ /(?:^|\/)models--([^/]+)\/snapshots\/([0-9a-f]{40,64})(?:\/|$)/i,
247
+ );
248
+ if (!match) return null;
249
+ const encodedRepo = match[1]!;
250
+ const separator = encodedRepo.indexOf("--");
251
+ if (separator <= 0 || separator === encodedRepo.length - 2) return null;
252
+ const repo = `${encodedRepo.slice(0, separator)}/${encodedRepo.slice(separator + 2)}`;
253
+ return `hf:${repo}@${match[2]!.toLowerCase()}`;
254
+ }
255
+
256
+ function familyProfile(config: ModelConfig, fingerprint: string): ModelProfile {
257
+ if (isGlm52Config(config)) return FAMILY_PROFILES.glm52;
258
+ if (isDiffusionGemmaConfig(config)) return FAMILY_PROFILES.diffusionGemma;
259
+ if (isMiniCPM5Config(config)) return FAMILY_PROFILES.minicpm5;
260
+ if (isQwen35Config(config)) return FAMILY_PROFILES.qwen35;
261
+ if (isQwen3MoeConfig(config)) return FAMILY_PROFILES.qwen3Moe;
262
+ if (isQwen3Config(config)) return FAMILY_PROFILES.qwen3;
263
+ if (config.modelType.startsWith("gemma4"))
264
+ return GENERATED.has(fingerprint) ? FAMILY_PROFILES.gemma4Generated : FAMILY_PROFILES.gemma4;
265
+ if (genericArgsFor(config)) return FAMILY_PROFILES.universal;
266
+
267
+ const arch = remapModelType(config.modelType);
268
+ throw new Error(
269
+ `unsupported model_type "${config.modelType}"` +
270
+ (arch !== config.modelType ? ` (mlx-lm remaps it to "${arch}")` : "") +
271
+ ` — targeted: gemma4*, diffusion_gemma, glm_moe_dsa, qwen3_5, qwen3, qwen3_moe, MiniCPM5;` +
272
+ ` generic (Tier-0): ${[...GENERIC_MODEL_TYPES].sort().join(", ")}`,
273
+ );
274
+ }
275
+
276
+ interface GraphMetadata {
277
+ readonly accepts: (config: ModelConfig) => boolean;
278
+ readonly capabilities: readonly EngineCapability[];
279
+ }
280
+
281
+ const GRAPH_METADATA: Readonly<Record<ModelGraph, GraphMetadata>> = Object.freeze({
282
+ "gemma4": {
283
+ accepts: (config) => config.modelType.startsWith("gemma4"),
284
+ capabilities: ["gemma4-graph"],
285
+ },
286
+ "minicpm5": { accepts: isMiniCPM5Config, capabilities: ["minicpm5-graph"] },
287
+ "qwen3.5": {
288
+ accepts: isQwen35Config,
289
+ capabilities: ["qwen3.5-graph", "recurrent-state"],
290
+ },
291
+ "qwen3": { accepts: isQwen3Config, capabilities: ["qwen3-graph"] },
292
+ "qwen3-moe": { accepts: isQwen3MoeConfig, capabilities: ["qwen3-moe-graph"] },
293
+ "diffusion-gemma": {
294
+ accepts: isDiffusionGemmaConfig,
295
+ capabilities: ["diffusion-gemma-graph"],
296
+ },
297
+ "glm5.2": {
298
+ accepts: isGlm52Config,
299
+ capabilities: ["glm5.2-graph", "streamed-experts"],
300
+ },
301
+ "universal-dense": {
302
+ accepts: (config) => {
303
+ try { return genericArgsFor(config) !== null; } catch { return false; }
304
+ },
305
+ capabilities: ["universal-dense-graph"],
306
+ },
307
+ });
308
+
309
+ function graphAccepts(profile: ModelProfile, config: ModelConfig): boolean {
310
+ return GRAPH_METADATA[profile.execution.graph].accepts(config);
311
+ }
312
+
313
+ function executionCapabilities(profile: ModelProfile): EngineCapability[] {
314
+ const required: EngineCapability[] = [profile.execution.loader === "colibri"
315
+ ? "colibri-container"
316
+ : "safetensors"];
317
+ if (profile.execution.loop === "diffusion") required.push("diffusion");
318
+ else required.push("autoregressive");
319
+ required.push(...GRAPH_METADATA[profile.execution.graph].capabilities);
320
+ if (profile.execution.specialization === "generated") required.push("generated-graph");
321
+ return required;
322
+ }
323
+
324
+ function validateProfile(profile: ModelProfile): void {
325
+ if (!profile.id) throw new Error("model profile id must not be empty");
326
+ if ((profile.artifactFingerprint === undefined) !== (profile.configFingerprint === undefined))
327
+ throw new Error(
328
+ `model profile ${profile.id} must declare artifactFingerprint and configFingerprint together`,
329
+ );
330
+ const fidelityOk =
331
+ (profile.fidelity.tier === "l1" && profile.fidelity.oracle === "mlx-lm" &&
332
+ profile.fidelity.claim === "bit-exact") ||
333
+ (profile.fidelity.tier === "l2" && profile.fidelity.oracle === "mlx-optiq" &&
334
+ profile.fidelity.claim === "bit-exact") ||
335
+ (profile.fidelity.tier === "l3" && profile.fidelity.oracle === null &&
336
+ profile.fidelity.claim === "measured");
337
+ if (!fidelityOk) throw new Error(`model profile ${profile.id} has an invalid fidelity contract`);
338
+ const expectedLoader = profile.execution.graph === "glm5.2" ? "colibri" : "safetensors";
339
+ const expectedLoop = profile.execution.graph === "diffusion-gemma"
340
+ ? "diffusion"
341
+ : "autoregressive";
342
+ if (profile.execution.loader !== expectedLoader || profile.execution.loop !== expectedLoop)
343
+ throw new Error(
344
+ `model profile ${profile.id} has an invalid execution composition for ` +
345
+ `${profile.execution.graph}`,
346
+ );
347
+ const declared = new Set(profile.requiredCapabilities);
348
+ const underdeclared = executionCapabilities(profile).filter((capability) => !declared.has(capability));
349
+ if (underdeclared.length)
350
+ throw new Error(
351
+ `model profile ${profile.id} does not declare execution capabilities: ` +
352
+ `${underdeclared.join(", ")}`,
353
+ );
354
+ }
355
+
356
+ export function resolveModelProfile(
357
+ config: ModelConfig,
358
+ options: ResolveModelProfileOptions = {},
359
+ ): ResolvedModelProfile {
360
+ const fingerprint = configFingerprint(config);
361
+ const artifactFingerprint = options.artifactFingerprint === undefined
362
+ ? externalArtifactFingerprint(config.modelDir)
363
+ : options.artifactFingerprint;
364
+ const artifactProfiles = options.artifactProfiles
365
+ ? [...BUILTIN_ARTIFACT_PROFILES, ...options.artifactProfiles]
366
+ : BUILTIN_ARTIFACT_PROFILES;
367
+ const capabilities = new Set(options.engineCapabilities ?? ENGINE_CAPABILITIES);
368
+
369
+ let profile: ModelProfile | undefined;
370
+ let exactArtifact = false;
371
+ if (artifactFingerprint) {
372
+ const matches = artifactProfiles.filter((entry) => {
373
+ validateProfile(entry);
374
+ return entry.artifactFingerprint === artifactFingerprint;
375
+ });
376
+ if (matches.length > 1)
377
+ throw new Error(`multiple model profiles declare artifact ${artifactFingerprint}`);
378
+ profile = matches[0];
379
+ if (profile) {
380
+ exactArtifact = true;
381
+ if (profile.configFingerprint !== fingerprint)
382
+ throw new Error(
383
+ `model profile ${profile.id} matched artifact ${artifactFingerprint}, but config fingerprint ` +
384
+ `${fingerprint} != declared ${profile.configFingerprint}; refusing to fall back`,
385
+ );
386
+ if (!graphAccepts(profile, config))
387
+ throw new Error(
388
+ `model profile ${profile.id} selects ${profile.execution.graph}, which is incompatible with ` +
389
+ `${config.modelType}; refusing to fall back`,
390
+ );
391
+ }
392
+ }
393
+ profile ??= familyProfile(config, fingerprint);
394
+ validateProfile(profile);
395
+ profile = snapshotProfile(profile);
396
+
397
+ const missing = profile.requiredCapabilities.filter((capability) => !capabilities.has(capability));
398
+ if (missing.length)
399
+ throw new Error(
400
+ `model profile ${profile.id} requires missing engine capabilities: ${missing.join(", ")}; ` +
401
+ `refusing to fall back`,
402
+ );
403
+
404
+ return Object.freeze({
405
+ profile,
406
+ artifact: Object.freeze({ fingerprint: artifactFingerprint, configFingerprint: fingerprint }),
407
+ exactArtifact,
408
+ });
409
+ }
410
+
411
+ /** Guard the factory seam when a caller passes a previously resolved profile. */
412
+ export function assertResolvedModelProfile(
413
+ config: ModelConfig,
414
+ resolved: ResolvedModelProfile,
415
+ ): void {
416
+ const fingerprint = configFingerprint(config);
417
+ if (resolved.artifact.configFingerprint !== fingerprint)
418
+ throw new Error(
419
+ `model profile ${resolved.profile.id} was resolved for config ` +
420
+ `${resolved.artifact.configFingerprint}, not ${fingerprint}`,
421
+ );
422
+ if (!graphAccepts(resolved.profile, config))
423
+ throw new Error(
424
+ `model profile ${resolved.profile.id} selects ${resolved.profile.execution.graph}, ` +
425
+ `which is incompatible with ${config.modelType}`,
426
+ );
427
+ if (resolved.exactArtifact &&
428
+ resolved.profile.artifactFingerprint !== resolved.artifact.fingerprint)
429
+ throw new Error(
430
+ `model profile ${resolved.profile.id} does not match artifact ` +
431
+ `${resolved.artifact.fingerprint ?? "<unidentified>"}`,
432
+ );
433
+ }