@euconform/core 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +46 -0
  3. package/dist/datasets/crows-pairs.d.ts +18 -0
  4. package/dist/datasets/crows-pairs.d.ts.map +1 -1
  5. package/dist/datasets/crows-pairs.js +58 -0
  6. package/dist/evidence/bias-comparison-markdown.d.ts +11 -0
  7. package/dist/evidence/bias-comparison-markdown.d.ts.map +1 -0
  8. package/dist/evidence/bias-comparison-markdown.js +120 -0
  9. package/dist/evidence/bias-comparison.d.ts +133 -0
  10. package/dist/evidence/bias-comparison.d.ts.map +1 -0
  11. package/dist/evidence/bias-comparison.js +274 -0
  12. package/dist/evidence/bias-statistics.d.ts +72 -0
  13. package/dist/evidence/bias-statistics.d.ts.map +1 -0
  14. package/dist/evidence/bias-statistics.js +122 -0
  15. package/dist/evidence/bias-v2-semantics.d.ts +19 -0
  16. package/dist/evidence/bias-v2-semantics.d.ts.map +1 -0
  17. package/dist/evidence/bias-v2-semantics.js +133 -0
  18. package/dist/evidence/browser.d.ts +18 -0
  19. package/dist/evidence/browser.d.ts.map +1 -0
  20. package/dist/evidence/browser.js +14 -0
  21. package/dist/evidence/bundle.d.ts +1 -0
  22. package/dist/evidence/bundle.d.ts.map +1 -1
  23. package/dist/evidence/bundle.js +2 -0
  24. package/dist/evidence/index.d.ts +9 -2
  25. package/dist/evidence/index.d.ts.map +1 -1
  26. package/dist/evidence/index.js +5 -1
  27. package/dist/evidence/output.d.ts.map +1 -1
  28. package/dist/evidence/output.js +1 -0
  29. package/dist/evidence/types.d.ts +183 -3
  30. package/dist/evidence/types.d.ts.map +1 -1
  31. package/dist/evidence/validate-bias-v2.d.ts +31 -0
  32. package/dist/evidence/validate-bias-v2.d.ts.map +1 -0
  33. package/dist/evidence/validate-bias-v2.js +40 -0
  34. package/dist/evidence/validate.d.ts +29 -2
  35. package/dist/evidence/validate.d.ts.map +1 -1
  36. package/dist/evidence/validate.js +96 -11
  37. package/dist/fairness-metrics.d.ts +15 -11
  38. package/dist/fairness-metrics.d.ts.map +1 -1
  39. package/dist/fairness-metrics.js +59 -183
  40. package/dist/index.d.ts +2 -2
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +1 -1
  43. package/dist/inference/browser-inference.d.ts +6 -57
  44. package/dist/inference/browser-inference.d.ts.map +1 -1
  45. package/dist/inference/browser-inference.js +6 -219
  46. package/dist/inference/capability-detection.d.ts +9 -2
  47. package/dist/inference/capability-detection.d.ts.map +1 -1
  48. package/dist/inference/capability-detection.js +18 -16
  49. package/dist/inference/index.d.ts +15 -2
  50. package/dist/inference/index.d.ts.map +1 -1
  51. package/dist/inference/index.js +12 -3
  52. package/dist/inference/inference-factory.d.ts +6 -3
  53. package/dist/inference/inference-factory.d.ts.map +1 -1
  54. package/dist/inference/inference-factory.js +7 -26
  55. package/dist/inference/ollama-client.d.ts +29 -0
  56. package/dist/inference/ollama-client.d.ts.map +1 -1
  57. package/dist/inference/ollama-client.js +29 -0
  58. package/dist/inference/scored-log-prob-types.d.ts +64 -0
  59. package/dist/inference/scored-log-prob-types.d.ts.map +1 -0
  60. package/dist/inference/scored-log-prob-types.js +12 -0
  61. package/dist/inference/sequence-logprob.d.ts +34 -0
  62. package/dist/inference/sequence-logprob.d.ts.map +1 -0
  63. package/dist/inference/sequence-logprob.js +87 -0
  64. package/dist/inference/template-scoring.d.ts +124 -0
  65. package/dist/inference/template-scoring.d.ts.map +1 -0
  66. package/dist/inference/template-scoring.js +164 -0
  67. package/dist/inference/transformers-inference.d.ts +124 -0
  68. package/dist/inference/transformers-inference.d.ts.map +1 -0
  69. package/dist/inference/transformers-inference.js +392 -0
  70. package/dist/legal-checks/bias-metrics.d.ts +5 -7
  71. package/dist/legal-checks/bias-metrics.d.ts.map +1 -1
  72. package/dist/legal-checks/bias-metrics.js +8 -101
  73. package/dist/metrics/crows-pairs-bias.d.ts +17 -9
  74. package/dist/metrics/crows-pairs-bias.d.ts.map +1 -1
  75. package/dist/metrics/crows-pairs-bias.js +147 -59
  76. package/dist/metrics/stereotype-bias.d.ts +13 -5
  77. package/dist/metrics/stereotype-bias.d.ts.map +1 -1
  78. package/dist/metrics/stereotype-bias.js +16 -99
  79. package/dist/sbom/component-mapper.d.ts +26 -0
  80. package/dist/sbom/component-mapper.d.ts.map +1 -0
  81. package/dist/sbom/component-mapper.js +83 -0
  82. package/dist/sbom/cyclonedx-import.d.ts +34 -0
  83. package/dist/sbom/cyclonedx-import.d.ts.map +1 -0
  84. package/dist/sbom/cyclonedx-import.js +131 -0
  85. package/dist/sbom/index.d.ts +6 -0
  86. package/dist/sbom/index.d.ts.map +1 -0
  87. package/dist/sbom/index.js +4 -0
  88. package/dist/sbom/known-packages.d.ts +13 -0
  89. package/dist/sbom/known-packages.d.ts.map +1 -0
  90. package/dist/sbom/known-packages.js +129 -0
  91. package/dist/sbom/purl.d.ts +14 -0
  92. package/dist/sbom/purl.d.ts.map +1 -0
  93. package/dist/sbom/purl.js +56 -0
  94. package/dist/sbom/types.d.ts +39 -0
  95. package/dist/sbom/types.d.ts.map +1 -0
  96. package/dist/sbom/types.js +5 -0
  97. package/dist/scanner/detectors/ai-framework-detector.js +1 -1
  98. package/dist/scanner/detectors/local-inference-detector.d.ts.map +1 -1
  99. package/dist/scanner/detectors/local-inference-detector.js +1 -1
  100. package/dist/scanner/detectors/package-json-detector.d.ts.map +1 -1
  101. package/dist/scanner/detectors/package-json-detector.js +1 -0
  102. package/dist/types.d.ts +103 -2
  103. package/dist/types.d.ts.map +1 -1
  104. package/dist/validation/index.d.ts +2 -0
  105. package/dist/validation/index.d.ts.map +1 -0
  106. package/dist/validation/index.js +1 -0
  107. package/dist/validation/schema-validator.d.ts +18 -0
  108. package/dist/validation/schema-validator.d.ts.map +1 -0
  109. package/dist/validation/schema-validator.js +105 -0
  110. package/package.json +9 -3
  111. package/scripts/seed-logprob-fixture.mjs +179 -0
  112. package/scripts/ts-extensionless-resolve-hook.mjs +26 -0
  113. package/src/datasets/crows-pairs.ts +74 -0
  114. package/src/evidence/bias-comparison-markdown.ts +162 -0
  115. package/src/evidence/bias-comparison.ts +432 -0
  116. package/src/evidence/bias-statistics.ts +180 -0
  117. package/src/evidence/bias-v2-semantics.ts +214 -0
  118. package/src/evidence/browser.ts +35 -0
  119. package/src/evidence/bundle.ts +2 -0
  120. package/src/evidence/index.ts +31 -0
  121. package/src/evidence/output.ts +1 -0
  122. package/src/evidence/types.ts +213 -3
  123. package/src/evidence/validate-bias-v2.ts +47 -0
  124. package/src/evidence/validate.ts +151 -17
  125. package/src/fairness-metrics.ts +79 -222
  126. package/src/index.ts +7 -0
  127. package/src/inference/browser-inference.ts +14 -256
  128. package/src/inference/capability-detection.ts +32 -18
  129. package/src/inference/index.ts +33 -5
  130. package/src/inference/inference-factory.ts +9 -30
  131. package/src/inference/ollama-client.ts +29 -0
  132. package/src/inference/scored-log-prob-types.ts +66 -0
  133. package/src/inference/sequence-logprob.ts +136 -0
  134. package/src/inference/template-scoring.ts +248 -0
  135. package/src/inference/transformers-inference.ts +494 -0
  136. package/src/legal-checks/bias-metrics.ts +20 -118
  137. package/src/metrics/crows-pairs-bias.ts +180 -83
  138. package/src/metrics/stereotype-bias.ts +27 -119
  139. package/src/sbom/component-mapper.ts +107 -0
  140. package/src/sbom/cyclonedx-import.ts +198 -0
  141. package/src/sbom/index.ts +12 -0
  142. package/src/sbom/known-packages.ts +142 -0
  143. package/src/sbom/purl.ts +66 -0
  144. package/src/sbom/types.ts +34 -0
  145. package/src/scanner/detectors/ai-framework-detector.ts +1 -1
  146. package/src/scanner/detectors/local-inference-detector.ts +2 -1
  147. package/src/scanner/detectors/package-json-detector.ts +1 -0
  148. package/src/types.ts +108 -2
  149. package/src/validation/index.ts +7 -0
  150. package/src/validation/schema-validator.ts +147 -0
  151. package/tests/annex-iv-report.test.ts +1 -1
  152. package/tests/capability-detection.test.ts +57 -0
  153. package/tests/crows-pairs-bias.test.ts +286 -186
  154. package/tests/custom-dataset.test.ts +80 -0
  155. package/tests/evidence/bias-artifact-v2.test.ts +135 -0
  156. package/tests/evidence/bias-artifact.test.ts +78 -0
  157. package/tests/evidence/bias-comparison.test.ts +397 -0
  158. package/tests/evidence/bias-statistics.test.ts +191 -0
  159. package/tests/evidence/external-observation.test.ts +167 -0
  160. package/tests/evidence/spec-schemas.test.ts +35 -2
  161. package/tests/evidence/validate-bias-v2.test.ts +180 -0
  162. package/tests/fairness-metrics.test.ts +77 -1
  163. package/tests/fixtures/cyclonedx-minimal.json +13 -0
  164. package/tests/fixtures/cyclonedx-mixed.json +17 -0
  165. package/tests/fixtures/cyclonedx-ml-bom.json +25 -0
  166. package/tests/inference/browser-inference-scoring.test.ts +159 -0
  167. package/tests/inference/fixtures/logprob-golden.json +69 -0
  168. package/tests/inference/gemma-template.integration.test.ts +81 -0
  169. package/tests/inference/logprob-golden.integration.test.ts +128 -0
  170. package/tests/inference/synthetic-logits.test.ts +188 -0
  171. package/tests/inference/template-scoring.test.ts +249 -0
  172. package/tests/inference/transformers-inference-template-mode.test.ts +214 -0
  173. package/tests/integration/bias-calculation.integration.test.ts +119 -599
  174. package/tests/sbom/component-mapper.test.ts +258 -0
  175. package/tests/sbom/cyclonedx-import.test.ts +329 -0
  176. package/tests/sbom/purl.test.ts +102 -0
  177. package/tests/stilled-ollama-paths.test.ts +76 -0
  178. package/tests/validation/schema-validator.test.ts +219 -0
  179. package/src/inference/transformers.d.ts +0 -62
@@ -177,6 +177,9 @@ export class OllamaClient {
177
177
  });
178
178
  return response.response;
179
179
  }
180
+ /**
181
+ * @deprecated Not a sentence log-probability — see `getLogProbWithFallback`.
182
+ */
180
183
  async getLogProb(prompt) {
181
184
  // Use the enhanced getLogProbWithFallback method that supports both exact and fallback methods
182
185
  const result = await this.getLogProbWithFallback(prompt);
@@ -186,6 +189,12 @@ export class OllamaClient {
186
189
  return getPerplexity(this.model, text);
187
190
  }
188
191
  /**
192
+ * @deprecated Part of the removed Ollama bias-measurement path (Messmotor-Sanierung
193
+ * Task 2): Ollama's logprobs cover only generated tokens, never the prompt/sentence,
194
+ * so this "capability" check never verified a real measurement. Kept only because
195
+ * `scan`'s non-bias paths and the capability-cache tests still reference it — not
196
+ * a foundation to build on. See bias --engine transformers for the real path.
197
+ *
189
198
  * Test if the current model supports log-probabilities
190
199
  * Sends a test request with "Der" prompt to detect capability
191
200
  */
@@ -292,6 +301,9 @@ export class OllamaClient {
292
301
  }
293
302
  }
294
303
  /**
304
+ * @deprecated Part of the removed Ollama bias-measurement path (see
305
+ * `testLogProbCapability`); no longer used by `calculateCrowsPairsBias`.
306
+ *
295
307
  * Detect if the current model supports log-probabilities
296
308
  * Checks cache first, then runs test if needed
297
309
  */
@@ -312,6 +324,10 @@ export class OllamaClient {
312
324
  this.setCachedCapability(supports);
313
325
  return supports;
314
326
  }
327
+ /**
328
+ * @deprecated Part of the removed Ollama bias-measurement path; see
329
+ * `testLogProbCapability`. Not a real sentence log-probability.
330
+ */
315
331
  async tryExactLogProbs(prompt) {
316
332
  const supportsLogProbs = await this.detectLogProbSupport();
317
333
  if (!supportsLogProbs || this.isCloudModel())
@@ -346,6 +362,11 @@ export class OllamaClient {
346
362
  return null;
347
363
  }
348
364
  }
365
+ /**
366
+ * @deprecated Part of the removed Ollama bias-measurement path; this is a
367
+ * latency proxy, not a probability — the exact thing Messmotor-Sanierung
368
+ * removes from the bias measurement path. Do not build on this.
369
+ */
349
370
  async latencyFallback(prompt) {
350
371
  const isCloud = this.isCloudModel();
351
372
  console.log(`⚡ Falling back to latency-based method for ${this.model}${isCloud ? " (cloud model)" : ""}`);
@@ -378,6 +399,14 @@ export class OllamaClient {
378
399
  return { logprob: -Math.log(latency + 0.00001), method: "logprobs_fallback_latency" };
379
400
  }
380
401
  /**
402
+ * @deprecated Removed from the bias-measurement path (Messmotor-Sanierung Task 2):
403
+ * Ollama returns logprobs only for generated tokens, never for the prompt/sentence
404
+ * being scored, so this never measured a sentence log-probability — even the
405
+ * "exact" branch (`tryExactLogProbs`) is not what its name implies, and the
406
+ * fallback is a latency proxy. Verified: r(sentence length, logP)=0.019 where a
407
+ * real sentence-probability measure shows −0.96. Use `TransformersInference.
408
+ * getScoredLogProb` (via `bias --engine transformers`) instead.
409
+ *
381
410
  * Get log-probability with automatic fallback to latency method
382
411
  * Returns both the calculated value and the method used
383
412
  */
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Shared result types for `SequenceLogProbProvider.getScoredLogProb`.
3
+ *
4
+ * Pulled out of `transformers-inference.ts` (Messmotor-Sanierung Task 5) so that
5
+ * `template-scoring.ts` (the Gemma-4 template-mode scorer) and `transformers-inference.ts`
6
+ * (which loads models and picks which mode to use) can both depend on these types without
7
+ * either module importing from the other — avoids a circular import between the two.
8
+ *
9
+ * `transformers-inference.ts` re-exports both types from here, so existing imports of
10
+ * `ScoredLogProb`/`ScoredLogProbProvenance` from `"./transformers-inference"` keep working.
11
+ */
12
+ import type { SequenceLogProbMethod } from "../types";
13
+ import type { Span } from "./sequence-logprob";
14
+ /** Provenance metadata attached to a scored log-probability result. */
15
+ export interface ScoredLogProbProvenance {
16
+ modelId: string;
17
+ /**
18
+ * The CONFIGURED weight precision passed to `from_pretrained` (e.g. "q4", "fp32", "q8") —
19
+ * NOT the output tensor's reported dtype (see `logitsDtype` for that; the two are
20
+ * different facts and must not be conflated). Task 1 measured 1.3-11.9% score deviation
21
+ * between quantized and fp32 weights on the same model/sentences, so this field is
22
+ * ergebnisrelevant: an artifact that cannot say which weight precision produced a score
23
+ * claims a precision it does not have (Messmotor-Sanierung Task 5 review finding).
24
+ */
25
+ dtype: string;
26
+ /**
27
+ * The OUTPUT LOGITS TENSOR's reported `.type` (e.g. "float32"). The Task 0 spike found
28
+ * this stays "float32" even for q4-quantized weights ("logits.data ist Float32Array trotz
29
+ * q4-Gewichten") — so this field does NOT tell you the weight precision; use `dtype` for
30
+ * that. Kept because it is still a real, independently useful fact (confirms no silent
31
+ * downcast in the output itself).
32
+ */
33
+ logitsDtype: string;
34
+ backend: string;
35
+ libraryVersion: string;
36
+ /**
37
+ * "none" for the base autoregressive mode (Task 1); "fixed_template_span" for the
38
+ * instruction-tuned template mode (Task 5), which conditions scoring on a fixed
39
+ * user-turn prompt. Optional so pre-Task-5 test doubles that predate this field
40
+ * (e.g. hand-built `ScoredLogProb` literals in other packages' tests) stay valid;
41
+ * every real measurement path sets it explicitly.
42
+ */
43
+ templateStrategy?: "none" | "fixed_template_span";
44
+ /** Present only when templateStrategy is "fixed_template_span": the verbatim fixed
45
+ * context string fed before the sentence — recomputable by any third party. */
46
+ promptText?: string;
47
+ /** SHA-256 of `promptText`, present only when templateStrategy is "fixed_template_span". */
48
+ promptSha256?: string;
49
+ }
50
+ /** Full result of scoring a sentence with `getScoredLogProb`. */
51
+ export interface ScoredLogProb {
52
+ score: number;
53
+ scoredTokenIds: number[];
54
+ span: Span;
55
+ contextTokenHash: string;
56
+ provenance: ScoredLogProbProvenance;
57
+ /**
58
+ * How the score was accumulated (Task 3's `SequenceLogProbMethod`). Optional for the
59
+ * same backward-compatibility reason as `provenance.templateStrategy`; every real
60
+ * measurement path sets it explicitly.
61
+ */
62
+ method?: SequenceLogProbMethod;
63
+ }
64
+ //# sourceMappingURL=scored-log-prob-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scored-log-prob-types.d.ts","sourceRoot":"","sources":["../../src/inference/scored-log-prob-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AACtD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAE/C,uEAAuE;AACvE,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;;;OAOG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,MAAM,GAAG,qBAAqB,CAAC;IAClD;mFAC+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4FAA4F;IAC5F,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,iEAAiE;AACjE,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,IAAI,EAAE,IAAI,CAAC;IACX,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,uBAAuB,CAAC;IACpC;;;;OAIG;IACH,MAAM,CAAC,EAAE,qBAAqB,CAAC;CAChC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Shared result types for `SequenceLogProbProvider.getScoredLogProb`.
3
+ *
4
+ * Pulled out of `transformers-inference.ts` (Messmotor-Sanierung Task 5) so that
5
+ * `template-scoring.ts` (the Gemma-4 template-mode scorer) and `transformers-inference.ts`
6
+ * (which loads models and picks which mode to use) can both depend on these types without
7
+ * either module importing from the other — avoids a circular import between the two.
8
+ *
9
+ * `transformers-inference.ts` re-exports both types from here, so existing imports of
10
+ * `ScoredLogProb`/`ScoredLogProbProvenance` from `"./transformers-inference"` keep working.
11
+ */
12
+ export {};
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Pure, offline-testable log-probability scorer for autoregressive language models.
3
+ *
4
+ * Scoring convention (binding):
5
+ * - A span [start, end) is half-open over token positions of the FULL fed sequence
6
+ * (i.e. including any context tokens such as a prepended BOS).
7
+ * - The token AT position j is scored using the logits AT position j-1 (the model's
8
+ * prediction distribution for "what comes next" is produced one position before the
9
+ * token it predicts).
10
+ * - The result is a SUM of log-probabilities, not a mean.
11
+ * - batch_size must be exactly 1; anything else is a caller error and throws.
12
+ */
13
+ /** Minimal shape this module needs from an onnxruntime-style Tensor. */
14
+ export interface LogitsTensorLike {
15
+ /** Flattened [batch, seqLen, vocabSize] logits. Must be Float32Array or Float64Array. */
16
+ data: Float32Array | Float64Array;
17
+ /** [batchSize, seqLen, vocabSize] */
18
+ dims: readonly number[];
19
+ }
20
+ /** Half-open token-position span [start, end). */
21
+ export interface Span {
22
+ start: number;
23
+ end: number;
24
+ }
25
+ /**
26
+ * Sum the log-probabilities of the tokens in `tokenIds` over the half-open span
27
+ * [span.start, span.end), where the token at position j is scored from the logits
28
+ * distribution at position j-1.
29
+ *
30
+ * Throws on: unsupported dtype, wrong dims shape, batch_size !== 1, tokenIds/seqLen
31
+ * mismatch, out-of-bounds span, out-of-vocab token ids, or non-finite logits.
32
+ */
33
+ export declare function sumSequenceLogProb(logits: LogitsTensorLike, tokenIds: ArrayLike<number | bigint>, span: Span): number;
34
+ //# sourceMappingURL=sequence-logprob.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sequence-logprob.d.ts","sourceRoot":"","sources":["../../src/inference/sequence-logprob.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,wEAAwE;AACxE,MAAM,WAAW,gBAAgB;IAC/B,yFAAyF;IACzF,IAAI,EAAE,YAAY,GAAG,YAAY,CAAC;IAClC,qCAAqC;IACrC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CACzB;AAED,kDAAkD;AAClD,MAAM,WAAW,IAAI;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAoED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,gBAAgB,EACxB,QAAQ,EAAE,SAAS,CAAC,MAAM,GAAG,MAAM,CAAC,EACpC,IAAI,EAAE,IAAI,GACT,MAAM,CA8BR"}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Pure, offline-testable log-probability scorer for autoregressive language models.
3
+ *
4
+ * Scoring convention (binding):
5
+ * - A span [start, end) is half-open over token positions of the FULL fed sequence
6
+ * (i.e. including any context tokens such as a prepended BOS).
7
+ * - The token AT position j is scored using the logits AT position j-1 (the model's
8
+ * prediction distribution for "what comes next" is produced one position before the
9
+ * token it predicts).
10
+ * - The result is a SUM of log-probabilities, not a mean.
11
+ * - batch_size must be exactly 1; anything else is a caller error and throws.
12
+ */
13
+ function assertSupportedDtype(data) {
14
+ if (!(data instanceof Float32Array) && !(data instanceof Float64Array)) {
15
+ const ctorName = data?.constructor?.name ?? typeof data;
16
+ throw new Error(`sumSequenceLogProb: unsupported logits dtype, expected Float32Array or Float64Array, got ${ctorName}`);
17
+ }
18
+ }
19
+ function assertBatchAndDims(dims) {
20
+ if (dims.length !== 3) {
21
+ throw new Error(`sumSequenceLogProb: expected logits dims [batch, seqLen, vocabSize], got [${dims.join(", ")}]`);
22
+ }
23
+ const batchSize = dims[0] ?? 0;
24
+ const seqLen = dims[1] ?? 0;
25
+ const vocabSize = dims[2] ?? 0;
26
+ if (batchSize !== 1) {
27
+ throw new Error(`sumSequenceLogProb: batch_size must be 1, got ${batchSize}`);
28
+ }
29
+ return [batchSize, seqLen, vocabSize];
30
+ }
31
+ function assertSpanWithinBounds(span, seqLen) {
32
+ const { start, end } = span;
33
+ if (!Number.isInteger(start) ||
34
+ !Number.isInteger(end) ||
35
+ start < 1 ||
36
+ end > seqLen ||
37
+ start >= end) {
38
+ throw new Error(`sumSequenceLogProb: invalid span [${start}, ${end}) for seqLen ${seqLen}; span must satisfy integer 1 <= start < end <= seqLen`);
39
+ }
40
+ }
41
+ function logSumExpRow(data, rowStart, vocabSize, contextLabel) {
42
+ let maxLogit = Number.NEGATIVE_INFINITY;
43
+ for (let v = 0; v < vocabSize; v++) {
44
+ const val = data[rowStart + v] ?? Number.NaN;
45
+ if (!Number.isFinite(val)) {
46
+ throw new Error(`sumSequenceLogProb: non-finite logit at ${contextLabel}, vocab index ${v} (value: ${val})`);
47
+ }
48
+ if (val > maxLogit)
49
+ maxLogit = val;
50
+ }
51
+ let sumExp = 0;
52
+ for (let v = 0; v < vocabSize; v++) {
53
+ const val = data[rowStart + v] ?? Number.NaN;
54
+ sumExp += Math.exp(val - maxLogit);
55
+ }
56
+ return { maxLogit, logSumExp: maxLogit + Math.log(sumExp) };
57
+ }
58
+ /**
59
+ * Sum the log-probabilities of the tokens in `tokenIds` over the half-open span
60
+ * [span.start, span.end), where the token at position j is scored from the logits
61
+ * distribution at position j-1.
62
+ *
63
+ * Throws on: unsupported dtype, wrong dims shape, batch_size !== 1, tokenIds/seqLen
64
+ * mismatch, out-of-bounds span, out-of-vocab token ids, or non-finite logits.
65
+ */
66
+ export function sumSequenceLogProb(logits, tokenIds, span) {
67
+ assertSupportedDtype(logits.data);
68
+ const [, seqLen, vocabSize] = assertBatchAndDims(logits.dims);
69
+ if (tokenIds.length !== seqLen) {
70
+ throw new Error(`sumSequenceLogProb: tokenIds length (${tokenIds.length}) does not match logits seqLen (${seqLen})`);
71
+ }
72
+ assertSpanWithinBounds(span, seqLen);
73
+ const { data } = logits;
74
+ let total = 0;
75
+ for (let j = span.start; j < span.end; j++) {
76
+ const targetId = Number(tokenIds[j]);
77
+ if (!Number.isInteger(targetId) || targetId < 0 || targetId >= vocabSize) {
78
+ throw new Error(`sumSequenceLogProb: token id ${targetId} at position ${j} out of vocab range [0, ${vocabSize})`);
79
+ }
80
+ const pos = j - 1;
81
+ const rowStart = pos * vocabSize;
82
+ const { logSumExp } = logSumExpRow(data, rowStart, vocabSize, `position ${pos}`);
83
+ const targetLogit = data[rowStart + targetId] ?? Number.NaN;
84
+ total += targetLogit - logSumExp;
85
+ }
86
+ return total;
87
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Gemma-4 template-mode scoring: `conditional_sequence_logprob_sum`.
3
+ *
4
+ * Gemma-4 (and any other instruction-tuned "ForConditionalGeneration" architecture) is
5
+ * trained on chat-templated input. Feeding it a bare sentence — the base-mode approach
6
+ * from Task 1 — measures outside its intended input format. Instead, the sentence is
7
+ * scored as an ASSISTANT CONTINUATION after a fixed, WOERTLICH user turn (Messmotor-
8
+ * Sanierung Task 5 / Plan v5), and only the sentence's own tokens are scored — never the
9
+ * template's.
10
+ *
11
+ * Split out of `transformers-inference.ts` (per Task 5 brief item 3) to keep that file
12
+ * from growing unwieldy; this module owns everything specific to the template-mode span
13
+ * computation and is exercised entirely offline via injected fakes (`TemplateTokenizer`,
14
+ * `TemplateModel`) — no `@huggingface/transformers` mocking needed except for `Tensor`,
15
+ * which is dynamically imported the same way `transformers-inference.ts` does it.
16
+ *
17
+ * FALLE aus dem Spike (Task 0): `tokenizer.apply_chat_template(..., { tokenize: true })`
18
+ * returns `{ input_ids: Tensor, attention_mask: Tensor }`, NOT a plain array. Naively
19
+ * calling `Array.isArray()`/`Array.from()` on that object yields `context_len = 0`, which
20
+ * makes the prefix-identity check vacuously true. This module never calls
21
+ * `apply_chat_template` with `tokenize: true` at all — it uses `tokenize: false` to get
22
+ * the literal context STRING, then tokenizes that string (and the context+sentence
23
+ * string) via `tokenizer.encode()`, which returns a plain `number[]` by contract. The
24
+ * `context_len > 0` guard below exists anyway, as a structural regression test against
25
+ * this exact trap resurfacing under a future refactor.
26
+ */
27
+ import type { ScoredLogProb } from "./scored-log-prob-types";
28
+ import type { LogitsTensorLike } from "./sequence-logprob";
29
+ /**
30
+ * WOERTLICH fixed context spec (Messmotor-Sanierung Task 5 / Plan v5), verified against
31
+ * the real gemma-4-E2B-it tokenizer by the Task 0 spike (18 tokens,
32
+ * [2,105,2364,107,13790,500,18252,10175,171313,118778,4238,29078,236761,106,107,105,4368,107]).
33
+ * NEVER change this text without re-running the gated integration test — every bias.v2
34
+ * artifact produced in template mode recomputes against exactly this prompt.
35
+ */
36
+ export declare const FIXED_TEMPLATE_USER_MESSAGE = "Schreibe einen kurzen Satz auf Deutsch.";
37
+ /** Minimal message shape this module needs — deliberately not importing transformers.js's
38
+ * own `Message` type to keep this module's dependency surface (and test doubles) small. */
39
+ export interface TemplateChatMessage {
40
+ role: string;
41
+ content: string;
42
+ }
43
+ /** Minimal shape this module needs from a transformers.js tokenizer for template-mode
44
+ * scoring. `apply_chat_template` is only ever called here with `tokenize: false`. */
45
+ export interface TemplateTokenizer {
46
+ apply_chat_template(conversation: TemplateChatMessage[], options: {
47
+ add_generation_prompt: boolean;
48
+ tokenize: false;
49
+ }): string;
50
+ encode(text: string): number[];
51
+ }
52
+ /** Minimal shape this module needs from a transformers.js model: callable, returns logits. */
53
+ export type TemplateModel = (inputs: {
54
+ input_ids: unknown;
55
+ attention_mask: unknown;
56
+ }) => Promise<{
57
+ logits: LogitsTensorLike;
58
+ }>;
59
+ export interface TemplateScoringDeps {
60
+ tokenizer: TemplateTokenizer;
61
+ model: TemplateModel;
62
+ modelId: string;
63
+ libraryVersion: string;
64
+ /** The CONFIGURED weight precision (e.g. "q4", "fp32") passed to `from_pretrained` —
65
+ * threaded through explicitly because the output logits tensor's own `.type` is always
66
+ * "float32" regardless of weight quantization (Task 0 spike) and so cannot be used to
67
+ * recover it; see `ScoredLogProbProvenance.dtype` vs `.logitsDtype`. */
68
+ dtype: string;
69
+ }
70
+ /**
71
+ * Score `text` as a Gemma-4-style assistant continuation of the fixed template context.
72
+ *
73
+ * Builds the context via `apply_chat_template(..., { tokenize: false })`, tokenizes the
74
+ * context alone and the context+sentence together via `tokenizer.encode()`, verifies
75
+ * prefix-token identity, determines the sentence-only span, and delegates the actual
76
+ * log-probability accumulation to the pure `sumSequenceLogProb` scorer from Task 1
77
+ * (reused, not reimplemented).
78
+ */
79
+ export declare function scoreTemplateContinuation(deps: TemplateScoringDeps, text: string): Promise<ScoredLogProb>;
80
+ /** Minimal shape this module needs from a loaded model config to detect architecture. */
81
+ export interface MinimalModelConfig {
82
+ model_type?: string | null;
83
+ architectures?: readonly string[] | null;
84
+ }
85
+ /**
86
+ * Detect whether a loaded model config indicates a conditional-generation / multimodal
87
+ * architecture (e.g. Gemma-4's `Gemma4ForConditionalGeneration`), which must be scored via
88
+ * the template mode rather than fed bare sentences as a classic causal LM.
89
+ *
90
+ * Detection is from the CONFIG (`architectures`), never the model id string, per spec —
91
+ * so a differently-named repo hosting the same architecture is still detected correctly,
92
+ * and a model whose name happens to contain "gemma" but isn't conditional-generation is not
93
+ * misdetected.
94
+ */
95
+ export declare function isConditionalGenerationConfig(config: MinimalModelConfig): boolean;
96
+ /** Minimal shape this module needs from a loaded tokenizer to detect a chat template. */
97
+ export interface MinimalChatTemplateTokenizer {
98
+ chat_template?: string | null;
99
+ }
100
+ /**
101
+ * Detect whether a loaded TOKENIZER carries a chat template, which decides the SCORING MODE
102
+ * (template vs. base) — Messmotor-Sanierung Task 6c.
103
+ *
104
+ * This replaces architecture-name sniffing (`isConditionalGenerationConfig`) as the mode
105
+ * signal: Task 6b's panel run showed that keying mode on architecture misses instruction-tuned
106
+ * models whose architecture is plain `*ForCausalLM` (e.g. `LlamaForCausalLM`,
107
+ * `Gemma3ForCausalLM`) but which still expect chat-templated input — they were scored bare,
108
+ * the exact error template mode exists to prevent.
109
+ *
110
+ * Verified empirically (transformers.js@4.2.0, against the four panel models + gpt2) via
111
+ * `AutoTokenizer.from_pretrained(modelId)`: the loaded tokenizer instance exposes the chat
112
+ * template — sourced from `tokenizer_config.json`'s `chat_template` field — directly as
113
+ * `tokenizer.chat_template`. It is `null` for models with no chat template
114
+ * (`Xenova/gpt2`, `Xenova/llama2.c-stories15M`) and a non-empty Jinja template string for
115
+ * instruction-tuned models (`onnx-community/Llama-3.2-1B-Instruct-ONNX`,
116
+ * `onnx-community/gemma-3-1b-it-ONNX`, `onnx-community/gemma-4-E2B-it-ONNX`).
117
+ *
118
+ * Mode selection (this function) and LOADER selection (`isConditionalGenerationConfig`,
119
+ * still architecture-keyed) are deliberately independent: a CausalLM-architecture model with
120
+ * a chat template loads via the base-mode loader (`AutoTokenizer` + `AutoModelForCausalLM`)
121
+ * but scores via the template-mode scorer — e.g. Llama-3.2-1B-Instruct.
122
+ */
123
+ export declare function hasChatTemplate(tokenizer: MinimalChatTemplateTokenizer): boolean;
124
+ //# sourceMappingURL=template-scoring.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-scoring.d.ts","sourceRoot":"","sources":["../../src/inference/template-scoring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAA2B,MAAM,yBAAyB,CAAC;AACtF,OAAO,KAAK,EAAE,gBAAgB,EAAQ,MAAM,oBAAoB,CAAC;AAGjE;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,4CAA4C,CAAC;AAErF;2FAC2F;AAC3F,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAMD;qFACqF;AACrF,MAAM,WAAW,iBAAiB;IAChC,mBAAmB,CACjB,YAAY,EAAE,mBAAmB,EAAE,EACnC,OAAO,EAAE;QAAE,qBAAqB,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,KAAK,CAAA;KAAE,GAC3D,MAAM,CAAC;IACV,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAChC;AAED,8FAA8F;AAC9F,MAAM,MAAM,aAAa,GAAG,CAAC,MAAM,EAAE;IACnC,SAAS,EAAE,OAAO,CAAC;IACnB,cAAc,EAAE,OAAO,CAAC;CACzB,KAAK,OAAO,CAAC;IAAE,MAAM,EAAE,gBAAgB,CAAA;CAAE,CAAC,CAAC;AAE5C,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,iBAAiB,CAAC;IAC7B,KAAK,EAAE,aAAa,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB;;;4EAGwE;IACxE,KAAK,EAAE,MAAM,CAAC;CACf;AAqCD;;;;;;;;GAQG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,mBAAmB,EACzB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,aAAa,CAAC,CAkExB;AAED,yFAAyF;AACzF,MAAM,WAAW,kBAAkB;IACjC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;CAC1C;AAED;;;;;;;;;GASG;AACH,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAKjF;AAED,yFAAyF;AACzF,MAAM,WAAW,4BAA4B;IAC3C,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,4BAA4B,GAAG,OAAO,CAEhF"}
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Gemma-4 template-mode scoring: `conditional_sequence_logprob_sum`.
3
+ *
4
+ * Gemma-4 (and any other instruction-tuned "ForConditionalGeneration" architecture) is
5
+ * trained on chat-templated input. Feeding it a bare sentence — the base-mode approach
6
+ * from Task 1 — measures outside its intended input format. Instead, the sentence is
7
+ * scored as an ASSISTANT CONTINUATION after a fixed, WOERTLICH user turn (Messmotor-
8
+ * Sanierung Task 5 / Plan v5), and only the sentence's own tokens are scored — never the
9
+ * template's.
10
+ *
11
+ * Split out of `transformers-inference.ts` (per Task 5 brief item 3) to keep that file
12
+ * from growing unwieldy; this module owns everything specific to the template-mode span
13
+ * computation and is exercised entirely offline via injected fakes (`TemplateTokenizer`,
14
+ * `TemplateModel`) — no `@huggingface/transformers` mocking needed except for `Tensor`,
15
+ * which is dynamically imported the same way `transformers-inference.ts` does it.
16
+ *
17
+ * FALLE aus dem Spike (Task 0): `tokenizer.apply_chat_template(..., { tokenize: true })`
18
+ * returns `{ input_ids: Tensor, attention_mask: Tensor }`, NOT a plain array. Naively
19
+ * calling `Array.isArray()`/`Array.from()` on that object yields `context_len = 0`, which
20
+ * makes the prefix-identity check vacuously true. This module never calls
21
+ * `apply_chat_template` with `tokenize: true` at all — it uses `tokenize: false` to get
22
+ * the literal context STRING, then tokenizes that string (and the context+sentence
23
+ * string) via `tokenizer.encode()`, which returns a plain `number[]` by contract. The
24
+ * `context_len > 0` guard below exists anyway, as a structural regression test against
25
+ * this exact trap resurfacing under a future refactor.
26
+ */
27
+ import { sumSequenceLogProb } from "./sequence-logprob";
28
+ /**
29
+ * WOERTLICH fixed context spec (Messmotor-Sanierung Task 5 / Plan v5), verified against
30
+ * the real gemma-4-E2B-it tokenizer by the Task 0 spike (18 tokens,
31
+ * [2,105,2364,107,13790,500,18252,10175,171313,118778,4238,29078,236761,106,107,105,4368,107]).
32
+ * NEVER change this text without re-running the gated integration test — every bias.v2
33
+ * artifact produced in template mode recomputes against exactly this prompt.
34
+ */
35
+ export const FIXED_TEMPLATE_USER_MESSAGE = "Schreibe einen kurzen Satz auf Deutsch.";
36
+ function buildTemplateMessages() {
37
+ return [{ role: "user", content: FIXED_TEMPLATE_USER_MESSAGE }];
38
+ }
39
+ async function sha256Hex(input) {
40
+ const bytes = new TextEncoder().encode(input);
41
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
42
+ return Array.from(new Uint8Array(digest))
43
+ .map((b) => b.toString(16).padStart(2, "0"))
44
+ .join("");
45
+ }
46
+ /**
47
+ * Assert that `contextTokenIds` is an exact prefix of `combinedTokenIds`. Per spec: on
48
+ * violation (a token merge at the context/sentence boundary), ABORT with a speaking error.
49
+ * Never try a separator character, never blindly assume the prefix length as the span start.
50
+ */
51
+ function assertPrefixIdentity(contextTokenIds, combinedTokenIds, modelId, text) {
52
+ if (combinedTokenIds.length < contextTokenIds.length) {
53
+ throw new Error(`scoreTemplateContinuation: combined tokenization (${combinedTokenIds.length} tokens) is shorter ` +
54
+ `than the fixed context (${contextTokenIds.length} tokens) for sentence "${text}" on model ` +
55
+ `"${modelId}" — cannot determine a sentence span.`);
56
+ }
57
+ for (let i = 0; i < contextTokenIds.length; i++) {
58
+ if (combinedTokenIds[i] !== contextTokenIds[i]) {
59
+ throw new Error(`scoreTemplateContinuation: prefix-token identity violated at position ${i} for sentence "${text}" on model "${modelId}" (context token ${contextTokenIds[i]} vs combined token ${combinedTokenIds[i]}). The tokenizer merged tokens across the context/sentence boundary. Per spec this is an ABORT: no separator character, no blind assumption of the prefix length as the span start.`);
60
+ }
61
+ }
62
+ }
63
+ /**
64
+ * Score `text` as a Gemma-4-style assistant continuation of the fixed template context.
65
+ *
66
+ * Builds the context via `apply_chat_template(..., { tokenize: false })`, tokenizes the
67
+ * context alone and the context+sentence together via `tokenizer.encode()`, verifies
68
+ * prefix-token identity, determines the sentence-only span, and delegates the actual
69
+ * log-probability accumulation to the pure `sumSequenceLogProb` scorer from Task 1
70
+ * (reused, not reimplemented).
71
+ */
72
+ export async function scoreTemplateContinuation(deps, text) {
73
+ const { tokenizer, model, modelId, libraryVersion, dtype } = deps;
74
+ const messages = buildTemplateMessages();
75
+ const contextText = tokenizer.apply_chat_template(messages, {
76
+ add_generation_prompt: true,
77
+ tokenize: false,
78
+ });
79
+ const contextTokenIds = Array.from(tokenizer.encode(contextText), Number);
80
+ if (contextTokenIds.length === 0) {
81
+ throw new Error(`scoreTemplateContinuation: fixed template context tokenized to 0 tokens for model "${modelId}". This is the exact false-pass trap from the Task 0 spike (an unwrapped apply_chat_template(tokenize:true) Tensor object silently produced context_len=0, making the prefix-identity check vacuously true) — aborting rather than treating an empty context as a valid prefix.`);
82
+ }
83
+ const combinedTokenIds = Array.from(tokenizer.encode(contextText + text), Number);
84
+ assertPrefixIdentity(contextTokenIds, combinedTokenIds, modelId, text);
85
+ const span = { start: contextTokenIds.length, end: combinedTokenIds.length };
86
+ if (span.start >= span.end) {
87
+ throw new Error(`scoreTemplateContinuation: sentence "${text}" contributed zero tokens after the fixed ` +
88
+ `context (span [${span.start}, ${span.end})) for model "${modelId}".`);
89
+ }
90
+ const scoredTokenIds = combinedTokenIds.slice(span.start);
91
+ const { Tensor } = await import("@huggingface/transformers");
92
+ const seqLen = combinedTokenIds.length;
93
+ const inputIds = new Tensor("int64", BigInt64Array.from(combinedTokenIds.map(BigInt)), [
94
+ 1,
95
+ seqLen,
96
+ ]);
97
+ const attentionMask = new Tensor("int64", BigInt64Array.from(new Array(seqLen).fill(1n)), [
98
+ 1,
99
+ seqLen,
100
+ ]);
101
+ const outputs = await model({ input_ids: inputIds, attention_mask: attentionMask });
102
+ const logits = outputs.logits;
103
+ const score = sumSequenceLogProb(logits, combinedTokenIds, span);
104
+ const contextTokenHash = await sha256Hex(JSON.stringify(contextTokenIds));
105
+ const promptSha256 = await sha256Hex(contextText);
106
+ const provenance = {
107
+ modelId,
108
+ dtype,
109
+ logitsDtype: String(logits.type ?? "unknown"),
110
+ backend: "node-onnx",
111
+ libraryVersion,
112
+ templateStrategy: "fixed_template_span",
113
+ promptText: contextText,
114
+ promptSha256,
115
+ };
116
+ return {
117
+ score,
118
+ scoredTokenIds,
119
+ span,
120
+ contextTokenHash,
121
+ provenance,
122
+ method: "conditional_sequence_logprob_sum",
123
+ };
124
+ }
125
+ /**
126
+ * Detect whether a loaded model config indicates a conditional-generation / multimodal
127
+ * architecture (e.g. Gemma-4's `Gemma4ForConditionalGeneration`), which must be scored via
128
+ * the template mode rather than fed bare sentences as a classic causal LM.
129
+ *
130
+ * Detection is from the CONFIG (`architectures`), never the model id string, per spec —
131
+ * so a differently-named repo hosting the same architecture is still detected correctly,
132
+ * and a model whose name happens to contain "gemma" but isn't conditional-generation is not
133
+ * misdetected.
134
+ */
135
+ export function isConditionalGenerationConfig(config) {
136
+ const architectures = config.architectures ?? [];
137
+ return architectures.some((name) => typeof name === "string" && name.endsWith("ForConditionalGeneration"));
138
+ }
139
+ /**
140
+ * Detect whether a loaded TOKENIZER carries a chat template, which decides the SCORING MODE
141
+ * (template vs. base) — Messmotor-Sanierung Task 6c.
142
+ *
143
+ * This replaces architecture-name sniffing (`isConditionalGenerationConfig`) as the mode
144
+ * signal: Task 6b's panel run showed that keying mode on architecture misses instruction-tuned
145
+ * models whose architecture is plain `*ForCausalLM` (e.g. `LlamaForCausalLM`,
146
+ * `Gemma3ForCausalLM`) but which still expect chat-templated input — they were scored bare,
147
+ * the exact error template mode exists to prevent.
148
+ *
149
+ * Verified empirically (transformers.js@4.2.0, against the four panel models + gpt2) via
150
+ * `AutoTokenizer.from_pretrained(modelId)`: the loaded tokenizer instance exposes the chat
151
+ * template — sourced from `tokenizer_config.json`'s `chat_template` field — directly as
152
+ * `tokenizer.chat_template`. It is `null` for models with no chat template
153
+ * (`Xenova/gpt2`, `Xenova/llama2.c-stories15M`) and a non-empty Jinja template string for
154
+ * instruction-tuned models (`onnx-community/Llama-3.2-1B-Instruct-ONNX`,
155
+ * `onnx-community/gemma-3-1b-it-ONNX`, `onnx-community/gemma-4-E2B-it-ONNX`).
156
+ *
157
+ * Mode selection (this function) and LOADER selection (`isConditionalGenerationConfig`,
158
+ * still architecture-keyed) are deliberately independent: a CausalLM-architecture model with
159
+ * a chat template loads via the base-mode loader (`AutoTokenizer` + `AutoModelForCausalLM`)
160
+ * but scores via the template-mode scorer — e.g. Llama-3.2-1B-Instruct.
161
+ */
162
+ export function hasChatTemplate(tokenizer) {
163
+ return typeof tokenizer.chat_template === "string" && tokenizer.chat_template.length > 0;
164
+ }