@simulatte/doppler 0.1.3 → 0.1.5

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 (114) hide show
  1. package/README.md +11 -5
  2. package/package.json +27 -4
  3. package/src/client/doppler-api.browser.d.ts +1 -0
  4. package/src/client/doppler-api.browser.js +288 -0
  5. package/src/client/doppler-api.d.ts +80 -0
  6. package/src/client/doppler-api.js +298 -0
  7. package/src/client/doppler-provider/types.js +1 -1
  8. package/src/client/doppler-registry.d.ts +23 -0
  9. package/src/client/doppler-registry.js +88 -0
  10. package/src/client/doppler-registry.json +39 -0
  11. package/src/config/execution-contract-check.d.ts +82 -0
  12. package/src/config/execution-contract-check.js +317 -0
  13. package/src/config/execution-v0-contract-check.d.ts +94 -0
  14. package/src/config/execution-v0-contract-check.js +251 -0
  15. package/src/config/execution-v0-graph-contract-check.d.ts +20 -0
  16. package/src/config/execution-v0-graph-contract-check.js +64 -0
  17. package/src/config/kernel-path-contract-check.d.ts +76 -0
  18. package/src/config/kernel-path-contract-check.js +479 -0
  19. package/src/config/kernel-path-loader.d.ts +16 -0
  20. package/src/config/kernel-path-loader.js +54 -0
  21. package/src/config/kernels/kernel-ref-digests.js +12 -0
  22. package/src/config/kernels/registry.json +556 -0
  23. package/src/config/loader.js +90 -67
  24. package/src/config/merge-contract-check.d.ts +16 -0
  25. package/src/config/merge-contract-check.js +321 -0
  26. package/src/config/merge-helpers.d.ts +58 -0
  27. package/src/config/merge-helpers.js +54 -0
  28. package/src/config/merge.js +3 -6
  29. package/src/config/presets/models/janus-text.json +27 -0
  30. package/src/config/quantization-contract-check.d.ts +12 -0
  31. package/src/config/quantization-contract-check.js +91 -0
  32. package/src/config/required-inference-fields-contract-check.d.ts +24 -0
  33. package/src/config/required-inference-fields-contract-check.js +231 -0
  34. package/src/config/schema/browser-suite-metrics.schema.d.ts +17 -0
  35. package/src/config/schema/browser-suite-metrics.schema.js +46 -0
  36. package/src/config/schema/conversion-report.schema.d.ts +40 -0
  37. package/src/config/schema/conversion-report.schema.js +108 -0
  38. package/src/config/schema/doppler.schema.js +12 -18
  39. package/src/config/schema/index.d.ts +22 -0
  40. package/src/config/schema/index.js +18 -0
  41. package/src/converter/core.d.ts +10 -0
  42. package/src/converter/core.js +49 -11
  43. package/src/converter/parsers/diffusion.js +63 -3
  44. package/src/converter/tokenizer-utils.js +17 -3
  45. package/src/formats/rdrr/validation.js +13 -0
  46. package/src/gpu/kernels/depthwise_conv2d.d.ts +29 -0
  47. package/src/gpu/kernels/depthwise_conv2d.js +98 -0
  48. package/src/gpu/kernels/depthwise_conv2d.wgsl +58 -0
  49. package/src/gpu/kernels/depthwise_conv2d_f16.wgsl +62 -0
  50. package/src/gpu/kernels/grouped_pointwise_conv2d.d.ts +27 -0
  51. package/src/gpu/kernels/grouped_pointwise_conv2d.js +92 -0
  52. package/src/gpu/kernels/grouped_pointwise_conv2d.wgsl +47 -0
  53. package/src/gpu/kernels/grouped_pointwise_conv2d_f16.wgsl +51 -0
  54. package/src/gpu/kernels/index.d.ts +30 -0
  55. package/src/gpu/kernels/index.js +25 -0
  56. package/src/gpu/kernels/relu.d.ts +18 -0
  57. package/src/gpu/kernels/relu.js +45 -0
  58. package/src/gpu/kernels/relu.wgsl +21 -0
  59. package/src/gpu/kernels/relu_f16.wgsl +23 -0
  60. package/src/gpu/kernels/repeat_channels.d.ts +21 -0
  61. package/src/gpu/kernels/repeat_channels.js +60 -0
  62. package/src/gpu/kernels/repeat_channels.wgsl +29 -0
  63. package/src/gpu/kernels/repeat_channels_f16.wgsl +31 -0
  64. package/src/gpu/kernels/sana_linear_attention.d.ts +27 -0
  65. package/src/gpu/kernels/sana_linear_attention.js +122 -0
  66. package/src/gpu/kernels/sana_linear_attention_apply.wgsl +44 -0
  67. package/src/gpu/kernels/sana_linear_attention_apply_f16.wgsl +47 -0
  68. package/src/gpu/kernels/sana_linear_attention_summary.wgsl +47 -0
  69. package/src/gpu/kernels/sana_linear_attention_summary_f16.wgsl +49 -0
  70. package/src/index-browser.d.ts +1 -0
  71. package/src/index-browser.js +2 -1
  72. package/src/index.d.ts +1 -0
  73. package/src/index.js +2 -1
  74. package/src/inference/browser-harness.js +164 -38
  75. package/src/inference/pipelines/diffusion/init.js +14 -0
  76. package/src/inference/pipelines/diffusion/pipeline.js +206 -77
  77. package/src/inference/pipelines/diffusion/sana-transformer.d.ts +53 -0
  78. package/src/inference/pipelines/diffusion/sana-transformer.js +738 -0
  79. package/src/inference/pipelines/diffusion/scheduler.d.ts +17 -1
  80. package/src/inference/pipelines/diffusion/scheduler.js +91 -3
  81. package/src/inference/pipelines/diffusion/text-encoder-gpu.d.ts +6 -4
  82. package/src/inference/pipelines/diffusion/text-encoder-gpu.js +270 -0
  83. package/src/inference/pipelines/diffusion/text-encoder.js +18 -1
  84. package/src/inference/pipelines/diffusion/types.d.ts +4 -0
  85. package/src/inference/pipelines/diffusion/vae.js +782 -78
  86. package/src/inference/pipelines/text/config.d.ts +5 -0
  87. package/src/inference/pipelines/text/config.js +1 -1
  88. package/src/inference/pipelines/text/execution-v0.js +141 -101
  89. package/src/inference/pipelines/text/init.js +41 -10
  90. package/src/inference/pipelines/text.js +7 -1
  91. package/src/rules/execution-rules-contract-check.d.ts +17 -0
  92. package/src/rules/execution-rules-contract-check.js +245 -0
  93. package/src/rules/kernels/depthwise-conv2d.rules.json +6 -0
  94. package/src/rules/kernels/grouped-pointwise-conv2d.rules.json +6 -0
  95. package/src/rules/kernels/relu.rules.json +6 -0
  96. package/src/rules/kernels/repeat-channels.rules.json +6 -0
  97. package/src/rules/kernels/sana-linear-attention.rules.json +6 -0
  98. package/src/rules/layer-pattern-contract-check.d.ts +17 -0
  99. package/src/rules/layer-pattern-contract-check.js +231 -0
  100. package/src/rules/rule-registry.d.ts +28 -0
  101. package/src/rules/rule-registry.js +38 -0
  102. package/src/tooling/conversion-config-materializer.d.ts +24 -0
  103. package/src/tooling/conversion-config-materializer.js +99 -0
  104. package/src/tooling/lean-execution-contract-runner.d.ts +43 -0
  105. package/src/tooling/lean-execution-contract-runner.js +158 -0
  106. package/src/tooling/lean-execution-contract.d.ts +16 -0
  107. package/src/tooling/lean-execution-contract.js +81 -0
  108. package/src/tooling/node-convert.d.ts +10 -0
  109. package/src/tooling/node-converter.js +59 -0
  110. package/src/tooling/node-webgpu.js +30 -9
  111. package/src/version.d.ts +2 -0
  112. package/src/version.js +2 -0
  113. package/tools/convert-safetensors-node.js +47 -0
  114. package/tools/doppler-cli.js +167 -6
@@ -5,6 +5,7 @@ import {
5
5
  } from './schema/index.js';
6
6
  import { createDopplerError, ERROR_CODES } from '../errors/index.js';
7
7
  import { loadJson } from '../utils/load-json.js';
8
+ import { chooseNullish, mergeLayeredShallowObjects } from './merge-helpers.js';
8
9
 
9
10
  // Static imports keep presets bundled for browser use.
10
11
  const transformerPreset = await loadJson('./presets/models/transformer.json', import.meta.url, 'Failed to load preset');
@@ -14,6 +15,7 @@ const gemma3Preset = await loadJson('./presets/models/gemma3.json', import.meta.
14
15
  const translateGemmaPreset = await loadJson('./presets/models/translategemma.json', import.meta.url, 'Failed to load preset');
15
16
  const embeddingGemmaPreset = await loadJson('./presets/models/embeddinggemma.json', import.meta.url, 'Failed to load preset');
16
17
  const functiongemmaPreset = await loadJson('./presets/models/functiongemma.json', import.meta.url, 'Failed to load preset');
18
+ const janusTextPreset = await loadJson('./presets/models/janus-text.json', import.meta.url, 'Failed to load preset');
17
19
  const llama3Preset = await loadJson('./presets/models/llama3.json', import.meta.url, 'Failed to load preset');
18
20
  const mixtralPreset = await loadJson('./presets/models/mixtral.json', import.meta.url, 'Failed to load preset');
19
21
  const deepseekPreset = await loadJson('./presets/models/deepseek.json', import.meta.url, 'Failed to load preset');
@@ -36,6 +38,7 @@ export const PRESET_REGISTRY = {
36
38
  translategemma: translateGemmaPreset,
37
39
  embeddinggemma: embeddingGemmaPreset,
38
40
  functiongemma: functiongemmaPreset,
41
+ janus_text: janusTextPreset,
39
42
  llama3: llama3Preset,
40
43
  mixtral: mixtralPreset,
41
44
  deepseek: deepseekPreset,
@@ -85,6 +88,7 @@ export const PRESET_DETECTION_ORDER = [
85
88
  // Most specific first (model variants)
86
89
  'functiongemma',
87
90
  'embeddinggemma',
91
+ 'janus_text',
88
92
  'modernbert',
89
93
  'diffusion',
90
94
  // Model families (check more specific patterns first)
@@ -107,8 +111,9 @@ export function detectPreset(
107
111
  config,
108
112
  architecture
109
113
  ) {
114
+ const nestedTextConfig = getNestedTextConfig(config);
110
115
  const archLower = (architecture || '').toLowerCase();
111
- const modelType = (config.model_type || '').toLowerCase();
116
+ const modelType = String(nestedTextConfig?.model_type ?? config.model_type ?? '').toLowerCase();
112
117
  // Weak hint case: architecture fallback is often model_type copy (e.g. qwen2).
113
118
  const hintsAreWeak = !archLower || !modelType || archLower === modelType;
114
119
 
@@ -186,18 +191,21 @@ export function resolveConfig(
186
191
  // Note: Uses nullish coalesce (??) so null values fall through to next level.
187
192
  // This means explicit null in manifest = "use preset/default".
188
193
  const presetArch = preset.architecture || {};
189
- const numLayers = manifestArch.numLayers ?? presetArch.numLayers;
190
- const hiddenSize = manifestArch.hiddenSize ?? presetArch.hiddenSize;
191
- const intermediateSize = manifestArch.intermediateSize ?? presetArch.intermediateSize;
192
- const numAttentionHeads = manifestArch.numAttentionHeads ?? presetArch.numAttentionHeads;
193
- const numKeyValueHeads = manifestArch.numKeyValueHeads ?? presetArch.numKeyValueHeads ?? numAttentionHeads;
194
- const headDim = manifestArch.headDim ?? presetArch.headDim ?? (
195
- hiddenSize && numAttentionHeads ? hiddenSize / numAttentionHeads : undefined
194
+ const numLayers = chooseNullish(manifestArch.numLayers, presetArch.numLayers);
195
+ const hiddenSize = chooseNullish(manifestArch.hiddenSize, presetArch.hiddenSize);
196
+ const intermediateSize = chooseNullish(manifestArch.intermediateSize, presetArch.intermediateSize);
197
+ const numAttentionHeads = chooseNullish(manifestArch.numAttentionHeads, presetArch.numAttentionHeads);
198
+ const numKeyValueHeads = chooseNullish(
199
+ manifestArch.numKeyValueHeads,
200
+ chooseNullish(presetArch.numKeyValueHeads, numAttentionHeads)
196
201
  );
197
- const vocabSize = manifestArch.vocabSize ?? presetArch.vocabSize;
198
- const maxSeqLen = manifestArch.maxSeqLen ?? presetArch.maxSeqLen;
199
- const ropeTheta = manifestArch.ropeTheta ?? presetArch.ropeTheta;
200
- const rmsNormEps = manifestArch.rmsNormEps ?? presetArch.rmsNormEps;
202
+ const headDim = chooseNullish(manifestArch.headDim, chooseNullish(presetArch.headDim, (
203
+ hiddenSize && numAttentionHeads ? hiddenSize / numAttentionHeads : undefined
204
+ )));
205
+ const vocabSize = chooseNullish(manifestArch.vocabSize, presetArch.vocabSize);
206
+ const maxSeqLen = chooseNullish(manifestArch.maxSeqLen, presetArch.maxSeqLen);
207
+ const ropeTheta = chooseNullish(manifestArch.ropeTheta, presetArch.ropeTheta);
208
+ const rmsNormEps = chooseNullish(manifestArch.rmsNormEps, presetArch.rmsNormEps);
201
209
 
202
210
  const architecture = {
203
211
  numLayers,
@@ -222,44 +230,44 @@ export function resolveConfig(
222
230
  const manifestInference = extractInferenceFromConfig(manifest.config || {});
223
231
 
224
232
  const inference = {
225
- attention: {
226
- ...baseInference.attention,
227
- ...presetInference.attention,
228
- ...manifestInference.attention,
229
- },
230
- normalization: {
231
- ...baseInference.normalization,
232
- ...presetInference.normalization,
233
- },
234
- ffn: {
235
- ...baseInference.ffn,
236
- ...presetInference.ffn,
237
- },
238
- moe: presetInference.moe ?? baseInference.moe ?? null,
239
- output: {
240
- ...baseInference.output,
241
- ...presetInference.output,
242
- ...manifestInference.output,
243
- },
244
- layerPattern: presetInference.layerPattern ?? baseInference.layerPattern,
245
- rope: {
246
- ...baseInference.rope,
247
- ...presetInference.rope,
248
- ...manifestInference.rope,
249
- },
250
- pipeline: manifestInference.pipeline ?? presetInference.pipeline ?? baseInference.pipeline,
251
- chatTemplate: {
252
- ...baseInference.chatTemplate,
253
- ...presetInference.chatTemplate,
254
- },
255
- kernelPath: presetInference.kernelPath ?? baseInference.kernelPath,
233
+ attention: mergeLayeredShallowObjects(
234
+ baseInference.attention,
235
+ presetInference.attention,
236
+ manifestInference.attention
237
+ ),
238
+ normalization: mergeLayeredShallowObjects(
239
+ baseInference.normalization,
240
+ presetInference.normalization
241
+ ),
242
+ ffn: mergeLayeredShallowObjects(
243
+ baseInference.ffn,
244
+ presetInference.ffn
245
+ ),
246
+ moe: chooseNullish(presetInference.moe, chooseNullish(baseInference.moe, null)),
247
+ output: mergeLayeredShallowObjects(
248
+ baseInference.output,
249
+ presetInference.output,
250
+ manifestInference.output
251
+ ),
252
+ layerPattern: chooseNullish(presetInference.layerPattern, baseInference.layerPattern),
253
+ rope: mergeLayeredShallowObjects(
254
+ baseInference.rope,
255
+ presetInference.rope,
256
+ manifestInference.rope
257
+ ),
258
+ pipeline: chooseNullish(manifestInference.pipeline, chooseNullish(presetInference.pipeline, baseInference.pipeline)),
259
+ chatTemplate: mergeLayeredShallowObjects(
260
+ baseInference.chatTemplate,
261
+ presetInference.chatTemplate
262
+ ),
263
+ kernelPath: chooseNullish(presetInference.kernelPath, baseInference.kernelPath),
256
264
  };
257
265
 
258
266
  // Merge tokenizer config
259
- const tokenizer = {
260
- ...preset.tokenizer,
261
- ...extractTokenizerFromManifest(manifest),
262
- };
267
+ const tokenizer = mergeLayeredShallowObjects(
268
+ preset.tokenizer,
269
+ extractTokenizerFromManifest(manifest)
270
+ );
263
271
 
264
272
  // Sampling defaults
265
273
  const sampling = preset.sampling ?? {
@@ -317,41 +325,56 @@ function assertArchitecture(manifest, architecture) {
317
325
  // =============================================================================
318
326
 
319
327
  function extractArchitectureFromConfig(config) {
328
+ const nestedTextConfig = getNestedTextConfig(config);
320
329
  return {
321
- numLayers: config.num_hidden_layers ?? config.n_layer ?? config.blockCount,
322
- hiddenSize: config.hidden_size ?? config.n_embd ?? config.embeddingLength,
323
- intermediateSize: config.intermediate_size ?? config.n_inner ?? config.feedForwardLength,
324
- numAttentionHeads: config.num_attention_heads ?? config.n_head ?? config.attentionHeadCount,
325
- numKeyValueHeads: config.num_key_value_heads ?? config.attentionHeadCountKV,
326
- headDim: config.head_dim,
327
- vocabSize: config.vocab_size ?? config.vocabSize,
328
- maxSeqLen: config.max_position_embeddings ?? config.n_positions ?? config.contextLength,
329
- ropeTheta: config.rope_theta ?? config.ropeFreqBase,
330
- rmsNormEps: config.rms_norm_eps ?? config.attentionLayerNormRMSEpsilon,
330
+ numLayers: config.num_hidden_layers ?? nestedTextConfig?.num_hidden_layers ?? config.n_layer ?? config.blockCount,
331
+ hiddenSize: config.hidden_size ?? nestedTextConfig?.hidden_size ?? config.n_embd ?? config.embeddingLength,
332
+ intermediateSize: config.intermediate_size ?? nestedTextConfig?.intermediate_size ?? config.n_inner ?? config.feedForwardLength,
333
+ numAttentionHeads: config.num_attention_heads ?? nestedTextConfig?.num_attention_heads ?? config.n_head ?? config.attentionHeadCount,
334
+ numKeyValueHeads: config.num_key_value_heads ?? nestedTextConfig?.num_key_value_heads ?? config.attentionHeadCountKV,
335
+ headDim: config.head_dim ?? nestedTextConfig?.head_dim,
336
+ vocabSize: config.vocab_size ?? nestedTextConfig?.vocab_size ?? config.vocabSize,
337
+ maxSeqLen: config.max_position_embeddings ?? nestedTextConfig?.max_position_embeddings ?? config.n_positions ?? config.contextLength,
338
+ ropeTheta: config.rope_theta ?? nestedTextConfig?.rope_theta ?? config.ropeFreqBase,
339
+ rmsNormEps: config.rms_norm_eps ?? nestedTextConfig?.rms_norm_eps ?? config.attentionLayerNormRMSEpsilon,
331
340
  };
332
341
  }
333
342
 
334
343
  function extractInferenceFromConfig(config) {
344
+ const nestedTextConfig = getNestedTextConfig(config);
335
345
  return {
336
346
  attention: {
337
- slidingWindow: config.sliding_window,
338
- attnLogitSoftcapping: config.attn_logit_softcapping,
339
- attentionOutputGate: config.attn_output_gate,
347
+ slidingWindow: config.sliding_window ?? nestedTextConfig?.sliding_window,
348
+ attnLogitSoftcapping: config.attn_logit_softcapping ?? nestedTextConfig?.attn_logit_softcapping,
349
+ attentionOutputGate: config.attn_output_gate ?? nestedTextConfig?.attn_output_gate,
340
350
  },
341
351
  output: {
342
- finalLogitSoftcapping: config.final_logit_softcapping,
343
- tieWordEmbeddings: config.tie_word_embeddings,
344
- scaleEmbeddings: config.scale_embeddings,
352
+ finalLogitSoftcapping: config.final_logit_softcapping ?? nestedTextConfig?.final_logit_softcapping,
353
+ tieWordEmbeddings: config.tie_word_embeddings ?? nestedTextConfig?.tie_word_embeddings,
354
+ scaleEmbeddings: config.scale_embeddings ?? nestedTextConfig?.scale_embeddings,
345
355
  },
346
- pipeline: config.pipeline,
356
+ pipeline: config.pipeline ?? nestedTextConfig?.pipeline,
347
357
  rope: {
348
- ropeTheta: config.rope_theta ?? config.ropeFreqBase,
349
- ropeScalingType: config.rope_scaling_type,
350
- ropeScalingFactor: config.rope_scaling_factor,
358
+ ropeTheta: config.rope_theta ?? nestedTextConfig?.rope_theta ?? config.ropeFreqBase,
359
+ ropeScalingType: config.rope_scaling_type ?? nestedTextConfig?.rope_scaling_type,
360
+ ropeScalingFactor: config.rope_scaling_factor ?? nestedTextConfig?.rope_scaling_factor,
351
361
  },
352
362
  };
353
363
  }
354
364
 
365
+ function getNestedTextConfig(config) {
366
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
367
+ return null;
368
+ }
369
+ if (config.text_config && typeof config.text_config === 'object' && !Array.isArray(config.text_config)) {
370
+ return config.text_config;
371
+ }
372
+ if (config.language_config && typeof config.language_config === 'object' && !Array.isArray(config.language_config)) {
373
+ return config.language_config;
374
+ }
375
+ return null;
376
+ }
377
+
355
378
  function extractTokenizerFromManifest(manifest) {
356
379
  if (!manifest.tokenizer) return {};
357
380
 
@@ -0,0 +1,16 @@
1
+ export interface MergeContractCheckResult {
2
+ id: string;
3
+ ok: boolean;
4
+ detail: string;
5
+ mode: 'actual' | 'modeled';
6
+ }
7
+
8
+ export interface MergeContractArtifact {
9
+ schemaVersion: 1;
10
+ source: 'doppler';
11
+ ok: boolean;
12
+ checks: MergeContractCheckResult[];
13
+ errors: string[];
14
+ }
15
+
16
+ export declare function buildMergeContractArtifact(): MergeContractArtifact;
@@ -0,0 +1,321 @@
1
+ import { resolveConfig, resolvePreset } from './loader.js';
2
+ import {
3
+ chooseNullish,
4
+ chooseDefinedWithSource,
5
+ mergeExecutionPatchLists,
6
+ mergeKernelPathPolicy,
7
+ mergeLayeredShallowObjects,
8
+ mergeShallowObject,
9
+ replaceSubtree,
10
+ } from './merge-helpers.js';
11
+ import { mergeConfig } from './merge.js';
12
+ import { createDopplerConfig } from './schema/index.js';
13
+
14
+ function buildWitnessManifestForArchitecture() {
15
+ return {
16
+ modelId: 'merge-semantics-witness',
17
+ modelType: 'transformer',
18
+ architecture: {
19
+ numLayers: 2,
20
+ hiddenSize: 256,
21
+ intermediateSize: 512,
22
+ numAttentionHeads: 4,
23
+ numKeyValueHeads: 4,
24
+ headDim: 64,
25
+ vocabSize: 1024,
26
+ maxSeqLen: 2048,
27
+ ropeTheta: null,
28
+ rmsNormEps: 1e-6,
29
+ },
30
+ config: {},
31
+ };
32
+ }
33
+
34
+ function buildWitnessMergeManifest() {
35
+ return {
36
+ modelId: 'merge-overlay-witness',
37
+ inference: {
38
+ attention: {
39
+ queryPreAttnScalar: 1,
40
+ attentionBias: false,
41
+ attnLogitSoftcapping: null,
42
+ slidingWindow: 4096,
43
+ queryKeyNorm: false,
44
+ attentionOutputGate: false,
45
+ causal: true,
46
+ },
47
+ normalization: {
48
+ rmsNormEps: 1e-6,
49
+ rmsNormWeightOffset: 0,
50
+ postAttentionNorm: true,
51
+ preFeedforwardNorm: true,
52
+ postFeedforwardNorm: false,
53
+ },
54
+ ffn: {
55
+ activation: 'gelu',
56
+ gatedActivation: false,
57
+ swigluLimit: null,
58
+ },
59
+ rope: {
60
+ ropeTheta: 1000000,
61
+ ropeLocalTheta: null,
62
+ ropeScalingType: null,
63
+ ropeScalingFactor: null,
64
+ ropeLocalScalingType: null,
65
+ ropeLocalScalingFactor: null,
66
+ yarnBetaFast: null,
67
+ yarnBetaSlow: null,
68
+ yarnOriginalMaxPos: null,
69
+ ropeLocalYarnBetaFast: null,
70
+ ropeLocalYarnBetaSlow: null,
71
+ ropeLocalYarnOriginalMaxPos: null,
72
+ },
73
+ output: {
74
+ finalLogitSoftcapping: 30,
75
+ tieWordEmbeddings: true,
76
+ scaleEmbeddings: false,
77
+ embeddingTranspose: false,
78
+ embeddingVocabSize: 1024,
79
+ },
80
+ layerPattern: null,
81
+ chatTemplate: {
82
+ type: 'gemma',
83
+ enabled: true,
84
+ },
85
+ defaultKernelPath: 'gemma3-f16-fused-f16a-online',
86
+ },
87
+ architecture: {
88
+ headDim: 64,
89
+ maxSeqLen: 2048,
90
+ },
91
+ };
92
+ }
93
+
94
+ function recordCheck(results, id, ok, detail, mode = 'actual') {
95
+ results.push({ id, ok, detail, mode });
96
+ }
97
+
98
+ export function buildMergeContractArtifact() {
99
+ const checks = [];
100
+ const preset = resolvePreset('gemma3');
101
+ const resolved = resolveConfig(buildWitnessManifestForArchitecture(), 'gemma3');
102
+ recordCheck(
103
+ checks,
104
+ 'loader.architecture.nullish_null_falls_through',
105
+ resolved.architecture.ropeTheta === preset.architecture.ropeTheta,
106
+ `resolved ropeTheta=${resolved.architecture.ropeTheta}, preset ropeTheta=${preset.architecture.ropeTheta}`
107
+ );
108
+
109
+ const mergedUndefined = mergeConfig(buildWitnessMergeManifest(), {});
110
+ recordCheck(
111
+ checks,
112
+ 'runtime.mergeConfig.defined_overlay_missing_falls_through',
113
+ mergedUndefined.inference.defaultKernelPath === 'gemma3-f16-fused-f16a-online'
114
+ && mergedUndefined._sources.get('inference.defaultKernelPath') === 'manifest',
115
+ `value=${mergedUndefined.inference.defaultKernelPath}, source=${mergedUndefined._sources.get('inference.defaultKernelPath')}`
116
+ );
117
+
118
+ const mergedNull = mergeConfig(buildWitnessMergeManifest(), {
119
+ defaultKernelPath: null,
120
+ chatTemplate: {
121
+ enabled: null,
122
+ },
123
+ });
124
+ recordCheck(
125
+ checks,
126
+ 'runtime.mergeConfig.defined_overlay_preserves_null',
127
+ mergedNull.inference.defaultKernelPath === null
128
+ && mergedNull._sources.get('inference.defaultKernelPath') === 'runtime',
129
+ `value=${mergedNull.inference.defaultKernelPath}, source=${mergedNull._sources.get('inference.defaultKernelPath')}`
130
+ );
131
+ recordCheck(
132
+ checks,
133
+ 'runtime.inference.chatTemplate.spread_preserves_null',
134
+ mergedNull.inference.chatTemplate.enabled === null
135
+ && mergedNull._sources.get('inference.chatTemplate.enabled') === 'runtime',
136
+ `value=${String(mergedNull.inference.chatTemplate.enabled)}`
137
+ );
138
+
139
+ const runtimeConfig = createDopplerConfig({
140
+ runtime: {
141
+ inference: {
142
+ chatTemplate: {
143
+ enabled: null,
144
+ },
145
+ },
146
+ },
147
+ });
148
+ recordCheck(
149
+ checks,
150
+ 'runtime.schema.chatTemplate.spread_preserves_null',
151
+ runtimeConfig.runtime.inference.chatTemplate.enabled === null,
152
+ `value=${String(runtimeConfig.runtime.inference.chatTemplate.enabled)}`
153
+ );
154
+
155
+ const overlaySources = new Map();
156
+ const chosenRuntimeValue = chooseDefinedWithSource(
157
+ 'inference.defaultKernelPath',
158
+ null,
159
+ 'manifest-path',
160
+ overlaySources
161
+ );
162
+ recordCheck(
163
+ checks,
164
+ 'runtime.mergeHelpers.chooseDefinedWithSource.runtime_marks_source',
165
+ chosenRuntimeValue === null && overlaySources.get('inference.defaultKernelPath') === 'runtime',
166
+ `value=${String(chosenRuntimeValue)}, source=${overlaySources.get('inference.defaultKernelPath')}`,
167
+ 'actual'
168
+ );
169
+
170
+ const manifestSources = new Map();
171
+ const chosenManifestValue = chooseDefinedWithSource(
172
+ 'inference.defaultKernelPath',
173
+ undefined,
174
+ 'manifest-path',
175
+ manifestSources
176
+ );
177
+ recordCheck(
178
+ checks,
179
+ 'runtime.mergeHelpers.chooseDefinedWithSource.manifest_marks_source',
180
+ chosenManifestValue === 'manifest-path' && manifestSources.get('inference.defaultKernelPath') === 'manifest',
181
+ `value=${String(chosenManifestValue)}, source=${manifestSources.get('inference.defaultKernelPath')}`,
182
+ 'actual'
183
+ );
184
+
185
+ const executionPatchBase = {
186
+ set: [{ op: 'seed' }],
187
+ remove: ['old_step'],
188
+ add: [{ id: 'new_step' }],
189
+ };
190
+ const executionPatchOverride = {
191
+ set: null,
192
+ remove: [],
193
+ add: undefined,
194
+ };
195
+ const mergedExecutionPatch = {
196
+ ...mergeExecutionPatchLists(executionPatchBase, executionPatchOverride),
197
+ };
198
+ recordCheck(
199
+ checks,
200
+ 'runtime.inference.executionPatch.nullish_null_falls_through',
201
+ mergedExecutionPatch.set === executionPatchBase.set
202
+ && mergedExecutionPatch.add === executionPatchBase.add
203
+ && Array.isArray(mergedExecutionPatch.remove)
204
+ && mergedExecutionPatch.remove.length === 0,
205
+ `setLength=${mergedExecutionPatch.set.length}, removeLength=${mergedExecutionPatch.remove.length}, addLength=${mergedExecutionPatch.add.length}`,
206
+ 'actual'
207
+ );
208
+
209
+ const sessionBase = {
210
+ kvcache: {
211
+ layout: 'paged',
212
+ maxSeqLen: 8192,
213
+ kvDtype: 'f16',
214
+ },
215
+ decodeLoop: {
216
+ batchSize: 16,
217
+ disableCommandBatching: false,
218
+ },
219
+ };
220
+ const sessionOverride = {
221
+ kvcache: {
222
+ layout: 'tiered',
223
+ },
224
+ decodeLoop: {
225
+ batchSize: 1,
226
+ },
227
+ };
228
+ const mergedSession = {
229
+ kvcache: replaceSubtree(sessionOverride.kvcache, sessionBase.kvcache),
230
+ decodeLoop: replaceSubtree(sessionOverride.decodeLoop, sessionBase.decodeLoop),
231
+ };
232
+ recordCheck(
233
+ checks,
234
+ 'runtime.inference.session.subtree_override_replaces_base',
235
+ mergedSession.kvcache.layout === 'tiered'
236
+ && mergedSession.kvcache.maxSeqLen === undefined
237
+ && mergedSession.decodeLoop.batchSize === 1
238
+ && mergedSession.decodeLoop.disableCommandBatching === undefined,
239
+ `kvcacheKeys=${Object.keys(mergedSession.kvcache).join(',')}; decodeLoopKeys=${Object.keys(mergedSession.decodeLoop).join(',')}`,
240
+ 'actual'
241
+ );
242
+
243
+ const mergedChatTemplate = mergeShallowObject(
244
+ { type: 'base', enabled: true },
245
+ { enabled: null }
246
+ );
247
+ recordCheck(
248
+ checks,
249
+ 'runtime.mergeShallowObject.spread_preserves_null',
250
+ mergedChatTemplate.enabled === null && mergedChatTemplate.type === 'base',
251
+ `type=${mergedChatTemplate.type}, enabled=${String(mergedChatTemplate.enabled)}`,
252
+ 'actual'
253
+ );
254
+
255
+ const layeredAttention = mergeLayeredShallowObjects(
256
+ { slidingWindow: 4096, attentionBias: false },
257
+ { slidingWindow: 2048 },
258
+ { slidingWindow: null }
259
+ );
260
+ recordCheck(
261
+ checks,
262
+ 'loader.mergeLayeredShallowObjects.spread_preserves_null',
263
+ layeredAttention.slidingWindow === null && layeredAttention.attentionBias === false,
264
+ `slidingWindow=${String(layeredAttention.slidingWindow)}, attentionBias=${String(layeredAttention.attentionBias)}`,
265
+ 'actual'
266
+ );
267
+
268
+ const mergedKernelPathPolicy = mergeKernelPathPolicy(
269
+ {
270
+ mode: 'locked',
271
+ sourceScope: ['model', 'manifest'],
272
+ allowSources: ['model', 'manifest'],
273
+ onIncompatible: 'error',
274
+ },
275
+ {
276
+ allowSources: ['runtime', 'execution-v0'],
277
+ onIncompatible: 'remap',
278
+ }
279
+ );
280
+ recordCheck(
281
+ checks,
282
+ 'runtime.kernelPathPolicy.source_scope_mirrors_allow_sources',
283
+ Array.isArray(mergedKernelPathPolicy.sourceScope)
284
+ && Array.isArray(mergedKernelPathPolicy.allowSources)
285
+ && mergedKernelPathPolicy.sourceScope.length === 2
286
+ && mergedKernelPathPolicy.sourceScope[0] === 'runtime'
287
+ && mergedKernelPathPolicy.allowSources[1] === 'execution-v0'
288
+ && mergedKernelPathPolicy.onIncompatible === 'remap',
289
+ `sourceScope=${JSON.stringify(mergedKernelPathPolicy.sourceScope)}, allowSources=${JSON.stringify(mergedKernelPathPolicy.allowSources)}`,
290
+ 'actual'
291
+ );
292
+
293
+ const runtimeConfigWithKernelPathPolicy = createDopplerConfig({
294
+ runtime: {
295
+ inference: {
296
+ kernelPathPolicy: {
297
+ allowSources: ['runtime', 'execution-v0'],
298
+ },
299
+ },
300
+ },
301
+ });
302
+ recordCheck(
303
+ checks,
304
+ 'runtime.schema.kernelPathPolicy.helper_is_used',
305
+ Array.isArray(runtimeConfigWithKernelPathPolicy.runtime.inference.kernelPathPolicy.sourceScope)
306
+ && runtimeConfigWithKernelPathPolicy.runtime.inference.kernelPathPolicy.sourceScope[0] === 'runtime'
307
+ && runtimeConfigWithKernelPathPolicy.runtime.inference.kernelPathPolicy.allowSources[1] === 'execution-v0',
308
+ `policy=${JSON.stringify(runtimeConfigWithKernelPathPolicy.runtime.inference.kernelPathPolicy)}`,
309
+ 'actual'
310
+ );
311
+
312
+ const errors = checks.filter((entry) => !entry.ok).map((entry) => `[MergeContract] ${entry.id}: ${entry.detail}`);
313
+
314
+ return {
315
+ schemaVersion: 1,
316
+ source: 'doppler',
317
+ ok: errors.length === 0,
318
+ checks,
319
+ errors,
320
+ };
321
+ }
@@ -0,0 +1,58 @@
1
+ export declare function chooseNullish<T>(
2
+ overrideValue: T | null | undefined,
3
+ fallbackValue: T
4
+ ): T;
5
+
6
+ export declare function chooseDefined<T>(
7
+ overrideValue: T | undefined,
8
+ fallbackValue: T
9
+ ): T;
10
+
11
+ export declare function chooseDefinedWithSource<T>(
12
+ path: string,
13
+ overrideValue: T | undefined,
14
+ fallbackValue: T,
15
+ sources: Map<string, string> | null | undefined
16
+ ): T;
17
+
18
+ export declare function mergeShallowObject<T extends object>(
19
+ base: T,
20
+ override: Partial<T> | null | undefined
21
+ ): T;
22
+
23
+ export declare function mergeLayeredShallowObjects<T extends object>(
24
+ ...layers: Array<Partial<T> | null | undefined>
25
+ ): T;
26
+
27
+ export declare function replaceSubtree<T>(
28
+ overrideValue: T | null | undefined,
29
+ fallbackValue: T
30
+ ): T;
31
+
32
+ export declare function mergeKernelPathPolicy<T extends {
33
+ mode?: unknown;
34
+ sourceScope?: unknown;
35
+ allowSources?: unknown;
36
+ onIncompatible?: unknown;
37
+ }>(
38
+ basePolicy: T | null | undefined,
39
+ overridePolicy: T | null | undefined
40
+ ): {
41
+ mode: unknown;
42
+ sourceScope: unknown;
43
+ allowSources: unknown;
44
+ onIncompatible: unknown;
45
+ };
46
+
47
+ export declare function mergeExecutionPatchLists<T extends {
48
+ set?: unknown;
49
+ remove?: unknown;
50
+ add?: unknown;
51
+ }>(
52
+ basePatch: T | null | undefined,
53
+ overridePatch: T | null | undefined
54
+ ): {
55
+ set: unknown;
56
+ remove: unknown;
57
+ add: unknown;
58
+ };
@@ -0,0 +1,54 @@
1
+ export function chooseNullish(overrideValue, fallbackValue) {
2
+ return overrideValue ?? fallbackValue;
3
+ }
4
+
5
+ export function chooseDefined(overrideValue, fallbackValue) {
6
+ return overrideValue !== undefined ? overrideValue : fallbackValue;
7
+ }
8
+
9
+ export function chooseDefinedWithSource(path, overrideValue, fallbackValue, sources) {
10
+ const value = chooseDefined(overrideValue, fallbackValue);
11
+ if (sources && typeof sources.set === 'function') {
12
+ sources.set(path, overrideValue !== undefined ? 'runtime' : 'manifest');
13
+ }
14
+ return value;
15
+ }
16
+
17
+ export function mergeShallowObject(base, override) {
18
+ if (!override || typeof override !== 'object' || Array.isArray(override)) {
19
+ return base;
20
+ }
21
+ return { ...base, ...override };
22
+ }
23
+
24
+ export function mergeLayeredShallowObjects(...layers) {
25
+ return layers.reduce((merged, layer) => mergeShallowObject(merged, layer), {});
26
+ }
27
+
28
+ export function replaceSubtree(overrideValue, fallbackValue) {
29
+ return chooseNullish(overrideValue, fallbackValue);
30
+ }
31
+
32
+ export function mergeKernelPathPolicy(basePolicy, overridePolicy) {
33
+ const base = basePolicy ?? {};
34
+ const override = overridePolicy ?? {};
35
+ const baseSourceScope = base.sourceScope ?? base.allowSources;
36
+ const overrideSourceScope = override.sourceScope ?? override.allowSources;
37
+ const sourceScope = overrideSourceScope ?? baseSourceScope;
38
+ return {
39
+ mode: override.mode ?? base.mode,
40
+ sourceScope,
41
+ allowSources: sourceScope,
42
+ onIncompatible: override.onIncompatible ?? base.onIncompatible,
43
+ };
44
+ }
45
+
46
+ export function mergeExecutionPatchLists(basePatch, overridePatch) {
47
+ const base = basePatch ?? {};
48
+ const override = overridePatch ?? {};
49
+ return {
50
+ set: chooseNullish(override.set, chooseNullish(base.set, [])),
51
+ remove: chooseNullish(override.remove, chooseNullish(base.remove, [])),
52
+ add: chooseNullish(override.add, chooseNullish(base.add, [])),
53
+ };
54
+ }