@simulatte/doppler 0.1.6 → 0.1.8

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 (355) hide show
  1. package/CHANGELOG.md +145 -0
  2. package/README.md +16 -23
  3. package/package.json +30 -32
  4. package/src/adapters/adapter-registry.js +12 -1
  5. package/src/adapters/lora-loader.js +23 -6
  6. package/src/bridge/extension-client.d.ts +5 -0
  7. package/src/bridge/extension-client.js +40 -0
  8. package/src/bridge/index.d.ts +2 -1
  9. package/src/bridge/index.js +6 -4
  10. package/src/browser/browser-converter.js +31 -1
  11. package/src/browser/file-picker.js +6 -0
  12. package/src/browser/safetensors-parser-browser.js +84 -1
  13. package/src/browser/shard-io-browser.js +2 -2
  14. package/src/browser/tensor-source-download.js +8 -2
  15. package/src/browser/tensor-source-http.d.ts +1 -0
  16. package/src/browser/tensor-source-http.js +5 -1
  17. package/src/client/doppler-api.browser.js +20 -4
  18. package/src/client/doppler-api.js +19 -3
  19. package/src/client/doppler-provider/generation.js +12 -0
  20. package/src/client/doppler-provider/model-manager.d.ts +10 -0
  21. package/src/client/doppler-provider/model-manager.js +91 -19
  22. package/src/client/doppler-provider/source-runtime.d.ts +2 -1
  23. package/src/client/doppler-provider/source-runtime.js +132 -13
  24. package/src/client/doppler-registry.json +5 -20
  25. package/src/config/backward-registry-loader.js +17 -2
  26. package/src/config/execution-v0-contract-check.js +113 -15
  27. package/src/config/kernel-path-contract-check.js +57 -29
  28. package/src/config/kernel-path-loader.d.ts +5 -0
  29. package/src/config/kernel-path-loader.js +18 -36
  30. package/src/config/kernels/kernel-ref-digests.js +1 -1
  31. package/src/config/kernels/registry.js +14 -1
  32. package/src/config/kernels/registry.json +81 -5
  33. package/src/config/loader.d.ts +1 -1
  34. package/src/config/loader.js +15 -2
  35. package/src/config/merge-contract-check.js +66 -4
  36. package/src/config/merge-helpers.js +128 -7
  37. package/src/config/merge.d.ts +1 -0
  38. package/src/config/merge.js +10 -0
  39. package/src/config/param-validator.js +47 -2
  40. package/src/config/presets/kernel-paths/{gemma2-q4k-dequant-f32a.json → gemma2-q4k-dequant-f32a-nosubgroups.json} +3 -3
  41. package/src/config/presets/kernel-paths/gemma3-f16-fused-f32a-online-streamingprefill.json +223 -0
  42. package/src/config/presets/kernel-paths/{gemma3-q4k-dequant-f32a.json → gemma3-q4k-dequant-f32a-nosubgroups.json} +3 -3
  43. package/src/config/presets/kernel-paths/gemma3-q4k-dequant-f32w-f32a-online.json +56 -0
  44. package/src/config/presets/kernel-paths/lfm2-q4k-dequant-f32a-nosubgroups.json +61 -0
  45. package/src/config/presets/kernel-paths/registry.json +43 -8
  46. package/src/config/presets/models/gemma2.json +3 -2
  47. package/src/config/presets/models/gemma3.json +2 -0
  48. package/src/config/presets/models/qwen3.json +4 -3
  49. package/src/config/presets/models/qwen3_5.json +16 -0
  50. package/src/config/presets/runtime/experiments/bench/gemma3-bench-q4k.json +1 -1
  51. package/src/config/presets/runtime/experiments/debug/gemma3-debug-q4k.json +1 -1
  52. package/src/config/presets/runtime/experiments/verify/gemma3-verify.json +1 -1
  53. package/src/config/presets/runtime/kernels/dequant-f16-q4k.json +6 -13
  54. package/src/config/presets/runtime/kernels/dequant-f32-q4k.json +6 -13
  55. package/src/config/presets/runtime/kernels/embeddinggemma-q4k-dequant-f32a.json +37 -0
  56. package/src/config/presets/runtime/kernels/fused-q4k.json +6 -13
  57. package/src/config/presets/runtime/kernels/gemma2-q4k-dequant-f16a.json +33 -0
  58. package/src/config/presets/runtime/kernels/gemma2-q4k-dequant-f32a-nosubgroups.json +33 -0
  59. package/src/config/presets/runtime/kernels/gemma2-q4k-fused-f32a.json +33 -0
  60. package/src/config/presets/runtime/kernels/safe-q4k.json +6 -13
  61. package/src/config/presets/runtime/model/qwen3-5-layer-probe.json +52 -0
  62. package/src/config/presets/runtime/model/qwen3-5-linear-attn-debug.json +90 -0
  63. package/src/config/presets/runtime/platform/metal-apple-q4k.json +1 -1
  64. package/src/config/runtime.js +6 -1
  65. package/src/config/schema/conversion.schema.d.ts +1 -0
  66. package/src/config/schema/debug.schema.d.ts +5 -0
  67. package/src/config/schema/doppler.schema.js +16 -21
  68. package/src/config/schema/inference-defaults.schema.js +3 -3
  69. package/src/config/schema/kernel-path.schema.d.ts +5 -1
  70. package/src/config/schema/kernel-thresholds.schema.js +12 -4
  71. package/src/config/schema/manifest.schema.d.ts +3 -2
  72. package/src/config/schema/manifest.schema.js +17 -4
  73. package/src/config/schema/storage.schema.js +1 -1
  74. package/src/config/training-defaults.js +30 -22
  75. package/src/converter/conversion-plan.js +104 -11
  76. package/src/converter/core.d.ts +7 -0
  77. package/src/converter/core.js +16 -9
  78. package/src/converter/execution-v0-manifest.js +4 -1
  79. package/src/converter/index.d.ts +1 -0
  80. package/src/converter/index.js +1 -0
  81. package/src/converter/manifest-inference.js +50 -29
  82. package/src/converter/parsers/diffusion.js +0 -3
  83. package/src/converter/parsers/transformer.js +4 -0
  84. package/src/converter/quantization-info.js +40 -16
  85. package/src/converter/quantizer.js +19 -12
  86. package/src/converter/rope-config.js +8 -6
  87. package/src/converter/shard-packer.d.ts +1 -1
  88. package/src/converter/shard-packer.js +4 -1
  89. package/src/converter/tokenizer-utils.d.ts +1 -0
  90. package/src/converter/tokenizer-utils.js +4 -1
  91. package/src/debug/config.js +123 -11
  92. package/src/debug/reference/hf_qwen35_linear_attn_debug.py +268 -0
  93. package/src/debug/signals.js +7 -1
  94. package/src/debug/tensor.d.ts +2 -0
  95. package/src/debug/tensor.js +13 -2
  96. package/src/distribution/p2p-control-plane.js +52 -12
  97. package/src/distribution/p2p-observability.js +43 -7
  98. package/src/distribution/p2p-webrtc-browser.js +20 -0
  99. package/src/distribution/shard-delivery.js +83 -27
  100. package/src/formats/gguf/types.js +33 -16
  101. package/src/formats/rdrr/groups.d.ts +12 -4
  102. package/src/formats/rdrr/groups.js +3 -6
  103. package/src/formats/rdrr/parsing.d.ts +4 -0
  104. package/src/formats/rdrr/parsing.js +53 -3
  105. package/src/formats/rdrr/types.d.ts +2 -1
  106. package/src/gpu/command-recorder.js +86 -61
  107. package/src/gpu/device.d.ts +1 -0
  108. package/src/gpu/device.js +73 -19
  109. package/src/gpu/kernel-tuner/benchmarks.js +326 -316
  110. package/src/gpu/kernel-tuner/cache.js +71 -4
  111. package/src/gpu/kernel-tuner/tuner.js +22 -4
  112. package/src/gpu/kernels/attention.js +15 -34
  113. package/src/gpu/kernels/backward/adam.js +62 -58
  114. package/src/gpu/kernels/backward/attention_backward.js +257 -169
  115. package/src/gpu/kernels/backward/conv2d_backward.js +14 -1
  116. package/src/gpu/kernels/cast.js +191 -149
  117. package/src/gpu/kernels/check-stop.js +33 -44
  118. package/src/gpu/kernels/conv2d.js +27 -17
  119. package/src/gpu/kernels/cross_entropy_loss.js +21 -15
  120. package/src/gpu/kernels/depthwise_conv2d.js +36 -26
  121. package/src/gpu/kernels/dequant.js +178 -126
  122. package/src/gpu/kernels/energy.d.ts +3 -21
  123. package/src/gpu/kernels/energy.js +111 -88
  124. package/src/gpu/kernels/feature-check.js +1 -1
  125. package/src/gpu/kernels/fused_ffn.js +84 -65
  126. package/src/gpu/kernels/fused_matmul_residual.js +56 -33
  127. package/src/gpu/kernels/fused_matmul_rmsnorm.js +62 -45
  128. package/src/gpu/kernels/gather.js +33 -15
  129. package/src/gpu/kernels/gelu.js +19 -11
  130. package/src/gpu/kernels/grouped_pointwise_conv2d.js +33 -23
  131. package/src/gpu/kernels/groupnorm.js +34 -23
  132. package/src/gpu/kernels/index.d.ts +8 -0
  133. package/src/gpu/kernels/index.js +6 -0
  134. package/src/gpu/kernels/kv-quantize.js +5 -2
  135. package/src/gpu/kernels/layernorm.js +35 -19
  136. package/src/gpu/kernels/logit-merge.js +5 -3
  137. package/src/gpu/kernels/matmul-selection.js +47 -4
  138. package/src/gpu/kernels/matmul.d.ts +2 -0
  139. package/src/gpu/kernels/matmul.js +59 -40
  140. package/src/gpu/kernels/modulate.js +23 -15
  141. package/src/gpu/kernels/moe.js +221 -175
  142. package/src/gpu/kernels/pixel_shuffle.js +22 -14
  143. package/src/gpu/kernels/relu.js +18 -10
  144. package/src/gpu/kernels/repeat_channels.js +25 -17
  145. package/src/gpu/kernels/residual.js +37 -27
  146. package/src/gpu/kernels/rmsnorm.js +66 -43
  147. package/src/gpu/kernels/rope.js +3 -0
  148. package/src/gpu/kernels/sample.js +27 -38
  149. package/src/gpu/kernels/sana_linear_attention.js +18 -10
  150. package/src/gpu/kernels/scale.js +18 -11
  151. package/src/gpu/kernels/shader-cache.js +4 -2
  152. package/src/gpu/kernels/silu.js +120 -72
  153. package/src/gpu/kernels/softmax.js +44 -25
  154. package/src/gpu/kernels/split_qg.d.ts +50 -0
  155. package/src/gpu/kernels/split_qg.js +46 -0
  156. package/src/gpu/kernels/split_qg.wgsl +58 -0
  157. package/src/gpu/kernels/split_qg_f16.wgsl +62 -0
  158. package/src/gpu/kernels/split_qkv.js +23 -13
  159. package/src/gpu/kernels/transpose.js +18 -10
  160. package/src/gpu/kernels/transpose.wgsl +5 -3
  161. package/src/gpu/kernels/upsample2d.js +21 -13
  162. package/src/gpu/kernels/utils.js +20 -13
  163. package/src/gpu/partitioned-buffer-pool.js +10 -2
  164. package/src/gpu/perf-guards.js +2 -9
  165. package/src/gpu/profiler.js +27 -22
  166. package/src/gpu/readback-utils.d.ts +16 -0
  167. package/src/gpu/readback-utils.js +41 -0
  168. package/src/gpu/submit-tracker.js +13 -0
  169. package/src/gpu/uniform-cache.d.ts +1 -0
  170. package/src/gpu/uniform-cache.js +30 -9
  171. package/src/gpu/weight-buffer.d.ts +1 -1
  172. package/src/gpu/weight-buffer.js +1 -1
  173. package/src/hotswap/intent-bundle.js +6 -0
  174. package/src/hotswap/manifest.d.ts +10 -1
  175. package/src/hotswap/manifest.js +12 -2
  176. package/src/hotswap/runtime.js +30 -8
  177. package/src/index-browser.d.ts +44 -0
  178. package/src/index-browser.js +14 -0
  179. package/src/inference/browser-harness-contract-helpers.d.ts +5 -0
  180. package/src/inference/browser-harness-contract-helpers.js +28 -0
  181. package/src/inference/browser-harness-diffusion-energy-suites.d.ts +2 -0
  182. package/src/inference/browser-harness-diffusion-energy-suites.js +269 -0
  183. package/src/inference/browser-harness-model-helpers.d.ts +16 -0
  184. package/src/inference/browser-harness-model-helpers.js +217 -0
  185. package/src/inference/browser-harness-report-helpers.d.ts +7 -0
  186. package/src/inference/browser-harness-report-helpers.js +42 -0
  187. package/src/inference/browser-harness-runtime-helpers.d.ts +61 -0
  188. package/src/inference/browser-harness-runtime-helpers.js +415 -0
  189. package/src/inference/browser-harness-suite-helpers.d.ts +28 -0
  190. package/src/inference/browser-harness-suite-helpers.js +268 -0
  191. package/src/inference/browser-harness-text-helpers.d.ts +27 -0
  192. package/src/inference/browser-harness-text-helpers.js +788 -0
  193. package/src/inference/browser-harness.d.ts +8 -0
  194. package/src/inference/browser-harness.js +149 -1996
  195. package/src/inference/kv-cache/base.js +140 -94
  196. package/src/inference/kv-cache/tiered.js +5 -3
  197. package/src/inference/moe-router.js +88 -56
  198. package/src/inference/multi-model-network.js +5 -3
  199. package/src/inference/network-evolution.d.ts +11 -2
  200. package/src/inference/network-evolution.js +20 -21
  201. package/src/inference/pipelines/context.d.ts +3 -0
  202. package/src/inference/pipelines/context.js +142 -2
  203. package/src/inference/pipelines/diffusion/helpers.js +10 -2
  204. package/src/inference/pipelines/diffusion/pipeline.js +2 -1
  205. package/src/inference/pipelines/diffusion/sd3-transformer.js +10 -10
  206. package/src/inference/pipelines/diffusion/text-encoder-gpu.js +8 -2
  207. package/src/inference/pipelines/diffusion/vae.js +3 -7
  208. package/src/inference/pipelines/energy/pipeline.js +27 -21
  209. package/src/inference/pipelines/energy/quintel.d.ts +5 -0
  210. package/src/inference/pipelines/energy/quintel.js +11 -0
  211. package/src/inference/pipelines/energy-head/row-head-pipeline.js +17 -13
  212. package/src/inference/pipelines/structured/json-head-pipeline.js +26 -11
  213. package/src/inference/pipelines/text/attention/output-projection.d.ts +12 -0
  214. package/src/inference/pipelines/text/attention/output-projection.js +8 -0
  215. package/src/inference/pipelines/text/attention/projections.d.ts +10 -1
  216. package/src/inference/pipelines/text/attention/projections.js +192 -112
  217. package/src/inference/pipelines/text/attention/record.js +77 -14
  218. package/src/inference/pipelines/text/attention/run.js +112 -14
  219. package/src/inference/pipelines/text/config.js +17 -4
  220. package/src/inference/pipelines/text/embed.js +2 -8
  221. package/src/inference/pipelines/text/execution-plan.js +46 -23
  222. package/src/inference/pipelines/text/execution-v0-contract-helpers.d.ts +59 -0
  223. package/src/inference/pipelines/text/execution-v0-contract-helpers.js +937 -0
  224. package/src/inference/pipelines/text/execution-v0-runtime-builders.d.ts +15 -0
  225. package/src/inference/pipelines/text/execution-v0-runtime-builders.js +279 -0
  226. package/src/inference/pipelines/text/execution-v0.js +62 -1013
  227. package/src/inference/pipelines/text/generator-runtime.js +5 -0
  228. package/src/inference/pipelines/text/generator-steps.d.ts +52 -0
  229. package/src/inference/pipelines/text/generator-steps.js +340 -221
  230. package/src/inference/pipelines/text/generator.js +56 -40
  231. package/src/inference/pipelines/text/init.d.ts +13 -0
  232. package/src/inference/pipelines/text/init.js +94 -25
  233. package/src/inference/pipelines/text/kernel-path-auto-select.js +2 -0
  234. package/src/inference/pipelines/text/kernel-trace.d.ts +2 -0
  235. package/src/inference/pipelines/text/kernel-trace.js +6 -0
  236. package/src/inference/pipelines/text/layer.js +4 -9
  237. package/src/inference/pipelines/text/linear-attention.d.ts +15 -0
  238. package/src/inference/pipelines/text/linear-attention.js +113 -9
  239. package/src/inference/pipelines/text/logits/gpu.js +12 -7
  240. package/src/inference/pipelines/text/logits/index.d.ts +6 -1
  241. package/src/inference/pipelines/text/logits/index.js +13 -12
  242. package/src/inference/pipelines/text/logits/utils.d.ts +7 -0
  243. package/src/inference/pipelines/text/logits/utils.js +9 -0
  244. package/src/inference/pipelines/text/lora-apply.js +50 -32
  245. package/src/inference/pipelines/text/model-load.js +282 -104
  246. package/src/inference/pipelines/text/moe-cache.js +5 -4
  247. package/src/inference/pipelines/text/moe-cpu-gptoss.js +74 -69
  248. package/src/inference/pipelines/text/moe-cpu.js +42 -38
  249. package/src/inference/pipelines/text/moe-gpu.js +110 -86
  250. package/src/inference/pipelines/text/ops.js +90 -90
  251. package/src/inference/pipelines/text/probes.js +9 -9
  252. package/src/inference/pipelines/text/sampling.js +52 -6
  253. package/src/inference/pipelines/text/weights.js +17 -7
  254. package/src/inference/pipelines/text.js +13 -1
  255. package/src/inference/speculative.d.ts +2 -2
  256. package/src/inference/speculative.js +4 -18
  257. package/src/inference/test-harness.d.ts +1 -1
  258. package/src/inference/test-harness.js +17 -7
  259. package/src/inference/tokenizer.d.ts +0 -5
  260. package/src/inference/tokenizer.js +4 -23
  261. package/src/inference/tokenizers/bpe.js +9 -0
  262. package/src/inference/tokenizers/bundled.js +20 -0
  263. package/src/inference/tokenizers/sentencepiece.js +12 -0
  264. package/src/loader/doppler-loader.js +38 -22
  265. package/src/loader/dtype-utils.js +3 -44
  266. package/src/loader/embedding-loader.js +7 -3
  267. package/src/loader/experts/expert-cache.js +13 -6
  268. package/src/loader/experts/expert-loader.js +10 -6
  269. package/src/loader/final-weights-loader.js +10 -4
  270. package/src/loader/layer-loader.js +2 -1
  271. package/src/loader/loader-state.js +2 -2
  272. package/src/loader/memory-monitor.js +8 -0
  273. package/src/loader/multi-model-loader.d.ts +14 -0
  274. package/src/loader/multi-model-loader.js +70 -24
  275. package/src/loader/shard-cache.js +84 -14
  276. package/src/loader/shard-resolver.js +25 -3
  277. package/src/loader/tensors/tensor-loader.js +214 -144
  278. package/src/loader/tensors/tensor-reader.js +76 -19
  279. package/src/loader/weight-downcast.js +1 -1
  280. package/src/memory/buffer-pool.d.ts +9 -1
  281. package/src/memory/buffer-pool.js +109 -44
  282. package/src/memory/unified-detect.js +1 -1
  283. package/src/rules/inference/dtype.rules.json +5 -0
  284. package/src/rules/inference/kernel-path.rules.json +24 -8
  285. package/src/rules/kernels/split-qg.rules.json +6 -0
  286. package/src/rules/rule-registry.js +27 -1
  287. package/src/storage/backends/opfs-store.js +68 -24
  288. package/src/storage/downloader.js +365 -83
  289. package/src/storage/index.d.ts +3 -0
  290. package/src/storage/index.js +3 -0
  291. package/src/storage/preflight.d.ts +2 -2
  292. package/src/storage/preflight.js +24 -2
  293. package/src/storage/quickstart-downloader.js +11 -5
  294. package/src/storage/registry.js +10 -4
  295. package/src/storage/reports.js +1 -1
  296. package/src/storage/shard-manager.d.ts +15 -1
  297. package/src/storage/shard-manager.js +55 -6
  298. package/src/storage/source-artifact-store.d.ts +52 -0
  299. package/src/storage/source-artifact-store.js +234 -0
  300. package/src/tooling/command-api-constants.d.ts +9 -0
  301. package/src/tooling/command-api-constants.js +9 -0
  302. package/src/tooling/command-api-family-normalizers.d.ts +9 -0
  303. package/src/tooling/command-api-family-normalizers.js +343 -0
  304. package/src/tooling/command-api-helpers.d.ts +25 -0
  305. package/src/tooling/command-api-helpers.js +262 -0
  306. package/src/tooling/command-api.js +16 -602
  307. package/src/tooling/command-envelope.js +4 -1
  308. package/src/tooling/command-runner-shared.js +52 -18
  309. package/src/tooling/conversion-config-materializer.js +3 -5
  310. package/src/tooling/lean-execution-contract.js +150 -3
  311. package/src/tooling/node-browser-command-runner.js +161 -271
  312. package/src/tooling/node-command-runner.js +29 -3
  313. package/src/tooling/node-converter.js +30 -1
  314. package/src/tooling/node-source-runtime.d.ts +1 -1
  315. package/src/tooling/node-source-runtime.js +120 -3
  316. package/src/tooling/node-webgpu.js +24 -21
  317. package/src/tooling/opfs-cache.js +21 -4
  318. package/src/tooling/runtime-input-composition.d.ts +38 -0
  319. package/src/tooling/runtime-input-composition.js +86 -0
  320. package/src/tooling/source-runtime-bundle.d.ts +40 -5
  321. package/src/tooling/source-runtime-bundle.js +261 -34
  322. package/src/tooling/source-runtime-materializer.d.ts +6 -0
  323. package/src/tooling/source-runtime-materializer.js +93 -0
  324. package/src/training/attention-backward.js +32 -17
  325. package/src/training/autograd.js +80 -52
  326. package/src/training/checkpoint-watch.d.ts +2 -1
  327. package/src/training/checkpoint-watch.js +39 -6
  328. package/src/training/checkpoint.js +40 -11
  329. package/src/training/clip.js +2 -1
  330. package/src/training/datasets/token-batch.js +20 -8
  331. package/src/training/distillation/checkpoint-watch.js +1 -0
  332. package/src/training/distillation/student-fixture.d.ts +22 -0
  333. package/src/training/distillation/student-fixture.js +846 -0
  334. package/src/training/distillation/suite-data.d.ts +45 -0
  335. package/src/training/distillation/suite-data.js +189 -0
  336. package/src/training/lora-pipeline.js +4 -7
  337. package/src/training/lora.js +26 -12
  338. package/src/training/loss.js +5 -6
  339. package/src/training/objectives/cross_entropy.js +2 -5
  340. package/src/training/objectives/distill_kd.js +4 -8
  341. package/src/training/objectives/distill_triplet.js +4 -8
  342. package/src/training/objectives/ul_stage2_base.js +4 -8
  343. package/src/training/operator-command.js +2 -0
  344. package/src/training/optimizer.js +19 -7
  345. package/src/training/runner.js +2 -1
  346. package/src/training/suite.js +18 -978
  347. package/src/training/tensor-factory.d.ts +9 -0
  348. package/src/training/tensor-factory.js +13 -0
  349. package/src/training/trainer.js +3 -5
  350. package/src/training/ul_dataset.js +3 -5
  351. package/src/training/workloads.js +70 -79
  352. package/src/types/model.d.ts +5 -0
  353. package/src/version.js +1 -1
  354. package/tools/convert-safetensors-node.js +22 -16
  355. package/tools/doppler-cli.js +50 -26
@@ -0,0 +1,415 @@
1
+ import { parseRuntimeOverridesFromURL } from './test-harness.js';
2
+ import { getRuntimeConfig, setRuntimeConfig } from '../config/runtime.js';
3
+ import {
4
+ setActiveKernelPath,
5
+ getActiveKernelPath,
6
+ getActiveKernelPathSource,
7
+ getActiveKernelPathPolicy,
8
+ } from '../config/kernel-path-loader.js';
9
+ import { mergeRuntimeValues } from '../config/runtime-merge.js';
10
+ import { buildRuntimeContractPatch } from '../tooling/command-api.js';
11
+ import {
12
+ applyOrderedRuntimeInputs,
13
+ resolveRuntimeFromConfig,
14
+ } from '../tooling/runtime-input-composition.js';
15
+
16
+ export function parseReportTimestamp(rawTimestamp, label = 'timestamp') {
17
+ if (rawTimestamp == null) {
18
+ return null;
19
+ }
20
+
21
+ if (rawTimestamp instanceof Date) {
22
+ const timestamp = rawTimestamp.getTime();
23
+ if (!Number.isFinite(timestamp)) {
24
+ throw new Error(`Invalid ${label}: not a valid Date.`);
25
+ }
26
+ return rawTimestamp.toISOString();
27
+ }
28
+
29
+ if (typeof rawTimestamp === 'number') {
30
+ if (!Number.isFinite(rawTimestamp)) {
31
+ throw new Error(`Invalid ${label}: must be a finite epoch timestamp.`);
32
+ }
33
+ return new Date(rawTimestamp).toISOString();
34
+ }
35
+
36
+ if (typeof rawTimestamp === 'string') {
37
+ const trimmed = rawTimestamp.trim();
38
+ if (trimmed.length === 0) {
39
+ return null;
40
+ }
41
+ const numericCandidate = Number(trimmed);
42
+ if (Number.isFinite(numericCandidate)) {
43
+ return new Date(numericCandidate).toISOString();
44
+ }
45
+ const parsed = new Date(trimmed);
46
+ if (Number.isNaN(parsed.getTime())) {
47
+ throw new Error(`Invalid ${label}: expected ISO-8601 string or epoch milliseconds.`);
48
+ }
49
+ return parsed.toISOString();
50
+ }
51
+
52
+ throw new Error(`Invalid ${label}: expected Date, ISO-8601 string, epoch milliseconds, or nullish.`);
53
+ }
54
+
55
+ export function resolveReportTimestamp(rawTimestamp, label, fallbackTimestamp = null) {
56
+ const parsed = parseReportTimestamp(rawTimestamp, label);
57
+ return parsed ?? (fallbackTimestamp == null ? new Date().toISOString() : String(fallbackTimestamp));
58
+ }
59
+
60
+ export function resolveRuntime(options) {
61
+ if (options.runtime) return options.runtime;
62
+ if (options.searchParams) return parseRuntimeOverridesFromURL(options.searchParams);
63
+ const runtimeConfig = cloneRuntimeConfig(getRuntimeConfig());
64
+ const runtime = typeof globalThis.location === 'undefined'
65
+ ? parseRuntimeOverridesFromURL(new URLSearchParams())
66
+ : parseRuntimeOverridesFromURL();
67
+ if (runtimeConfig) {
68
+ runtime.runtimeConfig = runtime.runtimeConfig
69
+ ? mergeRuntimeValues(runtimeConfig, runtime.runtimeConfig)
70
+ : runtimeConfig;
71
+ }
72
+ return runtime;
73
+ }
74
+
75
+ function normalizePresetPath(value) {
76
+ const trimmed = String(value || '').replace(/^[./]+/, '');
77
+ if (!trimmed) return null;
78
+ return trimmed.endsWith('.json') ? trimmed : `${trimmed}.json`;
79
+ }
80
+
81
+ function resolvePresetBaseUrl() {
82
+ try {
83
+ return new URL('../config/presets/runtime/', import.meta.url).toString().replace(/\/$/, '');
84
+ } catch {
85
+ if (typeof globalThis.location !== 'undefined' && globalThis.location?.href) {
86
+ return new URL('/src/config/presets/runtime/', globalThis.location.href).toString().replace(/\/$/, '');
87
+ }
88
+ return '/src/config/presets/runtime';
89
+ }
90
+ }
91
+
92
+ export function cloneRuntimeConfig(runtimeConfig) {
93
+ if (!runtimeConfig) return null;
94
+ if (typeof structuredClone === 'function') {
95
+ return structuredClone(runtimeConfig);
96
+ }
97
+ return JSON.parse(JSON.stringify(runtimeConfig));
98
+ }
99
+
100
+ export function snapshotRuntimeState() {
101
+ return {
102
+ runtimeConfig: cloneRuntimeConfig(getRuntimeConfig()),
103
+ activeKernelPath: getActiveKernelPath(),
104
+ activeKernelPathSource: getActiveKernelPathSource(),
105
+ activeKernelPathPolicy: getActiveKernelPathPolicy(),
106
+ };
107
+ }
108
+
109
+ export function restoreRuntimeState(snapshot) {
110
+ if (!snapshot) {
111
+ return;
112
+ }
113
+ setRuntimeConfig(snapshot.runtimeConfig);
114
+ setActiveKernelPath(
115
+ snapshot.activeKernelPath,
116
+ snapshot.activeKernelPathSource || 'none',
117
+ snapshot.activeKernelPathPolicy ?? null
118
+ );
119
+ }
120
+
121
+ export async function runWithRuntimeIsolationForSuite(run) {
122
+ const snapshot = snapshotRuntimeState();
123
+ try {
124
+ return await run();
125
+ } finally {
126
+ restoreRuntimeState(snapshot);
127
+ }
128
+ }
129
+
130
+ export function sanitizeReportOutput(output) {
131
+ if (output == null) return null;
132
+ if (typeof output !== 'object') return output;
133
+ if (ArrayBuffer.isView(output)) {
134
+ return {
135
+ type: output.constructor?.name || 'TypedArray',
136
+ length: Number.isFinite(output.length) ? output.length : null,
137
+ };
138
+ }
139
+ if (
140
+ Number.isFinite(output?.width)
141
+ && Number.isFinite(output?.height)
142
+ && ArrayBuffer.isView(output?.pixels)
143
+ ) {
144
+ const { pixels, ...rest } = output;
145
+ return {
146
+ ...rest,
147
+ width: output.width,
148
+ height: output.height,
149
+ pixels: {
150
+ type: pixels.constructor?.name || 'TypedArray',
151
+ length: Number.isFinite(pixels.length) ? pixels.length : null,
152
+ },
153
+ };
154
+ }
155
+ return output;
156
+ }
157
+
158
+ function normalizeExtends(value) {
159
+ if (Array.isArray(value)) {
160
+ return value.map((entry) => String(entry || '').trim()).filter(Boolean);
161
+ }
162
+ if (typeof value === 'string') {
163
+ const trimmed = value.trim();
164
+ return trimmed ? [trimmed] : [];
165
+ }
166
+ return [];
167
+ }
168
+
169
+ function normalizeExtendsPath(value) {
170
+ const trimmed = String(value || '').trim();
171
+ if (!trimmed) return null;
172
+ return trimmed.endsWith('.json') ? trimmed : `${trimmed}.json`;
173
+ }
174
+
175
+ function resolveAbsoluteUrl(target, base) {
176
+ try {
177
+ if (base) {
178
+ return new URL(target, base).toString();
179
+ }
180
+ if (typeof globalThis.location !== 'undefined' && globalThis.location?.href) {
181
+ return new URL(target, globalThis.location.href).toString();
182
+ }
183
+ return new URL(target, import.meta.url).toString();
184
+ } catch {
185
+ return target;
186
+ }
187
+ }
188
+
189
+ function isAbsoluteUrl(value) {
190
+ return /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(value);
191
+ }
192
+
193
+ function joinUrl(base, path) {
194
+ if (!base) return path;
195
+ if (isAbsoluteUrl(base)) {
196
+ return new URL(path, base.endsWith('/') ? base : `${base}/`).toString();
197
+ }
198
+ const normalizedBase = base.replace(/\/$/, '');
199
+ const normalizedPath = path.replace(/^\//, '');
200
+ return `${normalizedBase}/${normalizedPath}`;
201
+ }
202
+
203
+ function resolveExtendCandidates(ref, context) {
204
+ const normalized = normalizeExtendsPath(ref);
205
+ if (!normalized) return [];
206
+ if (isAbsoluteUrl(normalized) || normalized.startsWith('/')) {
207
+ return [normalized];
208
+ }
209
+ if (normalized.startsWith('./') || normalized.startsWith('../')) {
210
+ return [resolveAbsoluteUrl(normalized, context.sourceUrl)];
211
+ }
212
+ if (normalized.includes('/')) {
213
+ return [joinUrl(context.presetBaseUrl, normalized)];
214
+ }
215
+ const candidates = [];
216
+ if (context.presetBaseUrl) {
217
+ candidates.push(joinUrl(context.presetBaseUrl, normalized));
218
+ candidates.push(joinUrl(context.presetBaseUrl, `modes/${normalized}`));
219
+ }
220
+ if (context.sourceUrl) {
221
+ const sourceDir = resolveAbsoluteUrl('./', context.sourceUrl);
222
+ candidates.push(resolveAbsoluteUrl(normalized, sourceDir));
223
+ }
224
+ return [...new Set(candidates)];
225
+ }
226
+
227
+ async function fetchRuntimeConfig(url, options = {}) {
228
+ const response = await fetch(url, { signal: options.signal });
229
+ if (!response.ok) {
230
+ const error = new Error(`Failed to load runtime config: ${response.status}`);
231
+ error.code = response.status === 404 ? 'runtime_config_not_found' : 'runtime_config_fetch_failed';
232
+ throw error;
233
+ }
234
+ return response.json();
235
+ }
236
+
237
+ async function resolveRuntimeConfigExtends(config, context) {
238
+ const runtime = resolveRuntimeFromConfig(config);
239
+ if (!runtime) {
240
+ throw new Error('Runtime config is missing runtime fields');
241
+ }
242
+
243
+ const extendsRefs = normalizeExtends(config.extends);
244
+ let mergedRuntime = null;
245
+ let mergedConfig = null;
246
+
247
+ for (const ref of extendsRefs) {
248
+ const base = await loadRuntimeConfigFromRef(ref, context);
249
+ mergedRuntime = mergedRuntime ? mergeRuntimeValues(mergedRuntime, base.runtime) : base.runtime;
250
+ mergedConfig = mergedConfig ? mergeRuntimeValues(mergedConfig, base.config) : base.config;
251
+ }
252
+
253
+ const combinedRuntime = mergedRuntime ? mergeRuntimeValues(mergedRuntime, runtime) : runtime;
254
+ const combinedConfig = mergedConfig ? mergeRuntimeValues(mergedConfig, config) : { ...config };
255
+ const resolved = { ...combinedConfig, runtime: combinedRuntime };
256
+ if (resolved.extends !== undefined) {
257
+ delete resolved.extends;
258
+ }
259
+ return { config: resolved, runtime: combinedRuntime };
260
+ }
261
+
262
+ async function loadRuntimeConfigChain(url, options = {}, stack = []) {
263
+ const presetBaseUrl = options.presetBaseUrl || options.baseUrl || resolvePresetBaseUrl();
264
+ const resolvedUrl = resolveAbsoluteUrl(url);
265
+ if (stack.includes(resolvedUrl)) {
266
+ throw new Error(`Runtime config extends cycle: ${[...stack, resolvedUrl].join(' -> ')}`);
267
+ }
268
+ const config = await fetchRuntimeConfig(resolvedUrl, options);
269
+ return resolveRuntimeConfigExtends(config, {
270
+ ...options,
271
+ sourceUrl: resolvedUrl,
272
+ presetBaseUrl,
273
+ stack: [...stack, resolvedUrl],
274
+ });
275
+ }
276
+
277
+ export async function loadRuntimeConfigFromRef(ref, context) {
278
+ const candidates = resolveExtendCandidates(ref, context);
279
+ if (!candidates.length) {
280
+ throw new Error(`Runtime config extends is invalid: ${ref}`);
281
+ }
282
+ let lastError = null;
283
+ for (const candidate of candidates) {
284
+ try {
285
+ return await loadRuntimeConfigChain(candidate, context, context.stack ?? []);
286
+ } catch (error) {
287
+ if (error?.code === 'runtime_config_not_found') {
288
+ lastError = error;
289
+ continue;
290
+ }
291
+ throw error;
292
+ }
293
+ }
294
+ if (lastError) {
295
+ throw lastError;
296
+ }
297
+ throw new Error(`Runtime config extends not found: ${ref}`);
298
+ }
299
+
300
+ export async function loadRuntimeConfigFromUrl(url, options = {}) {
301
+ if (!url) {
302
+ throw new Error('runtime config url is required');
303
+ }
304
+ return loadRuntimeConfigChain(url, options);
305
+ }
306
+
307
+ export async function applyRuntimeConfigFromUrl(url, options = {}) {
308
+ const { runtime } = await loadRuntimeConfigFromUrl(url, options);
309
+ const mergedRuntime = mergeRuntimeValues(getRuntimeConfig(), runtime);
310
+ setRuntimeConfig(mergedRuntime);
311
+ return mergedRuntime;
312
+ }
313
+
314
+ export async function loadRuntimePreset(presetId, options = {}) {
315
+ const baseUrl = options.baseUrl || resolvePresetBaseUrl();
316
+ const normalized = normalizePresetPath(presetId);
317
+ if (!normalized) {
318
+ throw new Error('runtime preset id is required');
319
+ }
320
+ const url = `${baseUrl.replace(/\/$/, '')}/${normalized}`;
321
+ return loadRuntimeConfigFromUrl(url, { ...options, presetBaseUrl: baseUrl });
322
+ }
323
+
324
+ export async function applyRuntimePreset(presetId, options = {}) {
325
+ const { runtime } = await loadRuntimePreset(presetId, options);
326
+ const mergedRuntime = mergeRuntimeValues(getRuntimeConfig(), runtime);
327
+ setRuntimeConfig(mergedRuntime);
328
+ return mergedRuntime;
329
+ }
330
+
331
+ export function normalizeRuntimeConfigChain(value) {
332
+ if (!Array.isArray(value)) {
333
+ return [];
334
+ }
335
+ return value
336
+ .map((entry) => typeof entry === 'string' ? entry.trim() : '')
337
+ .filter(Boolean);
338
+ }
339
+
340
+ export async function applyRuntimeForRun(run, options = {}) {
341
+ const configChain = normalizeRuntimeConfigChain(
342
+ run.configChain
343
+ ?? run.runtime?.configChain
344
+ ?? options.runtime?.configChain
345
+ );
346
+ await applyOrderedRuntimeInputs({
347
+ getRuntimeConfig,
348
+ setRuntimeConfig,
349
+ }, {
350
+ configChain,
351
+ runtimePreset: run.runtimePreset ?? null,
352
+ runtimeConfigUrl: run.runtimeConfigUrl ?? null,
353
+ runtimeConfig: run.runtimeConfig ?? null,
354
+ runtimeContractPatch: typeof run.command === 'string' && run.command.trim()
355
+ ? () => buildRuntimeContractPatch({
356
+ ...run,
357
+ configChain: undefined,
358
+ modelId: run.modelId ?? null,
359
+ })
360
+ : null,
361
+ }, {
362
+ loadRuntimeConfigFromRef: (ref, runtimeOptions) => loadRuntimeConfigFromRef(ref, runtimeOptions),
363
+ applyRuntimePreset,
364
+ applyRuntimeConfigFromUrl,
365
+ }, options);
366
+ }
367
+
368
+ export function normalizeManifest(manifest) {
369
+ if (!manifest || typeof manifest !== 'object') {
370
+ throw new Error('Harness manifest must be an object.');
371
+ }
372
+ const runs = Array.isArray(manifest.runs) ? manifest.runs : [];
373
+ if (!runs.length) {
374
+ throw new Error('Harness manifest must include at least one run.');
375
+ }
376
+ return {
377
+ defaults: manifest.defaults ?? {},
378
+ runs,
379
+ reportModelId: manifest.reportModelId ?? manifest.id ?? 'manifest',
380
+ report: manifest.report ?? null,
381
+ };
382
+ }
383
+
384
+ export function mergeRunDefaults(defaults, run) {
385
+ return {
386
+ ...defaults,
387
+ ...run,
388
+ configChain: run.configChain ?? defaults.configChain ?? null,
389
+ runtimePreset: run.runtimePreset ?? defaults.runtimePreset ?? null,
390
+ runtimeConfigUrl: run.runtimeConfigUrl ?? defaults.runtimeConfigUrl ?? null,
391
+ runtimeConfig: run.runtimeConfig ?? defaults.runtimeConfig ?? null,
392
+ suite: run.suite ?? defaults.suite ?? 'inference',
393
+ };
394
+ }
395
+
396
+ export function summarizeManifestRuns(results) {
397
+ let passedRuns = 0;
398
+ let failedRuns = 0;
399
+ let durationMs = 0;
400
+ for (const result of results) {
401
+ const failures = (result.results || []).filter((entry) => !entry.passed && !entry.skipped);
402
+ if (failures.length > 0) {
403
+ failedRuns += 1;
404
+ } else {
405
+ passedRuns += 1;
406
+ }
407
+ durationMs += result.duration || 0;
408
+ }
409
+ return {
410
+ totalRuns: results.length,
411
+ passedRuns,
412
+ failedRuns,
413
+ durationMs,
414
+ };
415
+ }
@@ -0,0 +1,28 @@
1
+ export declare function buildSuiteSummary(
2
+ suiteName: string,
3
+ results: Array<Record<string, unknown>>,
4
+ startTimeMs: number
5
+ ): {
6
+ suite: string;
7
+ passed: number;
8
+ failed: number;
9
+ skipped: number;
10
+ duration: number;
11
+ results: Array<Record<string, unknown>>;
12
+ };
13
+ export declare function normalizeCacheMode(value: unknown): 'cold' | 'warm';
14
+ export declare function normalizeLoadMode(value: unknown, hasModelUrl: boolean): 'opfs' | 'http' | 'memory';
15
+ export declare function normalizeWorkloadType(value: unknown): string | null;
16
+ export declare function safeStatsValue(value: unknown): number;
17
+ export declare function calculateRatePerSecond(count: unknown, durationMs: unknown): number;
18
+ export declare function buildDiffusionPerformanceArtifact(options: Record<string, unknown>): Record<string, unknown>;
19
+ export declare function assertDiffusionPerformanceArtifact(metrics: Record<string, unknown>, contextLabel?: string): void;
20
+ export declare function toTimingNumber(value: unknown, fallback?: number | null): number | null;
21
+ export declare function safeToFixed(value: unknown, fallback?: number | null, digits?: number): number | null;
22
+ export declare function sampleTimingNumber(
23
+ stats: Record<string, unknown> | null | undefined,
24
+ key: string,
25
+ fallback?: number | null
26
+ ): number | null;
27
+ export declare function buildCanonicalTiming(overrides?: Record<string, unknown>): Record<string, unknown>;
28
+ export declare function buildTimingDiagnostics(timing?: Record<string, unknown>, options?: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,268 @@
1
+ export function buildSuiteSummary(suiteName, results, startTimeMs) {
2
+ let passed = 0;
3
+ let failed = 0;
4
+ let skipped = 0;
5
+ const safeResults = Array.isArray(results) ? results : [];
6
+ for (const result of safeResults) {
7
+ if (result.skipped) {
8
+ skipped++;
9
+ } else if (result.passed) {
10
+ passed++;
11
+ } else {
12
+ failed++;
13
+ }
14
+ }
15
+ const duration = Math.max(0, performance.now() - (Number.isFinite(startTimeMs) ? startTimeMs : performance.now()));
16
+ return { suite: suiteName, passed, failed, skipped, duration, results: safeResults };
17
+ }
18
+
19
+ export function normalizeCacheMode(value) {
20
+ return value === 'cold' || value === 'warm' ? value : 'warm';
21
+ }
22
+
23
+ export function normalizeLoadMode(value, hasModelUrl) {
24
+ if (value === 'opfs' || value === 'http' || value === 'memory') {
25
+ return value;
26
+ }
27
+ return hasModelUrl ? 'http' : 'opfs';
28
+ }
29
+
30
+ export function normalizeWorkloadType(value) {
31
+ const normalized = String(value || '').trim().toLowerCase();
32
+ return normalized || null;
33
+ }
34
+
35
+ export function safeStatsValue(value) {
36
+ return Number.isFinite(value) ? Number(value) : 0;
37
+ }
38
+
39
+ export function calculateRatePerSecond(count, durationMs) {
40
+ const safeCount = safeStatsValue(count);
41
+ const safeDurationMs = safeStatsValue(durationMs);
42
+ if (safeCount <= 0 || safeDurationMs <= 0) return 0;
43
+ return Number(((safeCount * 1000) / safeDurationMs).toFixed(2));
44
+ }
45
+
46
+ export function buildDiffusionPerformanceArtifact({
47
+ warmupRuns,
48
+ timedRuns,
49
+ width,
50
+ height,
51
+ steps,
52
+ guidanceScale,
53
+ avgPrefillTokens,
54
+ avgDecodeTokens,
55
+ cpuStats,
56
+ gpuStats,
57
+ }) {
58
+ const cpuPrefillMs = safeStatsValue(cpuStats?.prefillMs?.median);
59
+ const cpuDenoiseMs = safeStatsValue(cpuStats?.denoiseMs?.median);
60
+ const cpuVaeMs = safeStatsValue(cpuStats?.vaeMs?.median);
61
+ const cpuTotalMs = safeStatsValue(cpuStats?.totalMs?.median);
62
+ const gpuPrefillMs = safeStatsValue(gpuStats?.prefillMs?.median);
63
+ const gpuDenoiseMs = safeStatsValue(gpuStats?.denoiseMs?.median);
64
+ const gpuVaeMs = safeStatsValue(gpuStats?.vaeMs?.median);
65
+ const gpuTotalMs = safeStatsValue(gpuStats?.totalMs?.median);
66
+ const decodeStepsPerSec = calculateRatePerSecond(steps, cpuDenoiseMs);
67
+ const decodeTokensPerSec = calculateRatePerSecond(avgDecodeTokens, cpuDenoiseMs);
68
+ const prefillTokensPerSec = calculateRatePerSecond(avgPrefillTokens, cpuPrefillMs);
69
+
70
+ return {
71
+ schemaVersion: 1,
72
+ warmupRuns,
73
+ timedRuns,
74
+ shape: {
75
+ width,
76
+ height,
77
+ },
78
+ scheduler: {
79
+ steps,
80
+ guidanceScale,
81
+ },
82
+ cpu: {
83
+ totalMs: cpuTotalMs,
84
+ prefillMs: cpuPrefillMs,
85
+ denoiseMs: cpuDenoiseMs,
86
+ vaeMs: cpuVaeMs,
87
+ },
88
+ gpu: {
89
+ available: gpuStats?.available === true,
90
+ totalMs: gpuStats?.available === true ? gpuTotalMs : null,
91
+ prefillMs: gpuStats?.available === true ? gpuPrefillMs : null,
92
+ denoiseMs: gpuStats?.available === true ? gpuDenoiseMs : null,
93
+ vaeMs: gpuStats?.available === true ? gpuVaeMs : null,
94
+ },
95
+ throughput: {
96
+ prefillTokensPerSec,
97
+ decodeTokensPerSec,
98
+ decodeStepsPerSec,
99
+ },
100
+ tokens: {
101
+ avgPrefillTokens: safeStatsValue(avgPrefillTokens),
102
+ avgDecodeTokens: safeStatsValue(avgDecodeTokens),
103
+ },
104
+ };
105
+ }
106
+
107
+ export function assertDiffusionPerformanceArtifact(metrics, contextLabel = 'diffusion') {
108
+ const artifact = metrics?.performanceArtifact;
109
+ if (!artifact || typeof artifact !== 'object') {
110
+ throw new Error(`${contextLabel}: metrics.performanceArtifact is required.`);
111
+ }
112
+ if (artifact.schemaVersion !== 1) {
113
+ throw new Error(`${contextLabel}: metrics.performanceArtifact.schemaVersion must be 1.`);
114
+ }
115
+ if (!Number.isInteger(artifact.warmupRuns) || artifact.warmupRuns < 0) {
116
+ throw new Error(`${contextLabel}: metrics.performanceArtifact.warmupRuns must be a non-negative integer.`);
117
+ }
118
+ if (!Number.isInteger(artifact.timedRuns) || artifact.timedRuns < 1) {
119
+ throw new Error(`${contextLabel}: metrics.performanceArtifact.timedRuns must be a positive integer.`);
120
+ }
121
+ if (!Number.isFinite(artifact?.cpu?.prefillMs)) {
122
+ throw new Error(`${contextLabel}: metrics.performanceArtifact.cpu.prefillMs must be finite.`);
123
+ }
124
+ if (!Number.isFinite(artifact?.cpu?.denoiseMs)) {
125
+ throw new Error(`${contextLabel}: metrics.performanceArtifact.cpu.denoiseMs must be finite.`);
126
+ }
127
+ if (!Number.isFinite(artifact?.cpu?.vaeMs)) {
128
+ throw new Error(`${contextLabel}: metrics.performanceArtifact.cpu.vaeMs must be finite.`);
129
+ }
130
+ if (!Number.isFinite(artifact?.cpu?.totalMs)) {
131
+ throw new Error(`${contextLabel}: metrics.performanceArtifact.cpu.totalMs must be finite.`);
132
+ }
133
+ if (!Number.isFinite(artifact?.throughput?.decodeStepsPerSec)) {
134
+ throw new Error(`${contextLabel}: metrics.performanceArtifact.throughput.decodeStepsPerSec must be finite.`);
135
+ }
136
+ }
137
+
138
+ function formatMetricNumber(value, fallback = 0, digits = 2) {
139
+ const numericValue = Number(value);
140
+ if (!Number.isFinite(numericValue)) return fallback;
141
+ return Number(numericValue.toFixed(digits));
142
+ }
143
+
144
+ export function toTimingNumber(value, fallback = 0) {
145
+ return formatMetricNumber(value, fallback, 2);
146
+ }
147
+
148
+ export function safeToFixed(value, fallback = 0, digits = 2) {
149
+ return formatMetricNumber(value, fallback, digits);
150
+ }
151
+
152
+ export function sampleTimingNumber(stats, key, fallback = 0) {
153
+ return formatMetricNumber(stats?.[key], fallback, 2);
154
+ }
155
+
156
+ export function buildCanonicalTiming(overrides = {}) {
157
+ const cacheMode = normalizeCacheMode(overrides.cacheMode);
158
+ const modelLoadMs = toTimingNumber(overrides.modelLoadMs, 0);
159
+ const prefillMs = toTimingNumber(overrides.prefillMs, 0);
160
+ const decodeMs = toTimingNumber(overrides.decodeMs, 0);
161
+ const decodeMsPerTokenP50 = Number.isFinite(overrides.decodeMsPerTokenP50)
162
+ ? toTimingNumber(overrides.decodeMsPerTokenP50)
163
+ : null;
164
+ const decodeMsPerTokenP95 = Number.isFinite(overrides.decodeMsPerTokenP95)
165
+ ? toTimingNumber(overrides.decodeMsPerTokenP95)
166
+ : null;
167
+ const decodeMsPerTokenP99 = Number.isFinite(overrides.decodeMsPerTokenP99)
168
+ ? toTimingNumber(overrides.decodeMsPerTokenP99)
169
+ : null;
170
+ const decodeTokensPerSec = Number.isFinite(overrides.decodeTokensPerSec)
171
+ ? toTimingNumber(overrides.decodeTokensPerSec)
172
+ : null;
173
+ const prefillTokensPerSec = Number.isFinite(overrides.prefillTokensPerSec)
174
+ ? toTimingNumber(overrides.prefillTokensPerSec)
175
+ : null;
176
+ const totalRunMs = toTimingNumber(
177
+ overrides.totalRunMs,
178
+ toTimingNumber(prefillMs + decodeMs)
179
+ );
180
+ const firstTokenMs = Number.isFinite(overrides.firstTokenMs)
181
+ ? toTimingNumber(overrides.firstTokenMs)
182
+ : null;
183
+ const firstResponseMs = Number.isFinite(overrides.firstResponseMs)
184
+ ? toTimingNumber(overrides.firstResponseMs)
185
+ : toTimingNumber(modelLoadMs + totalRunMs);
186
+
187
+ return {
188
+ modelLoadMs,
189
+ firstTokenMs,
190
+ firstResponseMs,
191
+ prefillMs,
192
+ decodeMs,
193
+ decodeMsPerTokenP50,
194
+ decodeMsPerTokenP95,
195
+ decodeMsPerTokenP99,
196
+ decodeTokensPerSec,
197
+ prefillTokensPerSec,
198
+ totalRunMs,
199
+ cacheMode,
200
+ loadMode: overrides.loadMode,
201
+ };
202
+ }
203
+
204
+ export function buildTimingDiagnostics(timing = {}, options = {}) {
205
+ const prefillSemantics = String(options.prefillSemantics || 'internal_prefill_phase');
206
+ const source = String(options.source || 'doppler');
207
+ const modelLoadMs = Number.isFinite(timing.modelLoadMs) ? toTimingNumber(timing.modelLoadMs) : null;
208
+ const firstTokenMs = Number.isFinite(timing.firstTokenMs) ? toTimingNumber(timing.firstTokenMs) : null;
209
+ const firstResponseMs = Number.isFinite(timing.firstResponseMs) ? toTimingNumber(timing.firstResponseMs) : null;
210
+ const prefillMs = Number.isFinite(timing.prefillMs) ? toTimingNumber(timing.prefillMs) : null;
211
+ const decodeMs = Number.isFinite(timing.decodeMs) ? toTimingNumber(timing.decodeMs) : null;
212
+ const totalRunMs = Number.isFinite(timing.totalRunMs) ? toTimingNumber(timing.totalRunMs) : null;
213
+
214
+ const firstResponseFromLoadAndFirstTokenMs = (
215
+ Number.isFinite(modelLoadMs) && Number.isFinite(firstTokenMs)
216
+ )
217
+ ? toTimingNumber(modelLoadMs + firstTokenMs)
218
+ : null;
219
+ const runFromPrefillAndDecodeMs = (
220
+ Number.isFinite(prefillMs) && Number.isFinite(decodeMs)
221
+ )
222
+ ? toTimingNumber(prefillMs + decodeMs)
223
+ : null;
224
+
225
+ const firstResponseResidualMs = (
226
+ Number.isFinite(firstResponseMs) && Number.isFinite(firstResponseFromLoadAndFirstTokenMs)
227
+ )
228
+ ? toTimingNumber(firstResponseMs - firstResponseFromLoadAndFirstTokenMs)
229
+ : null;
230
+ const runResidualMs = (
231
+ Number.isFinite(totalRunMs) && Number.isFinite(runFromPrefillAndDecodeMs)
232
+ )
233
+ ? toTimingNumber(totalRunMs - runFromPrefillAndDecodeMs)
234
+ : null;
235
+
236
+ return {
237
+ schemaVersion: 1,
238
+ source,
239
+ semantics: {
240
+ modelLoadMs: 'model initialization/load before generation',
241
+ firstTokenMs: 'ttft from generation start',
242
+ firstResponseMs: 'modelLoadMs + firstTokenMs',
243
+ prefillMs: prefillSemantics,
244
+ decodeMs: 'time after first token',
245
+ totalRunMs: 'prefillMs + decodeMs',
246
+ },
247
+ componentsMs: {
248
+ modelLoadMs,
249
+ firstTokenMs,
250
+ firstResponseMs,
251
+ prefillMs,
252
+ decodeMs,
253
+ totalRunMs,
254
+ },
255
+ sumsMs: {
256
+ firstResponseFromLoadAndFirstTokenMs,
257
+ runFromPrefillAndDecodeMs,
258
+ },
259
+ residualsMs: {
260
+ firstResponseResidualMs,
261
+ runResidualMs,
262
+ },
263
+ consistent: {
264
+ firstResponse: Number.isFinite(firstResponseResidualMs) ? Math.abs(firstResponseResidualMs) <= 2 : null,
265
+ totalRun: Number.isFinite(runResidualMs) ? Math.abs(runResidualMs) <= 2 : null,
266
+ },
267
+ };
268
+ }