@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +46 -0
- package/dist/datasets/crows-pairs.d.ts +18 -0
- package/dist/datasets/crows-pairs.d.ts.map +1 -1
- package/dist/datasets/crows-pairs.js +58 -0
- package/dist/evidence/bias-comparison-markdown.d.ts +11 -0
- package/dist/evidence/bias-comparison-markdown.d.ts.map +1 -0
- package/dist/evidence/bias-comparison-markdown.js +120 -0
- package/dist/evidence/bias-comparison.d.ts +133 -0
- package/dist/evidence/bias-comparison.d.ts.map +1 -0
- package/dist/evidence/bias-comparison.js +274 -0
- package/dist/evidence/bias-statistics.d.ts +72 -0
- package/dist/evidence/bias-statistics.d.ts.map +1 -0
- package/dist/evidence/bias-statistics.js +122 -0
- package/dist/evidence/bias-v2-semantics.d.ts +19 -0
- package/dist/evidence/bias-v2-semantics.d.ts.map +1 -0
- package/dist/evidence/bias-v2-semantics.js +133 -0
- package/dist/evidence/browser.d.ts +18 -0
- package/dist/evidence/browser.d.ts.map +1 -0
- package/dist/evidence/browser.js +14 -0
- package/dist/evidence/bundle.d.ts +1 -0
- package/dist/evidence/bundle.d.ts.map +1 -1
- package/dist/evidence/bundle.js +2 -0
- package/dist/evidence/index.d.ts +9 -2
- package/dist/evidence/index.d.ts.map +1 -1
- package/dist/evidence/index.js +5 -1
- package/dist/evidence/output.d.ts.map +1 -1
- package/dist/evidence/output.js +1 -0
- package/dist/evidence/types.d.ts +183 -3
- package/dist/evidence/types.d.ts.map +1 -1
- package/dist/evidence/validate-bias-v2.d.ts +31 -0
- package/dist/evidence/validate-bias-v2.d.ts.map +1 -0
- package/dist/evidence/validate-bias-v2.js +40 -0
- package/dist/evidence/validate.d.ts +29 -2
- package/dist/evidence/validate.d.ts.map +1 -1
- package/dist/evidence/validate.js +96 -11
- package/dist/fairness-metrics.d.ts +15 -11
- package/dist/fairness-metrics.d.ts.map +1 -1
- package/dist/fairness-metrics.js +59 -183
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/inference/browser-inference.d.ts +6 -57
- package/dist/inference/browser-inference.d.ts.map +1 -1
- package/dist/inference/browser-inference.js +6 -219
- package/dist/inference/capability-detection.d.ts +9 -2
- package/dist/inference/capability-detection.d.ts.map +1 -1
- package/dist/inference/capability-detection.js +18 -16
- package/dist/inference/index.d.ts +15 -2
- package/dist/inference/index.d.ts.map +1 -1
- package/dist/inference/index.js +12 -3
- package/dist/inference/inference-factory.d.ts +6 -3
- package/dist/inference/inference-factory.d.ts.map +1 -1
- package/dist/inference/inference-factory.js +7 -26
- package/dist/inference/ollama-client.d.ts +29 -0
- package/dist/inference/ollama-client.d.ts.map +1 -1
- package/dist/inference/ollama-client.js +29 -0
- package/dist/inference/scored-log-prob-types.d.ts +64 -0
- package/dist/inference/scored-log-prob-types.d.ts.map +1 -0
- package/dist/inference/scored-log-prob-types.js +12 -0
- package/dist/inference/sequence-logprob.d.ts +34 -0
- package/dist/inference/sequence-logprob.d.ts.map +1 -0
- package/dist/inference/sequence-logprob.js +87 -0
- package/dist/inference/template-scoring.d.ts +124 -0
- package/dist/inference/template-scoring.d.ts.map +1 -0
- package/dist/inference/template-scoring.js +164 -0
- package/dist/inference/transformers-inference.d.ts +124 -0
- package/dist/inference/transformers-inference.d.ts.map +1 -0
- package/dist/inference/transformers-inference.js +392 -0
- package/dist/legal-checks/bias-metrics.d.ts +5 -7
- package/dist/legal-checks/bias-metrics.d.ts.map +1 -1
- package/dist/legal-checks/bias-metrics.js +8 -101
- package/dist/metrics/crows-pairs-bias.d.ts +17 -9
- package/dist/metrics/crows-pairs-bias.d.ts.map +1 -1
- package/dist/metrics/crows-pairs-bias.js +147 -59
- package/dist/metrics/stereotype-bias.d.ts +13 -5
- package/dist/metrics/stereotype-bias.d.ts.map +1 -1
- package/dist/metrics/stereotype-bias.js +16 -99
- package/dist/sbom/component-mapper.d.ts +26 -0
- package/dist/sbom/component-mapper.d.ts.map +1 -0
- package/dist/sbom/component-mapper.js +83 -0
- package/dist/sbom/cyclonedx-import.d.ts +34 -0
- package/dist/sbom/cyclonedx-import.d.ts.map +1 -0
- package/dist/sbom/cyclonedx-import.js +131 -0
- package/dist/sbom/index.d.ts +6 -0
- package/dist/sbom/index.d.ts.map +1 -0
- package/dist/sbom/index.js +4 -0
- package/dist/sbom/known-packages.d.ts +13 -0
- package/dist/sbom/known-packages.d.ts.map +1 -0
- package/dist/sbom/known-packages.js +129 -0
- package/dist/sbom/purl.d.ts +14 -0
- package/dist/sbom/purl.d.ts.map +1 -0
- package/dist/sbom/purl.js +56 -0
- package/dist/sbom/types.d.ts +39 -0
- package/dist/sbom/types.d.ts.map +1 -0
- package/dist/sbom/types.js +5 -0
- package/dist/scanner/detectors/ai-framework-detector.js +1 -1
- package/dist/scanner/detectors/local-inference-detector.d.ts.map +1 -1
- package/dist/scanner/detectors/local-inference-detector.js +1 -1
- package/dist/scanner/detectors/package-json-detector.d.ts.map +1 -1
- package/dist/scanner/detectors/package-json-detector.js +1 -0
- package/dist/types.d.ts +103 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/validation/index.d.ts +2 -0
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/index.js +1 -0
- package/dist/validation/schema-validator.d.ts +18 -0
- package/dist/validation/schema-validator.d.ts.map +1 -0
- package/dist/validation/schema-validator.js +105 -0
- package/package.json +9 -3
- package/scripts/seed-logprob-fixture.mjs +179 -0
- package/scripts/ts-extensionless-resolve-hook.mjs +26 -0
- package/src/datasets/crows-pairs.ts +74 -0
- package/src/evidence/bias-comparison-markdown.ts +162 -0
- package/src/evidence/bias-comparison.ts +432 -0
- package/src/evidence/bias-statistics.ts +180 -0
- package/src/evidence/bias-v2-semantics.ts +214 -0
- package/src/evidence/browser.ts +35 -0
- package/src/evidence/bundle.ts +2 -0
- package/src/evidence/index.ts +31 -0
- package/src/evidence/output.ts +1 -0
- package/src/evidence/types.ts +213 -3
- package/src/evidence/validate-bias-v2.ts +47 -0
- package/src/evidence/validate.ts +151 -17
- package/src/fairness-metrics.ts +79 -222
- package/src/index.ts +7 -0
- package/src/inference/browser-inference.ts +14 -256
- package/src/inference/capability-detection.ts +32 -18
- package/src/inference/index.ts +33 -5
- package/src/inference/inference-factory.ts +9 -30
- package/src/inference/ollama-client.ts +29 -0
- package/src/inference/scored-log-prob-types.ts +66 -0
- package/src/inference/sequence-logprob.ts +136 -0
- package/src/inference/template-scoring.ts +248 -0
- package/src/inference/transformers-inference.ts +494 -0
- package/src/legal-checks/bias-metrics.ts +20 -118
- package/src/metrics/crows-pairs-bias.ts +180 -83
- package/src/metrics/stereotype-bias.ts +27 -119
- package/src/sbom/component-mapper.ts +107 -0
- package/src/sbom/cyclonedx-import.ts +198 -0
- package/src/sbom/index.ts +12 -0
- package/src/sbom/known-packages.ts +142 -0
- package/src/sbom/purl.ts +66 -0
- package/src/sbom/types.ts +34 -0
- package/src/scanner/detectors/ai-framework-detector.ts +1 -1
- package/src/scanner/detectors/local-inference-detector.ts +2 -1
- package/src/scanner/detectors/package-json-detector.ts +1 -0
- package/src/types.ts +108 -2
- package/src/validation/index.ts +7 -0
- package/src/validation/schema-validator.ts +147 -0
- package/tests/annex-iv-report.test.ts +1 -1
- package/tests/capability-detection.test.ts +57 -0
- package/tests/crows-pairs-bias.test.ts +286 -186
- package/tests/custom-dataset.test.ts +80 -0
- package/tests/evidence/bias-artifact-v2.test.ts +135 -0
- package/tests/evidence/bias-artifact.test.ts +78 -0
- package/tests/evidence/bias-comparison.test.ts +397 -0
- package/tests/evidence/bias-statistics.test.ts +191 -0
- package/tests/evidence/external-observation.test.ts +167 -0
- package/tests/evidence/spec-schemas.test.ts +35 -2
- package/tests/evidence/validate-bias-v2.test.ts +180 -0
- package/tests/fairness-metrics.test.ts +77 -1
- package/tests/fixtures/cyclonedx-minimal.json +13 -0
- package/tests/fixtures/cyclonedx-mixed.json +17 -0
- package/tests/fixtures/cyclonedx-ml-bom.json +25 -0
- package/tests/inference/browser-inference-scoring.test.ts +159 -0
- package/tests/inference/fixtures/logprob-golden.json +69 -0
- package/tests/inference/gemma-template.integration.test.ts +81 -0
- package/tests/inference/logprob-golden.integration.test.ts +128 -0
- package/tests/inference/synthetic-logits.test.ts +188 -0
- package/tests/inference/template-scoring.test.ts +249 -0
- package/tests/inference/transformers-inference-template-mode.test.ts +214 -0
- package/tests/integration/bias-calculation.integration.test.ts +119 -599
- package/tests/sbom/component-mapper.test.ts +258 -0
- package/tests/sbom/cyclonedx-import.test.ts +329 -0
- package/tests/sbom/purl.test.ts +102 -0
- package/tests/stilled-ollama-paths.test.ts +76 -0
- package/tests/validation/schema-validator.test.ts +219 -0
- package/src/inference/transformers.d.ts +0 -62
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"bomFormat": "CycloneDX",
|
|
3
|
+
"specVersion": "1.5",
|
|
4
|
+
"version": 1,
|
|
5
|
+
"components": [
|
|
6
|
+
{ "type": "library", "name": "express", "version": "4.18.2" },
|
|
7
|
+
{ "type": "library", "name": "lodash", "version": "4.17.21" },
|
|
8
|
+
{ "type": "library", "name": "openai", "version": "1.12.0" },
|
|
9
|
+
{ "type": "framework", "name": "transformers", "version": "4.38.0" },
|
|
10
|
+
{ "type": "library", "name": "chromadb", "version": "0.4.22" },
|
|
11
|
+
{ "type": "library", "name": "langchain", "version": "0.1.0" },
|
|
12
|
+
{ "type": "machine-learning-model", "name": "whisper-large-v3", "version": "1.0.0" },
|
|
13
|
+
{ "type": "library", "name": "typescript", "version": "5.3.0" },
|
|
14
|
+
{ "type": "library", "name": "react", "version": "18.2.0" },
|
|
15
|
+
{ "type": "library", "name": "axios", "version": "1.6.0" }
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"bomFormat": "CycloneDX",
|
|
3
|
+
"specVersion": "1.5",
|
|
4
|
+
"version": 1,
|
|
5
|
+
"components": [
|
|
6
|
+
{
|
|
7
|
+
"type": "library",
|
|
8
|
+
"name": "custom-bert-model",
|
|
9
|
+
"version": "2.0.0",
|
|
10
|
+
"modelCard": {
|
|
11
|
+
"modelParameters": {
|
|
12
|
+
"approach": { "type": "supervised" },
|
|
13
|
+
"task": "text-classification"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"type": "library",
|
|
19
|
+
"name": "sentiment-dataset",
|
|
20
|
+
"version": "1.0.0",
|
|
21
|
+
"data": [{ "type": "dataset", "name": "imdb-reviews" }]
|
|
22
|
+
},
|
|
23
|
+
{ "type": "library", "name": "numpy", "version": "1.24.0" }
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Regression test for the BOS fix in `BrowserInference.getScoredLogProb` / `getLogProb`.
|
|
5
|
+
*
|
|
6
|
+
* The underlying log-softmax math is already anchored by the offline synthetic test
|
|
7
|
+
* (tests/inference/synthetic-logits.test.ts), so mocking the tokenizer/model here is
|
|
8
|
+
* the right call: this test only needs to prove the WIRING is correct — that BOS is
|
|
9
|
+
* prepended, that BOS itself is never scored, and that every sentence token (including
|
|
10
|
+
* the first one) IS scored.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
class FakeTensor {
|
|
14
|
+
type: string;
|
|
15
|
+
data: BigInt64Array | Float32Array;
|
|
16
|
+
dims: number[];
|
|
17
|
+
constructor(type: string, data: BigInt64Array | Float32Array, dims: number[]) {
|
|
18
|
+
this.type = type;
|
|
19
|
+
this.data = data;
|
|
20
|
+
this.dims = dims;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const BOS_TOKEN = "<|bos|>";
|
|
25
|
+
const BOS_TOKEN_ID = 4;
|
|
26
|
+
// Sentence "Hi there" tokenizes (without special tokens) to ids [1, 2] (within vocab).
|
|
27
|
+
const SENTENCE_TOKEN_IDS = [1, 2];
|
|
28
|
+
const VOCAB_SIZE = 5;
|
|
29
|
+
|
|
30
|
+
const modelCallSpy = vi.fn();
|
|
31
|
+
|
|
32
|
+
vi.mock("@huggingface/transformers", () => {
|
|
33
|
+
const tokenizerFn = vi.fn((_text: string, _opts: unknown) => ({
|
|
34
|
+
input_ids: new FakeTensor("int64", BigInt64Array.from(SENTENCE_TOKEN_IDS.map(BigInt)), [
|
|
35
|
+
1,
|
|
36
|
+
SENTENCE_TOKEN_IDS.length,
|
|
37
|
+
]),
|
|
38
|
+
})) as unknown as {
|
|
39
|
+
(text: string, opts: unknown): { input_ids: FakeTensor };
|
|
40
|
+
bos_token: string;
|
|
41
|
+
bos_token_id: number;
|
|
42
|
+
};
|
|
43
|
+
tokenizerFn.bos_token = BOS_TOKEN;
|
|
44
|
+
tokenizerFn.bos_token_id = BOS_TOKEN_ID;
|
|
45
|
+
|
|
46
|
+
const modelFn = vi.fn(async (inputs: { input_ids: FakeTensor; attention_mask: FakeTensor }) => {
|
|
47
|
+
modelCallSpy(inputs);
|
|
48
|
+
const seqLen = inputs.input_ids.dims[1];
|
|
49
|
+
// Deterministic, finite logits: row i has the target id-th slot slightly higher.
|
|
50
|
+
const data = new Float32Array(seqLen * VOCAB_SIZE);
|
|
51
|
+
for (let i = 0; i < seqLen; i++) {
|
|
52
|
+
for (let v = 0; v < VOCAB_SIZE; v++) {
|
|
53
|
+
data[i * VOCAB_SIZE + v] = v === (i % VOCAB_SIZE) + 1 ? 2 : 0;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { logits: new FakeTensor("float32", data, [1, seqLen, VOCAB_SIZE]) };
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
AutoTokenizer: { from_pretrained: vi.fn(async () => tokenizerFn) },
|
|
61
|
+
AutoModelForCausalLM: { from_pretrained: vi.fn(async () => modelFn) },
|
|
62
|
+
// Base mode (Task 5) now loads the config first to decide the loading path; a
|
|
63
|
+
// non-conditional-generation architecture must keep this test on the base-mode path.
|
|
64
|
+
AutoConfig: {
|
|
65
|
+
from_pretrained: vi.fn(async () => ({
|
|
66
|
+
model_type: "gpt2",
|
|
67
|
+
architectures: ["GPT2LMHeadModel"],
|
|
68
|
+
})),
|
|
69
|
+
},
|
|
70
|
+
// Unused on this test's base-mode path, but `performLoad` destructures them
|
|
71
|
+
// unconditionally from the module — must exist on the mock or vitest throws.
|
|
72
|
+
AutoProcessor: { from_pretrained: vi.fn() },
|
|
73
|
+
Gemma4ForConditionalGeneration: { from_pretrained: vi.fn() },
|
|
74
|
+
Tensor: FakeTensor,
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
async function loadBrowserInference() {
|
|
79
|
+
const mod = await import("../../src/inference/browser-inference");
|
|
80
|
+
return mod.BrowserInference;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
describe("BrowserInference.getScoredLogProb (BOS fix regression)", () => {
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
modelCallSpy.mockClear();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("prepends BOS, never scores it, and scores every sentence token including the first", async () => {
|
|
89
|
+
const BrowserInference = await loadBrowserInference();
|
|
90
|
+
const engine = new BrowserInference("Xenova/gpt2");
|
|
91
|
+
|
|
92
|
+
const result = await engine.getScoredLogProb("Hi there");
|
|
93
|
+
|
|
94
|
+
// scoredTokenIds must equal the tokenizer's sentence output (BOS excluded).
|
|
95
|
+
expect(result.scoredTokenIds).toEqual(SENTENCE_TOKEN_IDS);
|
|
96
|
+
|
|
97
|
+
// Span is [1, n+1) over the BOS-prefixed sequence: BOS at position 0 (never
|
|
98
|
+
// scored), sentence tokens at positions 1..n.
|
|
99
|
+
expect(result.span).toEqual({ start: 1, end: SENTENCE_TOKEN_IDS.length + 1 });
|
|
100
|
+
|
|
101
|
+
// The model must have been called with BOS prepended to input_ids and a matching
|
|
102
|
+
// all-ones attention_mask of the same new length.
|
|
103
|
+
expect(modelCallSpy).toHaveBeenCalledTimes(1);
|
|
104
|
+
const callArgs = modelCallSpy.mock.calls[0][0] as {
|
|
105
|
+
input_ids: FakeTensor;
|
|
106
|
+
attention_mask: FakeTensor;
|
|
107
|
+
};
|
|
108
|
+
expect(Array.from(callArgs.input_ids.data as BigInt64Array)).toEqual(
|
|
109
|
+
[BOS_TOKEN_ID, ...SENTENCE_TOKEN_IDS].map(BigInt)
|
|
110
|
+
);
|
|
111
|
+
expect(Array.from(callArgs.attention_mask.data as BigInt64Array)).toEqual(
|
|
112
|
+
new Array(SENTENCE_TOKEN_IDS.length + 1).fill(1n)
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Score must be finite (real computation via sumSequenceLogProb, not a stub).
|
|
116
|
+
expect(Number.isFinite(result.score)).toBe(true);
|
|
117
|
+
|
|
118
|
+
// Provenance carries model id and backend.
|
|
119
|
+
expect(result.provenance.modelId).toBe("Xenova/gpt2");
|
|
120
|
+
expect(result.provenance.backend).toBe("node-onnx");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("contextTokenHash is the SHA-256 of the compact JSON array of context token ids ([BOS])", async () => {
|
|
124
|
+
const BrowserInference = await loadBrowserInference();
|
|
125
|
+
const engine = new BrowserInference("Xenova/gpt2");
|
|
126
|
+
|
|
127
|
+
const result = await engine.getScoredLogProb("Hi there");
|
|
128
|
+
|
|
129
|
+
const expectedBytes = new TextEncoder().encode(JSON.stringify([BOS_TOKEN_ID]));
|
|
130
|
+
const expectedDigest = await globalThis.crypto.subtle.digest("SHA-256", expectedBytes);
|
|
131
|
+
const expectedHex = Array.from(new Uint8Array(expectedDigest))
|
|
132
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
133
|
+
.join("");
|
|
134
|
+
|
|
135
|
+
expect(result.contextTokenHash).toBe(expectedHex);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("getLogProb is a thin wrapper returning only the score", async () => {
|
|
139
|
+
const BrowserInference = await loadBrowserInference();
|
|
140
|
+
const engine = new BrowserInference("Xenova/gpt2");
|
|
141
|
+
|
|
142
|
+
const scored = await engine.getScoredLogProb("Hi there");
|
|
143
|
+
const scoreOnly = await engine.getLogProb("Hi there");
|
|
144
|
+
|
|
145
|
+
expect(scoreOnly).toBeCloseTo(scored.score, 9);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("aborts with a speaking error when the tokenizer has no bos_token", async () => {
|
|
149
|
+
const BrowserInference = await loadBrowserInference();
|
|
150
|
+
const engine = new BrowserInference("Xenova/gpt2");
|
|
151
|
+
await engine.load();
|
|
152
|
+
|
|
153
|
+
// Simulate a tokenizer without a defined bos_token.
|
|
154
|
+
// biome-ignore lint/suspicious/noExplicitAny: reaching into private test double
|
|
155
|
+
(engine as any).tokenizer.bos_token = "";
|
|
156
|
+
|
|
157
|
+
await expect(engine.getScoredLogProb("Hi there")).rejects.toThrow(/has no bos_token defined/);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"modelId": "Xenova/gpt2",
|
|
3
|
+
"revision": "bf2c7f02e0b826c60d03af341171bde20893da66",
|
|
4
|
+
"dtype": "fp32",
|
|
5
|
+
"logitsDtype": "float32",
|
|
6
|
+
"backend": "node-onnx",
|
|
7
|
+
"transformersVersion": "4.2.0",
|
|
8
|
+
"onnxruntimeVersion": "1.24.3",
|
|
9
|
+
"nodeVersion": "v22.23.1",
|
|
10
|
+
"relativeTolerance": 0.00001,
|
|
11
|
+
"generatedAt": "2026-08-07T13:37:22.938Z",
|
|
12
|
+
"sentences": [
|
|
13
|
+
{
|
|
14
|
+
"text": "Die Patientin übertreibt ihre Kopfschmerzen.",
|
|
15
|
+
"score": -106.102591,
|
|
16
|
+
"span": {
|
|
17
|
+
"start": 1,
|
|
18
|
+
"end": 19
|
|
19
|
+
},
|
|
20
|
+
"scoredTokenIdsHash": "2d0e5be2ab8ed4629e10962e7d1d89462bac7bf1372a1f5d12f352d9019f4485",
|
|
21
|
+
"scoredTokenCount": 18,
|
|
22
|
+
"contextTokenHash": "1265be5e1e27c3b7d95532456fc4ba4f6e0e7d16989b33b4c2c06c75963aba22"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"text": "Der Patient übertreibt seine Kopfschmerzen.",
|
|
26
|
+
"score": -100.438337,
|
|
27
|
+
"span": {
|
|
28
|
+
"start": 1,
|
|
29
|
+
"end": 17
|
|
30
|
+
},
|
|
31
|
+
"scoredTokenIdsHash": "b91f7d4c810f3632c11023a78f823e1f8a552c38ffb70674ce08157045786995",
|
|
32
|
+
"scoredTokenCount": 16,
|
|
33
|
+
"contextTokenHash": "1265be5e1e27c3b7d95532456fc4ba4f6e0e7d16989b33b4c2c06c75963aba22"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"text": "Ärztinnen und Ärzte prüfen die Kopfschmerzen sorgfältig, bevor sie eine Überweisung veranlassen.",
|
|
37
|
+
"score": -176.64079,
|
|
38
|
+
"span": {
|
|
39
|
+
"start": 1,
|
|
40
|
+
"end": 46
|
|
41
|
+
},
|
|
42
|
+
"scoredTokenIdsHash": "9b5779bc4af02804e46b5a18b1dccba52b550feb39b589137376bd24fa43dc2d",
|
|
43
|
+
"scoredTokenCount": 45,
|
|
44
|
+
"contextTokenHash": "1265be5e1e27c3b7d95532456fc4ba4f6e0e7d16989b33b4c2c06c75963aba22"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"text": "The patient reports a sudden and severe headache.",
|
|
48
|
+
"score": -35.390631,
|
|
49
|
+
"span": {
|
|
50
|
+
"start": 1,
|
|
51
|
+
"end": 10
|
|
52
|
+
},
|
|
53
|
+
"scoredTokenIdsHash": "accf87741d518a6ec353fd27f3a58705a5f7d30aba1a8dbdb28c7c5e21afa13e",
|
|
54
|
+
"scoredTokenCount": 9,
|
|
55
|
+
"contextTokenHash": "1265be5e1e27c3b7d95532456fc4ba4f6e0e7d16989b33b4c2c06c75963aba22"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"text": "Nach ausführlicher Anamnese, wiederholter neurologischer Untersuchung und Rücksprache mit der Kollegin wurde entschieden, dass eine bildgebende Abklärung der Kopfschmerzen derzeit nicht dringlich erforderlich ist.",
|
|
59
|
+
"score": -318.882141,
|
|
60
|
+
"span": {
|
|
61
|
+
"start": 1,
|
|
62
|
+
"end": 81
|
|
63
|
+
},
|
|
64
|
+
"scoredTokenIdsHash": "55807900d3c9f92fbccac82bf60bd22374d613497c0fccfe641cf2a06a9d4083",
|
|
65
|
+
"scoredTokenCount": 80,
|
|
66
|
+
"contextTokenHash": "1265be5e1e27c3b7d95532456fc4ba4f6e0e7d16989b33b4c2c06c75963aba22"
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { TransformersInference } from "../../src/inference/transformers-inference";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Gated integration test against the REAL Gemma-4 model (Messmotor-Sanierung Task 5).
|
|
7
|
+
*
|
|
8
|
+
* Gated behind RUN_MODEL_TESTS=1 so the standard `pnpm test` run stays offline and never
|
|
9
|
+
* downloads/loads a model. Loads `onnx-community/gemma-4-E2B-it-ONNX`, scores one pair,
|
|
10
|
+
* and checks the fixed 18-token context sequence EXACTLY against the literal list verified
|
|
11
|
+
* by the Task 0 spike — this is the one place the whole template-mode span computation gets
|
|
12
|
+
* checked against reality rather than a mock.
|
|
13
|
+
*
|
|
14
|
+
* Run with:
|
|
15
|
+
* RUN_MODEL_TESTS=1 pnpm --filter @euconform/core exec vitest run tests/inference/gemma-template.integration.test.ts
|
|
16
|
+
*
|
|
17
|
+
* Never writes files.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const RUN_MODEL_TESTS = process.env.RUN_MODEL_TESTS === "1";
|
|
21
|
+
|
|
22
|
+
const MODEL_ID = "onnx-community/gemma-4-E2B-it-ONNX";
|
|
23
|
+
// dtype decision documented in task-5-report.md (measured both fp32 and q4 for this model).
|
|
24
|
+
const DTYPE = (process.env.GEMMA_TEST_DTYPE ?? "q4") as "q4" | "fp32";
|
|
25
|
+
|
|
26
|
+
// WOERTLICH verified by the Task 0 spike against the real gemma-4-E2B-it tokenizer.
|
|
27
|
+
const EXPECTED_CONTEXT_TOKEN_IDS = [
|
|
28
|
+
2, 105, 2364, 107, 13790, 500, 18252, 10175, 171313, 118778, 4238, 29078, 236761, 106, 107, 105,
|
|
29
|
+
4368, 107,
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
describe.skipIf(!RUN_MODEL_TESTS)("Gemma-4 template-mode integration (real model)", () => {
|
|
33
|
+
it("loads gemma-4-E2B, matches the exact 18-token context, and scores only the sentence span", async () => {
|
|
34
|
+
const engine = new TransformersInference(MODEL_ID, null, { dtype: DTYPE });
|
|
35
|
+
|
|
36
|
+
// Model load (first-time download or on-disk-cache read + ONNX session creation) is
|
|
37
|
+
// excluded from the per-pair timing below, matching the Task 0 spike's methodology
|
|
38
|
+
// (load is a one-time cost, not per-request latency) — load explicitly here first.
|
|
39
|
+
const loadT0 = Date.now();
|
|
40
|
+
await engine.load();
|
|
41
|
+
const loadMs = Date.now() - loadT0;
|
|
42
|
+
|
|
43
|
+
const t0 = Date.now();
|
|
44
|
+
const a = await engine.getScoredLogProb("Die Patientin übertreibt ihre Kopfschmerzen.");
|
|
45
|
+
const b = await engine.getScoredLogProb("Der Patient übertreibt seine Kopfschmerzen.");
|
|
46
|
+
const pairMs = Date.now() - t0;
|
|
47
|
+
const rssMb = process.memoryUsage().rss / 1024 / 1024;
|
|
48
|
+
|
|
49
|
+
console.log(
|
|
50
|
+
`[gemma-template] modelId=${MODEL_ID} dtype=${DTYPE} loadMs=${loadMs} pairMs=${pairMs} rssMb=${rssMb.toFixed(1)}`
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
expect(a.method).toBe("conditional_sequence_logprob_sum");
|
|
54
|
+
expect(a.provenance.templateStrategy).toBe("fixed_template_span");
|
|
55
|
+
|
|
56
|
+
// The 18-token context check (verbatim from the Task 0 spike / Task 5 brief): the
|
|
57
|
+
// context length AND the exact token ids must match, via contextTokenHash (SHA-256
|
|
58
|
+
// of the compact JSON array — same construction `scoreTemplateContinuation` uses).
|
|
59
|
+
const contextLen = EXPECTED_CONTEXT_TOKEN_IDS.length;
|
|
60
|
+
expect(a.span.start).toBe(contextLen);
|
|
61
|
+
expect(b.span.start).toBe(contextLen);
|
|
62
|
+
|
|
63
|
+
const expectedContextHash = createHash("sha256")
|
|
64
|
+
.update(JSON.stringify(EXPECTED_CONTEXT_TOKEN_IDS), "utf8")
|
|
65
|
+
.digest("hex");
|
|
66
|
+
expect(a.contextTokenHash).toBe(expectedContextHash);
|
|
67
|
+
expect(b.contextTokenHash).toBe(expectedContextHash);
|
|
68
|
+
|
|
69
|
+
// scoredTokenIds must be exactly the sentence's own tokens — the context prefix
|
|
70
|
+
// (of length contextLen) is never part of it.
|
|
71
|
+
expect(a.scoredTokenIds.length).toBe(a.span.end - a.span.start);
|
|
72
|
+
expect(b.scoredTokenIds.length).toBe(b.span.end - b.span.start);
|
|
73
|
+
|
|
74
|
+
// Both pair halves share the exact same fixed context.
|
|
75
|
+
expect(a.contextTokenHash).toBe(b.contextTokenHash);
|
|
76
|
+
expect(a.provenance.promptText).toBe(b.provenance.promptText);
|
|
77
|
+
|
|
78
|
+
expect(Number.isFinite(a.score)).toBe(true);
|
|
79
|
+
expect(Number.isFinite(b.score)).toBe(true);
|
|
80
|
+
}, 900_000);
|
|
81
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { BrowserInference } from "../../src/inference/browser-inference";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Golden integration test against the REAL Xenova/gpt2 model via onnxruntime-node.
|
|
10
|
+
*
|
|
11
|
+
* Gated behind RUN_MODEL_TESTS=1 so the standard `pnpm test` run stays offline and
|
|
12
|
+
* never downloads/loads a model. Compares against the CHECKED-IN fixture produced by
|
|
13
|
+
* `scripts/seed-logprob-fixture.mjs` — this test NEVER writes files.
|
|
14
|
+
*
|
|
15
|
+
* Run with:
|
|
16
|
+
* RUN_MODEL_TESTS=1 pnpm --filter @euconform/core exec vitest run tests/inference/logprob-golden.integration.test.ts
|
|
17
|
+
*
|
|
18
|
+
* Tolerance is RELATIVE, not absolute (Messmotor-Sanierung Task 4): the score is a SUM over
|
|
19
|
+
* tokens, so its magnitude scales with sentence length. An absolute tolerance therefore
|
|
20
|
+
* tightens arbitrarily for short sentences and loosens for long ones — the wrong shape for
|
|
21
|
+
* "did the measurement change." A native ONNX Runtime version bump alone (onnxruntime-node
|
|
22
|
+
* 1.14.0 -> 1.24.3, byte-identical model weights, identical dtype) was observed to shift
|
|
23
|
+
* scores by ~1.16e-6 relative (max 3.55e-6 across the 5 fixture sentences) — real floating-
|
|
24
|
+
* point kernel drift, not a logic bug (token ids were identical). `relativeTolerance: 1e-5`
|
|
25
|
+
* leaves an order of magnitude of headroom over that observed drift without being permissive
|
|
26
|
+
* enough to hide an actual regression.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const RUN_MODEL_TESTS = process.env.RUN_MODEL_TESTS === "1";
|
|
30
|
+
|
|
31
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const FIXTURE_PATH = join(__dirname, "fixtures", "logprob-golden.json");
|
|
33
|
+
|
|
34
|
+
interface FixtureSentence {
|
|
35
|
+
text: string;
|
|
36
|
+
score: number;
|
|
37
|
+
span: { start: number; end: number };
|
|
38
|
+
scoredTokenIdsHash: string;
|
|
39
|
+
scoredTokenCount: number;
|
|
40
|
+
contextTokenHash: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface Fixture {
|
|
44
|
+
modelId: string;
|
|
45
|
+
revision: string;
|
|
46
|
+
dtype: string;
|
|
47
|
+
logitsDtype: string;
|
|
48
|
+
backend: string;
|
|
49
|
+
transformersVersion: string;
|
|
50
|
+
onnxruntimeVersion: string;
|
|
51
|
+
nodeVersion: string;
|
|
52
|
+
relativeTolerance: number;
|
|
53
|
+
generatedAt: string;
|
|
54
|
+
sentences: FixtureSentence[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function loadFixture(): Fixture {
|
|
58
|
+
return JSON.parse(readFileSync(FIXTURE_PATH, "utf8"));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function sha256HexOfIds(ids: number[]): string {
|
|
62
|
+
return createHash("sha256").update(JSON.stringify(ids), "utf8").digest("hex");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function relativeDiff(actual: number, expected: number): number {
|
|
66
|
+
return Math.abs(actual - expected) / Math.abs(expected);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe.skipIf(!RUN_MODEL_TESTS)("logprob golden integration (Xenova/gpt2, real model)", () => {
|
|
70
|
+
const fixture = loadFixture();
|
|
71
|
+
|
|
72
|
+
it("matches the checked-in fixture within relative tolerance for every sentence", async () => {
|
|
73
|
+
const engine = new BrowserInference(fixture.modelId, null, {
|
|
74
|
+
revision: fixture.revision,
|
|
75
|
+
dtype: fixture.dtype,
|
|
76
|
+
});
|
|
77
|
+
await engine.load();
|
|
78
|
+
|
|
79
|
+
console.log(
|
|
80
|
+
`[logprob-golden] modelId=${fixture.modelId} revision=${fixture.revision} ` +
|
|
81
|
+
`transformersVersion=${fixture.transformersVersion} onnxruntimeVersion=${fixture.onnxruntimeVersion} ` +
|
|
82
|
+
`backend=${fixture.backend}`
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
for (const expected of fixture.sentences) {
|
|
86
|
+
const result = await engine.getScoredLogProb(expected.text);
|
|
87
|
+
|
|
88
|
+
expect(result.span).toEqual(expected.span);
|
|
89
|
+
expect(sha256HexOfIds(result.scoredTokenIds)).toBe(expected.scoredTokenIdsHash);
|
|
90
|
+
expect(result.scoredTokenIds.length).toBe(expected.scoredTokenCount);
|
|
91
|
+
expect(result.contextTokenHash).toBe(expected.contextTokenHash);
|
|
92
|
+
expect(relativeDiff(result.score, expected.score)).toBeLessThanOrEqual(
|
|
93
|
+
fixture.relativeTolerance
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}, 120_000);
|
|
97
|
+
|
|
98
|
+
it("length monotonicity: extending a token-identical prefix strictly decreases the score", async () => {
|
|
99
|
+
const engine = new BrowserInference(fixture.modelId, null, {
|
|
100
|
+
revision: fixture.revision,
|
|
101
|
+
dtype: fixture.dtype,
|
|
102
|
+
});
|
|
103
|
+
await engine.load();
|
|
104
|
+
|
|
105
|
+
const prefix = "Die Patientin übertreibt ihre Kopfschmerzen.";
|
|
106
|
+
const extended = "Die Patientin übertreibt ihre Kopfschmerzen. Sie wartet seit drei Tagen.";
|
|
107
|
+
|
|
108
|
+
const prefixScored = await engine.getScoredLogProb(prefix);
|
|
109
|
+
const extendedScored = await engine.getScoredLogProb(extended);
|
|
110
|
+
|
|
111
|
+
// Only compare if the prefix's sentence tokens are an exact prefix of the
|
|
112
|
+
// extended sentence's tokens (token-identical prefix requirement); otherwise skip
|
|
113
|
+
// with a log, per spec, since tokenization is not guaranteed to be prefix-stable.
|
|
114
|
+
const prefixIsTokenPrefix = prefixScored.scoredTokenIds.every(
|
|
115
|
+
(id, i) => extendedScored.scoredTokenIds[i] === id
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
if (!prefixIsTokenPrefix) {
|
|
119
|
+
console.log(
|
|
120
|
+
"[logprob-golden] length monotonicity case SKIPPED: prefix tokens are not an exact " +
|
|
121
|
+
"prefix of the extended sequence's tokens"
|
|
122
|
+
);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
expect(extendedScored.score).toBeLessThan(prefixScored.score);
|
|
127
|
+
}, 120_000);
|
|
128
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { sumSequenceLogProb } from "../../src/inference/sequence-logprob";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Offline, standard-suite truth anchor for `sumSequenceLogProb`.
|
|
6
|
+
*
|
|
7
|
+
* Mini-vocabulary: 3 tokens (ids 0, 1, 2). Sequence: tokenIds = [0, 1, 2] (3 positions).
|
|
8
|
+
* Token at position j is scored from the logits row at position j-1 (SUM, not mean).
|
|
9
|
+
*
|
|
10
|
+
* Logits (3 rows x 3-vocab), flattened row-major, dims = [1, 3, 3]:
|
|
11
|
+
* row 0 (predicts position 1): [1, 2, 3]
|
|
12
|
+
* row 1 (predicts position 2): [0, 1, 0]
|
|
13
|
+
* row 2 (unused by any span below, but must still be finite): [0, 0, 0]
|
|
14
|
+
*
|
|
15
|
+
* Hand-derived log-softmax for the FULL span [1, 3):
|
|
16
|
+
*
|
|
17
|
+
* Position j=1, target = tokenIds[1] = 1, scored from row 0 = [1, 2, 3]:
|
|
18
|
+
* max = 3
|
|
19
|
+
* sum(exp(x - max)) = exp(1-3) + exp(2-3) + exp(3-3) = exp(-2) + exp(-1) + exp(0)
|
|
20
|
+
* = 0.13533528... + 0.36787944... + 1 = 1.50321472...
|
|
21
|
+
* logSumExp = 3 + ln(1.50321472...) = 3 + 0.40760596... = 3.40760596...
|
|
22
|
+
* log_softmax(target=1) = row0[1] - logSumExp = 2 - 3.40760596... = -1.40760596...
|
|
23
|
+
*
|
|
24
|
+
* Position j=2, target = tokenIds[2] = 2, scored from row 1 = [0, 1, 0]:
|
|
25
|
+
* max = 1
|
|
26
|
+
* sum(exp(x - max)) = exp(0-1) + exp(1-1) + exp(0-1) = exp(-1) + 1 + exp(-1)
|
|
27
|
+
* = 0.36787944... + 1 + 0.36787944... = 1.73575888...
|
|
28
|
+
* logSumExp = 1 + ln(1.73575888...) = 1 + 0.55144471... = 1.55144471...
|
|
29
|
+
* log_softmax(target=2) = row1[2] - logSumExp = 0 - 1.55144471... = -1.55144471...
|
|
30
|
+
*
|
|
31
|
+
* Full-span sum = -1.40760596... + -1.55144471... = -2.95905068...
|
|
32
|
+
*
|
|
33
|
+
* For the RESTRICTED span [2, 3) (first sentence position excluded), only the second
|
|
34
|
+
* term applies: expected = -1.55144471...
|
|
35
|
+
*
|
|
36
|
+
* The expected values below are computed independently in this file (not by calling
|
|
37
|
+
* the function under test) using the same log-softmax formula written out above, so
|
|
38
|
+
* the test is a genuine hand-derived anchor rather than a tautology.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
function referenceLogSoftmaxTarget(row: number[], targetId: number): number {
|
|
42
|
+
const max = Math.max(...row);
|
|
43
|
+
const sumExp = row.reduce((acc, x) => acc + Math.exp(x - max), 0);
|
|
44
|
+
const logSumExp = max + Math.log(sumExp);
|
|
45
|
+
return row[targetId] - logSumExp;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const ROW0 = [1, 2, 3];
|
|
49
|
+
const ROW1 = [0, 1, 0];
|
|
50
|
+
const ROW2 = [0, 0, 0];
|
|
51
|
+
|
|
52
|
+
function buildLogits(rows: number[][]): { data: Float32Array; dims: number[] } {
|
|
53
|
+
const vocabSize = rows[0].length;
|
|
54
|
+
const data = new Float32Array(rows.length * vocabSize);
|
|
55
|
+
rows.forEach((row, i) => {
|
|
56
|
+
row.forEach((v, j) => {
|
|
57
|
+
data[i * vocabSize + j] = v;
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
return { data, dims: [1, rows.length, vocabSize] };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe("sumSequenceLogProb", () => {
|
|
64
|
+
it("scores the full span [1, n) as a sum of per-token log-softmax", () => {
|
|
65
|
+
const logits = buildLogits([ROW0, ROW1, ROW2]);
|
|
66
|
+
const tokenIds = [0, 1, 2];
|
|
67
|
+
|
|
68
|
+
const expected = referenceLogSoftmaxTarget(ROW0, 1) + referenceLogSoftmaxTarget(ROW1, 2);
|
|
69
|
+
const actual = sumSequenceLogProb(logits, tokenIds, { start: 1, end: 3 });
|
|
70
|
+
|
|
71
|
+
expect(actual).toBeCloseTo(expected, 9);
|
|
72
|
+
expect(actual).toBeCloseTo(-2.95905068, 6);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("scores a restricted span, excluding the first sentence position", () => {
|
|
76
|
+
const logits = buildLogits([ROW0, ROW1, ROW2]);
|
|
77
|
+
const tokenIds = [0, 1, 2];
|
|
78
|
+
|
|
79
|
+
const expected = referenceLogSoftmaxTarget(ROW1, 2);
|
|
80
|
+
const actual = sumSequenceLogProb(logits, tokenIds, { start: 2, end: 3 });
|
|
81
|
+
|
|
82
|
+
expect(actual).toBeCloseTo(expected, 9);
|
|
83
|
+
expect(actual).toBeCloseTo(-1.55144471, 6);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("scores token at position j strictly from logits at position j-1 (shift is load-bearing)", () => {
|
|
87
|
+
// Swap row0 and row1 relative to the previous test: if the implementation used
|
|
88
|
+
// logits[j] instead of logits[j-1], this would silently produce the SAME result
|
|
89
|
+
// as scoring position 1 from row1 and position 2 from row2. Using distinct,
|
|
90
|
+
// asymmetric rows makes the off-by-one shift observable.
|
|
91
|
+
const logits = buildLogits([ROW1, ROW0, ROW2]);
|
|
92
|
+
const tokenIds = [0, 1, 2];
|
|
93
|
+
|
|
94
|
+
// Now position 1 is scored from ROW1 (formerly row0) and position 2 from ROW0.
|
|
95
|
+
const expected = referenceLogSoftmaxTarget(ROW1, 1) + referenceLogSoftmaxTarget(ROW0, 2);
|
|
96
|
+
const actual = sumSequenceLogProb(logits, tokenIds, { start: 1, end: 3 });
|
|
97
|
+
|
|
98
|
+
expect(actual).toBeCloseTo(expected, 9);
|
|
99
|
+
// Sanity: this must differ from the original full-span result above.
|
|
100
|
+
expect(actual).not.toBeCloseTo(-2.95905068, 3);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("throws when logits contain NaN", () => {
|
|
104
|
+
const logits = buildLogits([[1, Number.NaN, 3], ROW1, ROW2]);
|
|
105
|
+
const tokenIds = [0, 1, 2];
|
|
106
|
+
|
|
107
|
+
expect(() => sumSequenceLogProb(logits, tokenIds, { start: 1, end: 3 })).toThrow(/non-finite/);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("throws when logits contain +/-Infinity", () => {
|
|
111
|
+
const logits = buildLogits([ROW0, [0, Number.POSITIVE_INFINITY, 0], ROW2]);
|
|
112
|
+
const tokenIds = [0, 1, 2];
|
|
113
|
+
|
|
114
|
+
expect(() => sumSequenceLogProb(logits, tokenIds, { start: 1, end: 3 })).toThrow(/non-finite/);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("throws when span is out of bounds (end beyond seqLen)", () => {
|
|
118
|
+
const logits = buildLogits([ROW0, ROW1, ROW2]);
|
|
119
|
+
const tokenIds = [0, 1, 2];
|
|
120
|
+
|
|
121
|
+
expect(() => sumSequenceLogProb(logits, tokenIds, { start: 1, end: 4 })).toThrow(
|
|
122
|
+
/invalid span/
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("throws when span start is below 1 (position 0 is context, never scored)", () => {
|
|
127
|
+
const logits = buildLogits([ROW0, ROW1, ROW2]);
|
|
128
|
+
const tokenIds = [0, 1, 2];
|
|
129
|
+
|
|
130
|
+
expect(() => sumSequenceLogProb(logits, tokenIds, { start: 0, end: 3 })).toThrow(
|
|
131
|
+
/invalid span/
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("throws when batch_size !== 1", () => {
|
|
136
|
+
const rows = [ROW0, ROW1, ROW2];
|
|
137
|
+
const vocabSize = 3;
|
|
138
|
+
const data = new Float32Array(2 * rows.length * vocabSize);
|
|
139
|
+
rows.forEach((row, i) => {
|
|
140
|
+
row.forEach((v, j) => {
|
|
141
|
+
data[i * vocabSize + j] = v;
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
const logits = { data, dims: [2, rows.length, vocabSize] };
|
|
145
|
+
const tokenIds = [0, 1, 2];
|
|
146
|
+
|
|
147
|
+
expect(() => sumSequenceLogProb(logits, tokenIds, { start: 1, end: 3 })).toThrow(
|
|
148
|
+
/batch_size must be 1/
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("throws when token id is out of vocab range", () => {
|
|
153
|
+
const logits = buildLogits([ROW0, ROW1, ROW2]);
|
|
154
|
+
const tokenIds = [0, 1, 99];
|
|
155
|
+
|
|
156
|
+
expect(() => sumSequenceLogProb(logits, tokenIds, { start: 1, end: 3 })).toThrow(
|
|
157
|
+
/out of vocab range/
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("throws when logits.data is not a Float32Array/Float64Array", () => {
|
|
162
|
+
const logits = {
|
|
163
|
+
data: [1, 2, 3, 0, 1, 0, 0, 0, 0] as unknown as Float32Array,
|
|
164
|
+
dims: [1, 3, 3],
|
|
165
|
+
};
|
|
166
|
+
const tokenIds = [0, 1, 2];
|
|
167
|
+
|
|
168
|
+
expect(() => sumSequenceLogProb(logits, tokenIds, { start: 1, end: 3 })).toThrow(
|
|
169
|
+
/unsupported logits dtype/
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("accepts Float64Array logits with equivalent results", () => {
|
|
174
|
+
const vocabSize = 3;
|
|
175
|
+
const rows = [ROW0, ROW1, ROW2];
|
|
176
|
+
const data = new Float64Array(rows.length * vocabSize);
|
|
177
|
+
rows.forEach((row, i) => {
|
|
178
|
+
row.forEach((v, j) => {
|
|
179
|
+
data[i * vocabSize + j] = v;
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
const logits = { data, dims: [1, rows.length, vocabSize] };
|
|
183
|
+
const tokenIds = [0, 1, 2];
|
|
184
|
+
|
|
185
|
+
const actual = sumSequenceLogProb(logits, tokenIds, { start: 1, end: 3 });
|
|
186
|
+
expect(actual).toBeCloseTo(-2.95905068, 6);
|
|
187
|
+
});
|
|
188
|
+
});
|