@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
@@ -9,7 +9,10 @@ import {
9
9
  ensureCommandSupportedOnSurface,
10
10
  normalizeToolingCommandRequest,
11
11
  } from './command-api.js';
12
- import { normalizeToToolingCommandError } from './command-envelope.js';
12
+ import {
13
+ isToolingSuccessEnvelope,
14
+ normalizeToToolingCommandError,
15
+ } from './command-envelope.js';
13
16
 
14
17
  const DEFAULT_HOST = '127.0.0.1';
15
18
  const DEFAULT_RUNNER_PATH = '/src/tooling/command-runner.html';
@@ -359,6 +362,26 @@ function asNonEmptyString(value) {
359
362
  return normalized === '' ? null : normalized;
360
363
  }
361
364
 
365
+ function createPersistentContextRequiredError(requestedLoadMode, cause = null) {
366
+ const baseMessage = requestedLoadMode === 'opfs'
367
+ ? 'browser command: loadMode=opfs requires persistent browser context; persistent launch failed.'
368
+ : 'browser command: persistent browser context is required when OPFS cache is enabled; persistent launch failed.';
369
+ const causeMessage = asNonEmptyString(cause?.message || cause);
370
+ return new Error(
371
+ `${baseMessage} Re-run with run.browser.opfsCache=false to use a non-persistent browser session.${causeMessage ? ` (${causeMessage})` : ''}`
372
+ );
373
+ }
374
+
375
+ export function finalizeBrowserRelayResponse(response, request) {
376
+ if (!isToolingSuccessEnvelope(response)) {
377
+ throw new Error('browser command: runner returned an invalid success envelope.');
378
+ }
379
+ return {
380
+ ...response,
381
+ request,
382
+ };
383
+ }
384
+
362
385
  function normalizeWebgpuBackend(value) {
363
386
  const raw = asNonEmptyString(value);
364
387
  if (!raw) return null;
@@ -570,8 +593,10 @@ async function launchPersistentBrowser(chromium, userDataDir, launchOptions, opt
570
593
 
571
594
  export async function runBrowserCommandInNode(commandRequest, options = {}) {
572
595
  let request = null;
596
+ let sourceRequest = null;
573
597
  try {
574
598
  ({ request } = ensureCommandSupportedOnSurface(commandRequest, 'browser'));
599
+ sourceRequest = request;
575
600
 
576
601
  if (request.keepPipeline) {
577
602
  throw new Error(
@@ -583,321 +608,186 @@ export async function runBrowserCommandInNode(commandRequest, options = {}) {
583
608
  throw new Error('browser command relay does not support convert. Use --surface node for convert commands.');
584
609
  }
585
610
 
586
- let useOpfsCache = options.opfsCache !== false;
587
- const userDataDir = options.userDataDir || DEFAULT_OPFS_CACHE_DIR;
611
+ let useOpfsCache = options.opfsCache !== false;
612
+ let relayRequest = request;
613
+ const userDataDir = options.userDataDir || DEFAULT_OPFS_CACHE_DIR;
588
614
 
589
- if (options.wipeCacheBeforeLaunch && useOpfsCache) {
590
- await fs.rm(userDataDir, { recursive: true, force: true }).catch(() => {});
591
- }
615
+ if (options.wipeCacheBeforeLaunch && useOpfsCache) {
616
+ await fs.rm(userDataDir, { recursive: true, force: true }).catch(() => {});
617
+ }
592
618
 
593
- const { chromium } = await import('playwright');
594
- const baseUrl = normalizeBaseUrl(options.baseUrl);
595
- // When OPFS caching is enabled, use a fixed port so the browser origin stays the same
596
- // across runs (OPFS is origin-scoped). Without this, random ports create new origins.
597
- const serverPort = options.port ?? (useOpfsCache ? DEFAULT_OPFS_CACHE_PORT : 0);
598
- const server = baseUrl
599
- ? null
600
- : await createStaticFileServer({
601
- rootDir: options.staticRootDir,
602
- staticMounts: options.staticMounts,
603
- host: options.host,
604
- port: serverPort,
605
- }).catch((error) => {
606
- const message = error?.message || String(error);
607
- throw new Error(
608
- `browser command: failed to start static server (${message}). Pass --browser-base-url to reuse an existing server.`
609
- );
610
- });
619
+ const { chromium } = await import('playwright');
620
+ const baseUrl = normalizeBaseUrl(options.baseUrl);
621
+ // When OPFS caching is enabled, use a fixed port so the browser origin stays the same
622
+ // across runs (OPFS is origin-scoped). Without this, random ports create new origins.
623
+ const serverPort = options.port ?? (useOpfsCache ? DEFAULT_OPFS_CACHE_PORT : 0);
624
+ const server = baseUrl
625
+ ? null
626
+ : await createStaticFileServer({
627
+ rootDir: options.staticRootDir,
628
+ staticMounts: options.staticMounts,
629
+ host: options.host,
630
+ port: serverPort,
631
+ }).catch((error) => {
632
+ const message = error?.message || String(error);
633
+ throw new Error(
634
+ `browser command: failed to start static server (${message}). Pass --browser-base-url to reuse an existing server.`
635
+ );
636
+ });
611
637
 
612
- const launchOptions = {
613
- headless: normalizeHeadless(options.headless),
614
- args: browserLaunchArgs(normalizeBrowserArgs(options.browserArgs)),
615
- };
638
+ const launchOptions = {
639
+ headless: normalizeHeadless(options.headless),
640
+ args: browserLaunchArgs(normalizeBrowserArgs(options.browserArgs)),
641
+ };
616
642
 
617
- if (options.channel) {
618
- launchOptions.channel = String(options.channel);
619
- }
620
- if (options.executablePath) {
621
- launchOptions.executablePath = String(options.executablePath);
622
- }
643
+ if (options.channel) {
644
+ launchOptions.channel = String(options.channel);
645
+ }
646
+ if (options.executablePath) {
647
+ launchOptions.executablePath = String(options.executablePath);
648
+ }
623
649
 
624
- const timeoutMs = normalizeTimeoutMs(options.timeoutMs);
625
- const runnerPath = normalizeRunnerPath(options.runnerPath);
626
- const resolvedBaseUrl = baseUrl || server.baseUrl;
627
- const requestedLoadMode = request.loadMode;
628
- const requireOpfsLoad = requestedLoadMode === 'opfs';
629
- if (requireOpfsLoad && useOpfsCache === false) {
630
- throw new Error('browser command: loadMode=opfs requires OPFS cache support (remove --no-opfs-cache).');
631
- }
632
- if (!requestedLoadMode && request.modelUrl && useOpfsCache === false) {
633
- request = {
634
- ...request,
635
- loadMode: 'http',
636
- };
637
- }
650
+ const timeoutMs = normalizeTimeoutMs(options.timeoutMs);
651
+ const runnerPath = normalizeRunnerPath(options.runnerPath);
652
+ const resolvedBaseUrl = baseUrl || server.baseUrl;
653
+ const requestedLoadMode = sourceRequest.loadMode;
654
+ const requireOpfsLoad = requestedLoadMode === 'opfs';
655
+ if (requireOpfsLoad && useOpfsCache === false) {
656
+ throw new Error('browser command: loadMode=opfs requires OPFS cache support (remove --no-opfs-cache).');
657
+ }
658
+ if (requireOpfsLoad && sourceRequest.modelUrl && !sourceRequest.modelId) {
659
+ throw new Error(
660
+ 'browser command: loadMode=opfs requires modelId when modelUrl is provided so the relay can verify and load the cached OPFS artifact.'
661
+ );
662
+ }
638
663
 
639
664
  let browser = null;
640
665
  let context = null;
641
666
  try {
642
- if (useOpfsCache) {
643
- // Persistent context: OPFS data survives between runs.
644
- // launchPersistentContext returns a BrowserContext directly (no separate Browser).
645
- try {
646
- context = await launchPersistentBrowser(chromium, userDataDir, launchOptions, {
647
- explicitChannel: Boolean(options.channel),
648
- explicitExecutablePath: Boolean(options.executablePath),
649
- });
650
- } catch (error) {
651
- if (!isRecoverablePersistentLaunchError(error)) {
652
- throw error;
653
- }
654
- if (typeof options.onConsole === 'function') {
655
- options.onConsole({
656
- type: 'warning',
657
- text: '[browser] Persistent browser launch failed; retrying with a clean OPFS profile.',
658
- });
659
- }
660
- await fs.rm(userDataDir, { recursive: true, force: true }).catch(() => {});
667
+ if (useOpfsCache) {
668
+ // Persistent context: OPFS data survives between runs.
669
+ // launchPersistentContext returns a BrowserContext directly (no separate Browser).
661
670
  try {
662
671
  context = await launchPersistentBrowser(chromium, userDataDir, launchOptions, {
663
672
  explicitChannel: Boolean(options.channel),
664
673
  explicitExecutablePath: Boolean(options.executablePath),
665
674
  });
666
- } catch (retryError) {
667
- if (!isRecoverablePersistentLaunchError(retryError)) {
668
- throw retryError;
675
+ } catch (error) {
676
+ if (!isRecoverablePersistentLaunchError(error)) {
677
+ throw error;
669
678
  }
670
679
  if (typeof options.onConsole === 'function') {
671
680
  options.onConsole({
672
681
  type: 'warning',
673
- text: '[browser] Persistent launch still failing; falling back to non-persistent mode.',
682
+ text: '[browser] Persistent browser launch failed; retrying with a clean OPFS profile.',
674
683
  });
675
684
  }
676
- if (requireOpfsLoad) {
677
- throw new Error(
678
- 'browser command: loadMode=opfs requires persistent browser context; persistent launch failed.'
679
- );
680
- }
681
- useOpfsCache = false;
682
- if (request.loadMode === 'opfs') {
683
- request = {
684
- ...request,
685
- loadMode: 'http',
686
- };
685
+ await fs.rm(userDataDir, { recursive: true, force: true }).catch(() => {});
686
+ try {
687
+ context = await launchPersistentBrowser(chromium, userDataDir, launchOptions, {
688
+ explicitChannel: Boolean(options.channel),
689
+ explicitExecutablePath: Boolean(options.executablePath),
690
+ });
691
+ } catch (retryError) {
692
+ if (!isRecoverablePersistentLaunchError(retryError)) {
693
+ throw retryError;
694
+ }
695
+ throw createPersistentContextRequiredError(requestedLoadMode, retryError);
687
696
  }
688
- browser = await launchBrowser(chromium, launchOptions, {
689
- explicitChannel: Boolean(options.channel),
690
- explicitExecutablePath: Boolean(options.executablePath),
691
- });
692
- context = await browser.newContext();
693
697
  }
698
+ } else {
699
+ browser = await launchBrowser(chromium, launchOptions, {
700
+ explicitChannel: Boolean(options.channel),
701
+ explicitExecutablePath: Boolean(options.executablePath),
702
+ });
703
+ context = await browser.newContext();
694
704
  }
695
- } else {
696
- browser = await launchBrowser(chromium, launchOptions, {
697
- explicitChannel: Boolean(options.channel),
698
- explicitExecutablePath: Boolean(options.executablePath),
699
- });
700
- context = await browser.newContext();
701
- }
702
705
 
703
- const page = await context.newPage();
704
- page.setDefaultTimeout(timeoutMs);
705
- const pageDiagnostics = [];
706
+ const page = await context.newPage();
707
+ page.setDefaultTimeout(timeoutMs);
708
+ const pageDiagnostics = [];
706
709
 
707
- if (typeof options.onConsole === 'function') {
708
- page.on('console', (message) => {
709
- options.onConsole({
710
- type: message.type(),
711
- text: message.text(),
710
+ if (typeof options.onConsole === 'function') {
711
+ page.on('console', (message) => {
712
+ options.onConsole({
713
+ type: message.type(),
714
+ text: message.text(),
715
+ });
712
716
  });
713
- });
714
- }
715
-
716
- page.on('pageerror', (error) => {
717
- pageDiagnostics.push(`pageerror: ${error?.message || String(error)}`);
718
- });
719
- page.on('requestfailed', (request) => {
720
- const failure = request.failure();
721
- pageDiagnostics.push(
722
- `requestfailed: ${request.url()} (${failure?.errorText || 'unknown error'})`
723
- );
724
- });
717
+ }
725
718
 
726
- const runnerUrl = new URL(runnerPath, resolvedBaseUrl);
727
- runnerUrl.searchParams.set('_dopplerRunner', String(Date.now()));
728
- await page.goto(runnerUrl.toString(), { waitUntil: 'load' });
729
- try {
730
- await page.waitForFunction(() => globalThis.__dopplerRunnerReady === true, null, {
731
- timeout: timeoutMs,
719
+ page.on('pageerror', (error) => {
720
+ pageDiagnostics.push(`pageerror: ${error?.message || String(error)}`);
721
+ });
722
+ page.on('requestfailed', (request) => {
723
+ const failure = request.failure();
724
+ pageDiagnostics.push(
725
+ `requestfailed: ${request.url()} (${failure?.errorText || 'unknown error'})`
726
+ );
732
727
  });
733
- } catch (error) {
734
- const diagnostics = pageDiagnostics.length
735
- ? pageDiagnostics.slice(0, 10).join(' | ')
736
- : 'no page diagnostics captured';
737
- throw new Error(
738
- `browser command: runner did not become ready within ${timeoutMs}ms (${diagnostics}).`
739
- );
740
- }
741
728
 
742
- // OPFS cache: ensure model is cached before running the command.
743
- // On cache hit, strip modelUrl so the harness takes the fast OPFS path.
744
- if (useOpfsCache && request.modelId && request.modelUrl) {
729
+ const runnerUrl = new URL(runnerPath, resolvedBaseUrl);
730
+ runnerUrl.searchParams.set('_dopplerRunner', String(Date.now()));
731
+ await page.goto(runnerUrl.toString(), { waitUntil: 'load' });
745
732
  try {
746
- const cacheResult = await page.evaluate(async (payload) => {
747
- if (typeof globalThis.__dopplerEnsureCached !== 'function') {
748
- return { cached: false, error: '__dopplerEnsureCached not available' };
749
- }
750
- return globalThis.__dopplerEnsureCached(payload.modelId, payload.modelBaseUrl);
751
- }, {
752
- modelId: request.modelId,
753
- modelBaseUrl: request.modelUrl,
733
+ await page.waitForFunction(() => globalThis.__dopplerRunnerReady === true, null, {
734
+ timeout: timeoutMs,
754
735
  });
736
+ } catch (error) {
737
+ const diagnostics = pageDiagnostics.length
738
+ ? pageDiagnostics.slice(0, 10).join(' | ')
739
+ : 'no page diagnostics captured';
740
+ throw new Error(
741
+ `browser command: runner did not become ready within ${timeoutMs}ms (${diagnostics}).`
742
+ );
743
+ }
744
+
745
+ // Explicit loadMode=opfs must be satisfied without rewriting the shared request contract.
746
+ if (useOpfsCache && requireOpfsLoad && relayRequest.modelId && relayRequest.modelUrl) {
747
+ try {
748
+ const cacheResult = await page.evaluate(async (payload) => {
749
+ if (typeof globalThis.__dopplerEnsureCached !== 'function') {
750
+ return { cached: false, error: '__dopplerEnsureCached not available' };
751
+ }
752
+ return globalThis.__dopplerEnsureCached(payload.modelId, payload.modelBaseUrl);
753
+ }, {
754
+ modelId: relayRequest.modelId,
755
+ modelBaseUrl: relayRequest.modelUrl,
756
+ });
755
757
 
756
- if (cacheResult.cached) {
757
- // Remove modelUrl so the harness loads from OPFS instead of HTTP.
758
- request = { ...request };
759
- delete request.modelUrl;
760
- request.loadMode = 'opfs';
761
- } else {
762
- if (requireOpfsLoad) {
758
+ if (cacheResult.cached) {
759
+ relayRequest = { ...relayRequest };
760
+ delete relayRequest.modelUrl;
761
+ } else {
763
762
  const cacheError = cacheResult?.error || 'model not cached';
764
763
  throw new Error(
765
- `[opfs-cache] loadMode=opfs requested but cache is unavailable for "${request.modelId || 'unknown-model'}": ${cacheError}`
764
+ `[opfs-cache] model cache is unavailable for "${relayRequest.modelId || 'unknown-model'}": ${cacheError}.`
766
765
  );
767
766
  }
768
- if (!requestedLoadMode) {
769
- request = { ...request, loadMode: 'http' };
770
- }
771
- if (cacheResult.error) {
772
- if (typeof options.onConsole === 'function') {
773
- options.onConsole({
774
- type: 'warning',
775
- text: `[opfs-cache] Cache check failed (${cacheResult.error}), falling back to HTTP`,
776
- });
777
- }
778
- }
779
- }
780
- } catch (error) {
781
- if (requireOpfsLoad) {
767
+ } catch (error) {
782
768
  throw new Error(
783
- `[opfs-cache] loadMode=opfs requested but cache priming failed: ${error?.message || error}`
769
+ `[opfs-cache] cache priming failed: ${error?.message || error}.`
784
770
  );
785
771
  }
786
- if (!requestedLoadMode && request.modelUrl) {
787
- request = { ...request, loadMode: 'http' };
788
- }
789
-
790
- // OPFS cache is best-effort; fall back to HTTP on any error.
791
- if (typeof options.onConsole === 'function') {
792
- options.onConsole({
793
- type: 'warning',
794
- text: `[opfs-cache] Error (${error?.message || error}), falling back to HTTP`,
795
- });
796
- }
797
- }
798
- }
799
-
800
- const response = await page.evaluate(async (payload) => {
801
- if (typeof globalThis.__dopplerRunBrowserCommand !== 'function') {
802
- throw new Error('browser command runner is missing globalThis.__dopplerRunBrowserCommand');
803
772
  }
804
- return globalThis.__dopplerRunBrowserCommand(payload.request, payload.options || {});
805
- }, {
806
- request,
807
- options: {
808
- runtimeLoadOptions: options.runtimeLoadOptions || {},
809
- },
810
- });
811
773
 
812
- const result = response?.result;
813
- if (result && typeof result === 'object' && !Array.isArray(result)) {
814
- const cpuInfo = typeof os.cpus === 'function' ? os.cpus() : null;
815
- const hostEnvironment = {
816
- platform: process.platform,
817
- arch: process.arch,
818
- nodeVersion: process.version,
819
- osRelease: typeof os.release === 'function' ? os.release() : null,
820
- cpuModel: Array.isArray(cpuInfo) && cpuInfo.length > 0 ? asNonEmptyString(cpuInfo[0]?.model) : null,
821
- };
822
- const webgpuBackend = inferWebgpuBackendFromArgs(launchOptions.args, hostEnvironment.platform);
823
- const env = result.env && typeof result.env === 'object' ? result.env : {};
824
- const deviceInfo = result.deviceInfo && typeof result.deviceInfo === 'object'
825
- ? result.deviceInfo
826
- : {};
827
- result.env = {
828
- ...env,
829
- webgpuBackend: normalizeWebgpuBackend(env.webgpuBackend)
830
- || normalizeWebgpuBackend(env.gpuBackend)
831
- || normalizeWebgpuBackend(env.graphicsBackend)
832
- || webgpuBackend,
833
- };
834
- const existingEnvironment = result.environment && typeof result.environment === 'object'
835
- ? result.environment
836
- : {};
837
- result.environment = {
838
- ...existingEnvironment,
839
- host: {
840
- ...(existingEnvironment.host && typeof existingEnvironment.host === 'object' ? existingEnvironment.host : {}),
841
- platform: asNonEmptyString(existingEnvironment?.host?.platform) || hostEnvironment.platform,
842
- arch: asNonEmptyString(existingEnvironment?.host?.arch) || hostEnvironment.arch,
843
- nodeVersion: asNonEmptyString(existingEnvironment?.host?.nodeVersion) || hostEnvironment.nodeVersion,
844
- osRelease: asNonEmptyString(existingEnvironment?.host?.osRelease) || hostEnvironment.osRelease,
845
- cpuModel: asNonEmptyString(existingEnvironment?.host?.cpuModel) || hostEnvironment.cpuModel,
846
- },
847
- browser: {
848
- ...(existingEnvironment.browser && typeof existingEnvironment.browser === 'object' ? existingEnvironment.browser : {}),
849
- userAgent: asNonEmptyString(existingEnvironment?.browser?.userAgent) || asNonEmptyString(env.browserUserAgent),
850
- platform: asNonEmptyString(existingEnvironment?.browser?.platform) || asNonEmptyString(env.browserPlatform),
851
- language: asNonEmptyString(existingEnvironment?.browser?.language) || asNonEmptyString(env.browserLanguage),
852
- vendor: asNonEmptyString(existingEnvironment?.browser?.vendor) || asNonEmptyString(env.browserVendor),
853
- executable: asNonEmptyString(existingEnvironment?.browser?.executable) || asNonEmptyString(options.executablePath),
854
- channel: asNonEmptyString(existingEnvironment?.browser?.channel) || asNonEmptyString(options.channel),
855
- },
856
- gpu: {
857
- ...(existingEnvironment.gpu && typeof existingEnvironment.gpu === 'object' ? existingEnvironment.gpu : {}),
858
- api: asNonEmptyString(existingEnvironment?.gpu?.api) || 'webgpu',
859
- backend: normalizeWebgpuBackend(existingEnvironment?.gpu?.backend)
860
- || normalizeWebgpuBackend(env.webgpuBackend)
861
- || webgpuBackend,
862
- vendor: asNonEmptyString(existingEnvironment?.gpu?.vendor) || asNonEmptyString(deviceInfo.vendor),
863
- architecture: asNonEmptyString(existingEnvironment?.gpu?.architecture) || asNonEmptyString(deviceInfo.architecture),
864
- device: asNonEmptyString(existingEnvironment?.gpu?.device) || asNonEmptyString(deviceInfo.device),
865
- description: asNonEmptyString(existingEnvironment?.gpu?.description) || asNonEmptyString(deviceInfo.description),
866
- hasF16: typeof existingEnvironment?.gpu?.hasF16 === 'boolean'
867
- ? existingEnvironment.gpu.hasF16
868
- : (typeof deviceInfo.hasF16 === 'boolean' ? deviceInfo.hasF16 : null),
869
- hasSubgroups: typeof existingEnvironment?.gpu?.hasSubgroups === 'boolean'
870
- ? existingEnvironment.gpu.hasSubgroups
871
- : (typeof deviceInfo.hasSubgroups === 'boolean' ? deviceInfo.hasSubgroups : null),
872
- hasTimestampQuery: typeof existingEnvironment?.gpu?.hasTimestampQuery === 'boolean'
873
- ? existingEnvironment.gpu.hasTimestampQuery
874
- : (typeof deviceInfo.hasTimestampQuery === 'boolean' ? deviceInfo.hasTimestampQuery : null),
875
- },
876
- runtime: {
877
- ...(existingEnvironment.runtime && typeof existingEnvironment.runtime === 'object' ? existingEnvironment.runtime : {}),
878
- library: asNonEmptyString(existingEnvironment?.runtime?.library) || asNonEmptyString(env.library) || 'doppler',
879
- version: asNonEmptyString(existingEnvironment?.runtime?.version) || asNonEmptyString(env.version),
880
- surface: asNonEmptyString(existingEnvironment?.runtime?.surface) || asNonEmptyString(env.runtime) || 'browser',
881
- device: asNonEmptyString(existingEnvironment?.runtime?.device) || asNonEmptyString(env.device),
882
- dtype: asNonEmptyString(existingEnvironment?.runtime?.dtype) || asNonEmptyString(env.dtype),
883
- requestedDtype: asNonEmptyString(existingEnvironment?.runtime?.requestedDtype) || asNonEmptyString(env.requestedDtype),
884
- executionProviderMode: asNonEmptyString(existingEnvironment?.runtime?.executionProviderMode)
885
- || asNonEmptyString(env.executionProviderMode),
886
- cacheMode: asNonEmptyString(existingEnvironment?.runtime?.cacheMode)
887
- || asNonEmptyString(result.cacheMode)
888
- || asNonEmptyString(result?.timing?.cacheMode),
889
- loadMode: asNonEmptyString(existingEnvironment?.runtime?.loadMode)
890
- || asNonEmptyString(result.loadMode)
891
- || asNonEmptyString(result?.timing?.loadMode),
774
+ const response = await page.evaluate(async (payload) => {
775
+ if (typeof globalThis.__dopplerRunBrowserCommand !== 'function') {
776
+ throw new Error('browser command runner is missing globalThis.__dopplerRunBrowserCommand');
777
+ }
778
+ return globalThis.__dopplerRunBrowserCommand(payload.request, payload.options || {});
779
+ }, {
780
+ request: relayRequest,
781
+ options: {
782
+ runtimeLoadOptions: options.runtimeLoadOptions || {},
892
783
  },
893
- };
894
- }
784
+ });
895
785
 
896
- return response;
786
+ return finalizeBrowserRelayResponse(response, sourceRequest);
897
787
  } catch (error) {
898
788
  throw normalizeToToolingCommandError(error, {
899
789
  surface: 'browser',
900
- request,
790
+ request: sourceRequest,
901
791
  });
902
792
  } finally {
903
793
  if (context) {
@@ -913,7 +803,7 @@ export async function runBrowserCommandInNode(commandRequest, options = {}) {
913
803
  } catch (error) {
914
804
  throw normalizeToToolingCommandError(error, {
915
805
  surface: 'browser',
916
- request,
806
+ request: sourceRequest,
917
807
  });
918
808
  }
919
809
  }
@@ -28,6 +28,28 @@ function asOptionalPlainObject(value, label) {
28
28
  return value;
29
29
  }
30
30
 
31
+ function assertNoUnsupportedRuntimeInputs(request) {
32
+ const runtimeFields = [];
33
+ if (Array.isArray(request?.configChain) && request.configChain.length > 0) {
34
+ runtimeFields.push('configChain');
35
+ }
36
+ if (typeof request?.runtimePreset === 'string' && request.runtimePreset.trim()) {
37
+ runtimeFields.push('runtimePreset');
38
+ }
39
+ if (typeof request?.runtimeConfigUrl === 'string' && request.runtimeConfigUrl.trim()) {
40
+ runtimeFields.push('runtimeConfigUrl');
41
+ }
42
+ if (request?.runtimeConfig != null) {
43
+ runtimeFields.push('runtimeConfig');
44
+ }
45
+ if (runtimeFields.length > 0) {
46
+ throw new Error(
47
+ `${request.command} does not support runtime input fields on the node operator surface: ` +
48
+ `${runtimeFields.join(', ')}. Put those settings into the workload/config asset instead.`
49
+ );
50
+ }
51
+ }
52
+
31
53
  let runtimeModulesPromise = null;
32
54
 
33
55
  async function loadRuntimeModules() {
@@ -51,16 +73,19 @@ export function hasNodeWebGPUSupport() {
51
73
  }
52
74
 
53
75
  async function assertNodeWebGPUSupport() {
76
+ let bootstrapProvider = null;
54
77
  if (!hasNodeWebGPUSupport()) {
55
78
  const bootstrap = await bootstrapNodeWebGPU();
56
- if (bootstrap.ok && bootstrap.provider) {
57
- console.error(`[surface] node WebGPU provider: ${bootstrap.provider}`);
79
+ if (bootstrap.provider) {
80
+ bootstrapProvider = bootstrap.provider;
58
81
  }
59
82
  }
60
83
 
61
84
  if (hasNodeWebGPUSupport()) return;
62
85
  throw new Error(
63
- 'node command: WebGPU runtime is incomplete in Node. Run in browser relay, or run under a WebGPU-enabled Node build.'
86
+ 'node command: WebGPU runtime is incomplete in Node.' +
87
+ (bootstrapProvider ? ` Provider resolution stopped at "${bootstrapProvider}".` : '') +
88
+ ' Run in browser relay, or run under a WebGPU-enabled Node build.'
64
89
  );
65
90
  }
66
91
 
@@ -94,6 +119,7 @@ export async function runNodeCommand(commandRequest, options = {}) {
94
119
  if (request.command === 'lora' || request.command === 'distill') {
95
120
  const gpuOptionalActions = new Set(['compare', 'quality-gate', 'subsets']);
96
121
  installNodeFileFetchShim();
122
+ assertNoUnsupportedRuntimeInputs(request);
97
123
  if (!gpuOptionalActions.has(request.action)) {
98
124
  await assertNodeWebGPUSupport();
99
125
  }