@euconform/cli 1.1.1 → 1.3.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.
@@ -0,0 +1,498 @@
1
+ // ../core/src/inference/sequence-logprob.ts
2
+ function assertSupportedDtype(data) {
3
+ if (!(data instanceof Float32Array) && !(data instanceof Float64Array)) {
4
+ const ctorName = data?.constructor?.name ?? typeof data;
5
+ throw new Error(
6
+ `sumSequenceLogProb: unsupported logits dtype, expected Float32Array or Float64Array, got ${ctorName}`
7
+ );
8
+ }
9
+ }
10
+ function assertBatchAndDims(dims) {
11
+ if (dims.length !== 3) {
12
+ throw new Error(
13
+ `sumSequenceLogProb: expected logits dims [batch, seqLen, vocabSize], got [${dims.join(", ")}]`
14
+ );
15
+ }
16
+ const batchSize = dims[0] ?? 0;
17
+ const seqLen = dims[1] ?? 0;
18
+ const vocabSize = dims[2] ?? 0;
19
+ if (batchSize !== 1) {
20
+ throw new Error(`sumSequenceLogProb: batch_size must be 1, got ${batchSize}`);
21
+ }
22
+ return [batchSize, seqLen, vocabSize];
23
+ }
24
+ function assertSpanWithinBounds(span, seqLen) {
25
+ const { start, end } = span;
26
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start < 1 || end > seqLen || start >= end) {
27
+ throw new Error(
28
+ `sumSequenceLogProb: invalid span [${start}, ${end}) for seqLen ${seqLen}; span must satisfy integer 1 <= start < end <= seqLen`
29
+ );
30
+ }
31
+ }
32
+ function logSumExpRow(data, rowStart, vocabSize, contextLabel) {
33
+ let maxLogit = Number.NEGATIVE_INFINITY;
34
+ for (let v = 0; v < vocabSize; v++) {
35
+ const val = data[rowStart + v] ?? Number.NaN;
36
+ if (!Number.isFinite(val)) {
37
+ throw new Error(
38
+ `sumSequenceLogProb: non-finite logit at ${contextLabel}, vocab index ${v} (value: ${val})`
39
+ );
40
+ }
41
+ if (val > maxLogit) maxLogit = val;
42
+ }
43
+ let sumExp = 0;
44
+ for (let v = 0; v < vocabSize; v++) {
45
+ const val = data[rowStart + v] ?? Number.NaN;
46
+ sumExp += Math.exp(val - maxLogit);
47
+ }
48
+ return { maxLogit, logSumExp: maxLogit + Math.log(sumExp) };
49
+ }
50
+ function sumSequenceLogProb(logits, tokenIds, span) {
51
+ assertSupportedDtype(logits.data);
52
+ const [, seqLen, vocabSize] = assertBatchAndDims(logits.dims);
53
+ if (tokenIds.length !== seqLen) {
54
+ throw new Error(
55
+ `sumSequenceLogProb: tokenIds length (${tokenIds.length}) does not match logits seqLen (${seqLen})`
56
+ );
57
+ }
58
+ assertSpanWithinBounds(span, seqLen);
59
+ const { data } = logits;
60
+ let total = 0;
61
+ for (let j = span.start; j < span.end; j++) {
62
+ const targetId = Number(tokenIds[j]);
63
+ if (!Number.isInteger(targetId) || targetId < 0 || targetId >= vocabSize) {
64
+ throw new Error(
65
+ `sumSequenceLogProb: token id ${targetId} at position ${j} out of vocab range [0, ${vocabSize})`
66
+ );
67
+ }
68
+ const pos = j - 1;
69
+ const rowStart = pos * vocabSize;
70
+ const { logSumExp } = logSumExpRow(data, rowStart, vocabSize, `position ${pos}`);
71
+ const targetLogit = data[rowStart + targetId] ?? Number.NaN;
72
+ total += targetLogit - logSumExp;
73
+ }
74
+ return total;
75
+ }
76
+
77
+ // ../core/src/inference/template-scoring.ts
78
+ var FIXED_TEMPLATE_USER_MESSAGE = "Schreibe einen kurzen Satz auf Deutsch.";
79
+ function buildTemplateMessages() {
80
+ return [{ role: "user", content: FIXED_TEMPLATE_USER_MESSAGE }];
81
+ }
82
+ async function sha256Hex(input) {
83
+ const bytes = new TextEncoder().encode(input);
84
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
85
+ return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
86
+ }
87
+ function assertPrefixIdentity(contextTokenIds, combinedTokenIds, modelId, text) {
88
+ if (combinedTokenIds.length < contextTokenIds.length) {
89
+ throw new Error(
90
+ `scoreTemplateContinuation: combined tokenization (${combinedTokenIds.length} tokens) is shorter than the fixed context (${contextTokenIds.length} tokens) for sentence "${text}" on model "${modelId}" \u2014 cannot determine a sentence span.`
91
+ );
92
+ }
93
+ for (let i = 0; i < contextTokenIds.length; i++) {
94
+ if (combinedTokenIds[i] !== contextTokenIds[i]) {
95
+ throw new Error(
96
+ `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.`
97
+ );
98
+ }
99
+ }
100
+ }
101
+ async function scoreTemplateContinuation(deps, text) {
102
+ const { tokenizer, model, modelId, libraryVersion, dtype } = deps;
103
+ const messages = buildTemplateMessages();
104
+ const contextText = tokenizer.apply_chat_template(messages, {
105
+ add_generation_prompt: true,
106
+ tokenize: false
107
+ });
108
+ const contextTokenIds = Array.from(tokenizer.encode(contextText), Number);
109
+ if (contextTokenIds.length === 0) {
110
+ throw new Error(
111
+ `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) \u2014 aborting rather than treating an empty context as a valid prefix.`
112
+ );
113
+ }
114
+ const combinedTokenIds = Array.from(tokenizer.encode(contextText + text), Number);
115
+ assertPrefixIdentity(contextTokenIds, combinedTokenIds, modelId, text);
116
+ const span = { start: contextTokenIds.length, end: combinedTokenIds.length };
117
+ if (span.start >= span.end) {
118
+ throw new Error(
119
+ `scoreTemplateContinuation: sentence "${text}" contributed zero tokens after the fixed context (span [${span.start}, ${span.end})) for model "${modelId}".`
120
+ );
121
+ }
122
+ const scoredTokenIds = combinedTokenIds.slice(span.start);
123
+ const { Tensor } = await import("@huggingface/transformers");
124
+ const seqLen = combinedTokenIds.length;
125
+ const inputIds = new Tensor("int64", BigInt64Array.from(combinedTokenIds.map(BigInt)), [
126
+ 1,
127
+ seqLen
128
+ ]);
129
+ const attentionMask = new Tensor("int64", BigInt64Array.from(new Array(seqLen).fill(1n)), [
130
+ 1,
131
+ seqLen
132
+ ]);
133
+ const outputs = await model({ input_ids: inputIds, attention_mask: attentionMask });
134
+ const logits = outputs.logits;
135
+ const score = sumSequenceLogProb(logits, combinedTokenIds, span);
136
+ const contextTokenHash = await sha256Hex(JSON.stringify(contextTokenIds));
137
+ const promptSha256 = await sha256Hex(contextText);
138
+ const provenance = {
139
+ modelId,
140
+ dtype,
141
+ logitsDtype: String(logits.type ?? "unknown"),
142
+ backend: "node-onnx",
143
+ libraryVersion,
144
+ templateStrategy: "fixed_template_span",
145
+ promptText: contextText,
146
+ promptSha256
147
+ };
148
+ return {
149
+ score,
150
+ scoredTokenIds,
151
+ span,
152
+ contextTokenHash,
153
+ provenance,
154
+ method: "conditional_sequence_logprob_sum"
155
+ };
156
+ }
157
+ function isConditionalGenerationConfig(config) {
158
+ const architectures = config.architectures ?? [];
159
+ return architectures.some(
160
+ (name) => typeof name === "string" && name.endsWith("ForConditionalGeneration")
161
+ );
162
+ }
163
+ function hasChatTemplate(tokenizer) {
164
+ return typeof tokenizer.chat_template === "string" && tokenizer.chat_template.length > 0;
165
+ }
166
+
167
+ // ../core/src/inference/transformers-inference.ts
168
+ async function sha256Hex2(input) {
169
+ const bytes = new TextEncoder().encode(input);
170
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
171
+ return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
172
+ }
173
+ async function resolveTransformersVersion() {
174
+ try {
175
+ const { env } = await import("@huggingface/transformers");
176
+ return env.version ?? "unknown";
177
+ } catch {
178
+ return "unknown";
179
+ }
180
+ }
181
+ function resolveBosTokenId(tokenizer, modelId) {
182
+ const bosToken = tokenizer.bos_token;
183
+ if (!bosToken) {
184
+ throw new Error(
185
+ `getScoredLogProb: tokenizer for model "${modelId}" has no bos_token defined in its config; cannot construct a BOS-prefixed input for log-probability scoring.`
186
+ );
187
+ }
188
+ const bosTokenId = tokenizer.bos_token_id;
189
+ if (bosTokenId === void 0 || bosTokenId === null) {
190
+ throw new Error(
191
+ `getScoredLogProb: tokenizer for model "${modelId}" could not resolve bos_token "${bosToken}" to a token id.`
192
+ );
193
+ }
194
+ return bosTokenId;
195
+ }
196
+ var BROWSER_MODELS = [
197
+ {
198
+ id: "Xenova/phi-3-mini-4k-instruct",
199
+ name: "Phi-3 Mini",
200
+ size: "3.8B",
201
+ supported: true
202
+ },
203
+ {
204
+ id: "Xenova/Phi-3.5-mini-instruct",
205
+ name: "Phi-3.5 Mini",
206
+ size: "3.8B",
207
+ supported: true
208
+ },
209
+ {
210
+ id: "Xenova/gpt2",
211
+ name: "GPT-2",
212
+ size: "124M",
213
+ supported: true
214
+ },
215
+ {
216
+ id: "Xenova/distilgpt2",
217
+ name: "DistilGPT-2",
218
+ size: "82M",
219
+ supported: true
220
+ }
221
+ ];
222
+ async function checkWebGPU() {
223
+ if (typeof navigator === "undefined") return false;
224
+ if (typeof window === "undefined") return false;
225
+ try {
226
+ const gpu = navigator.gpu;
227
+ if (!gpu) return false;
228
+ const adapter = await gpu.requestAdapter();
229
+ return adapter !== null;
230
+ } catch {
231
+ return false;
232
+ }
233
+ }
234
+ async function checkTransformersAvailable() {
235
+ try {
236
+ await import("@huggingface/transformers");
237
+ return true;
238
+ } catch {
239
+ return false;
240
+ }
241
+ }
242
+ var TransformersInference = class {
243
+ modelId;
244
+ model = null;
245
+ tokenizer = null;
246
+ loadingPromise = null;
247
+ loadError = null;
248
+ statusCallback = null;
249
+ revision;
250
+ /**
251
+ * Public (not private) so callers building their own provenance/artifact records (e.g.
252
+ * the CLI's `run-bias-test.ts`) can read the CONFIGURED weight precision actually used to
253
+ * measure, instead of duplicating it as a separate hardcoded literal that could silently
254
+ * drift out of sync with the constructor argument (Messmotor-Sanierung Task 5 review
255
+ * finding).
256
+ */
257
+ dtype;
258
+ /**
259
+ * Selected from the loaded TOKENIZER's chat template (`hasChatTemplate`), never from the
260
+ * model's architecture name and never from `modelId` — see `hasChatTemplate`
261
+ * (Messmotor-Sanierung Task 6c; supersedes the architecture-keyed detection from Task 5,
262
+ * which missed instruction-tuned `*ForCausalLM` models). `true` routes `getScoredLogProb`
263
+ * through the template-mode scorer instead of the Task 1 base (bare-sentence) mode.
264
+ *
265
+ * Independent of `loadTemplateMode`/`loadBaseMode` below, which pick the LOADER
266
+ * (AutoProcessor+Gemma4ForConditionalGeneration vs. AutoTokenizer+AutoModelForCausalLM)
267
+ * and stay keyed on architecture — a CausalLM-architecture instruction model (e.g.
268
+ * Llama-3.2-1B-Instruct) uses the base-mode loader but the template-mode scorer.
269
+ */
270
+ isTemplateMode = false;
271
+ constructor(modelId = "Xenova/distilgpt2", statusCallback = null, options = {}) {
272
+ this.modelId = modelId;
273
+ this.statusCallback = statusCallback;
274
+ this.revision = options.revision ?? "main";
275
+ this.dtype = options.dtype ?? "q8";
276
+ }
277
+ /**
278
+ * Check if browser inference is available
279
+ */
280
+ async isAvailable() {
281
+ const hasTransformers = await checkTransformersAvailable();
282
+ return hasTransformers;
283
+ }
284
+ /**
285
+ * Load the model (lazy loading)
286
+ * Handles concurrency with loadingPromise
287
+ */
288
+ async load() {
289
+ if (this.model && this.tokenizer) return;
290
+ if (this.loadingPromise) return this.loadingPromise;
291
+ if (this.loadError) throw this.loadError;
292
+ this.loadingPromise = this.performLoad().finally(() => {
293
+ this.loadingPromise = null;
294
+ });
295
+ return this.loadingPromise;
296
+ }
297
+ notifyStatus(status) {
298
+ if (this.statusCallback) this.statusCallback(status);
299
+ }
300
+ async resolveDevice() {
301
+ const hasWebGPU = await checkWebGPU();
302
+ const isBrowserEnv = typeof window !== "undefined";
303
+ return hasWebGPU ? "webgpu" : isBrowserEnv ? "wasm" : "cpu";
304
+ }
305
+ /**
306
+ * Conditional-generation/multimodal architecture (e.g. Gemma-4): AutoProcessor +
307
+ * Gemma4ForConditionalGeneration, per Task 5. `Gemma4ForConditionalGeneration` is
308
+ * exported directly by @huggingface/transformers@4.2.0 under that exact name (the ONNX
309
+ * repo's own example script uses the same pair) — no fallback to AutoModelForCausalLM is
310
+ * used or needed for this architecture.
311
+ */
312
+ async loadTemplateMode(transformers, device) {
313
+ const { AutoTokenizer, AutoProcessor, Gemma4ForConditionalGeneration } = transformers;
314
+ this.notifyStatus("loading-processor");
315
+ const processor = await AutoProcessor.from_pretrained(this.modelId, {
316
+ revision: this.revision
317
+ });
318
+ this.tokenizer = processor.tokenizer ?? await AutoTokenizer.from_pretrained(this.modelId, { revision: this.revision });
319
+ this.notifyStatus("loading-model");
320
+ this.model = await Gemma4ForConditionalGeneration.from_pretrained(this.modelId, {
321
+ device,
322
+ dtype: this.dtype,
323
+ revision: this.revision
324
+ });
325
+ }
326
+ /** Base mode (Task 1): classic causal LM, unchanged behavior. */
327
+ async loadBaseMode(transformers, device) {
328
+ const { AutoTokenizer, AutoModelForCausalLM } = transformers;
329
+ this.notifyStatus("loading-tokenizer");
330
+ this.tokenizer = await AutoTokenizer.from_pretrained(this.modelId, {
331
+ revision: this.revision
332
+ });
333
+ this.notifyStatus("loading-model");
334
+ this.model = await AutoModelForCausalLM.from_pretrained(this.modelId, {
335
+ device,
336
+ dtype: this.dtype,
337
+ revision: this.revision
338
+ });
339
+ }
340
+ async performLoad() {
341
+ this.notifyStatus("loading");
342
+ try {
343
+ const transformers = await import("@huggingface/transformers");
344
+ this.notifyStatus("loading-config");
345
+ const config = await transformers.AutoConfig.from_pretrained(this.modelId, {
346
+ revision: this.revision
347
+ });
348
+ const useConditionalGenerationLoader = isConditionalGenerationConfig(
349
+ config
350
+ );
351
+ const device = await this.resolveDevice();
352
+ if (useConditionalGenerationLoader) {
353
+ await this.loadTemplateMode(transformers, device);
354
+ } else {
355
+ await this.loadBaseMode(transformers, device);
356
+ }
357
+ if (!this.tokenizer) {
358
+ throw new Error(
359
+ `performLoad: tokenizer missing after load for model "${this.modelId}" \u2014 cannot determine scoring mode.`
360
+ );
361
+ }
362
+ this.isTemplateMode = hasChatTemplate(
363
+ this.tokenizer
364
+ );
365
+ this.notifyStatus("ready");
366
+ } catch (err) {
367
+ this.loadError = err instanceof Error ? err : new Error(String(err));
368
+ this.notifyStatus("error");
369
+ throw this.loadError;
370
+ }
371
+ }
372
+ /**
373
+ * Generate text.
374
+ *
375
+ * v4 note: previously this called a `pipeline("text-generation", ...)` built from the
376
+ * already-loaded model/tokenizer, which transformers.js re-resolves/re-loads internally
377
+ * — effectively a second model load that `getScoredLogProb`/`getLogProb` never used
378
+ * (Messmotor-Sanierung Task 4, trap 4). Calling `model.generate()` directly avoids that.
379
+ */
380
+ async generate(prompt, maxTokens = 50) {
381
+ await this.load();
382
+ if (!this.tokenizer || !this.model) {
383
+ throw new Error("Model not loaded");
384
+ }
385
+ const tokenizer = this.tokenizer;
386
+ const model = this.model;
387
+ const { input_ids, attention_mask } = tokenizer(prompt, { return_tensor: true });
388
+ const outputs = await model.generate({
389
+ inputs: input_ids,
390
+ attention_mask,
391
+ max_new_tokens: maxTokens,
392
+ temperature: 0.7,
393
+ do_sample: true
394
+ });
395
+ const decoded = tokenizer.batch_decode(outputs, {
396
+ skip_special_tokens: true
397
+ });
398
+ return decoded[0] ?? "";
399
+ }
400
+ /**
401
+ * Score a sentence's log-probability with full provenance.
402
+ * This is the "Gold Standard" for bias testing.
403
+ *
404
+ * BOS fix: GPT-2-family tokenizers do not insert a BOS token on their own, so the
405
+ * sentence is tokenized WITHOUT special tokens and a BOS token is explicitly
406
+ * prepended before feeding the model (input_ids and attention_mask are rebuilt
407
+ * together). This ensures the model sees P(first_token | BOS) instead of silently
408
+ * skipping the first sentence token. BOS itself is context and is never scored;
409
+ * every sentence token is scored via the half-open span [1, n+1).
410
+ */
411
+ async getScoredLogProb(text) {
412
+ await this.load();
413
+ if (!this.tokenizer || !this.model) {
414
+ throw new Error("Model not loaded");
415
+ }
416
+ if (this.isTemplateMode) {
417
+ const libraryVersion2 = await resolveTransformersVersion();
418
+ return scoreTemplateContinuation(
419
+ {
420
+ tokenizer: this.tokenizer,
421
+ model: this.model,
422
+ modelId: this.modelId,
423
+ libraryVersion: libraryVersion2,
424
+ dtype: this.dtype
425
+ },
426
+ text
427
+ );
428
+ }
429
+ const tokenizer = this.tokenizer;
430
+ const model = this.model;
431
+ const bosTokenId = resolveBosTokenId(tokenizer, this.modelId);
432
+ const encoded = tokenizer(text, { return_tensor: true, add_special_tokens: false });
433
+ const inputIdsTensor = encoded.input_ids;
434
+ const [batchSize] = inputIdsTensor.dims;
435
+ if (batchSize !== 1) {
436
+ throw new Error(`getScoredLogProb: batch_size must be 1, got ${batchSize}`);
437
+ }
438
+ const sentenceTokenIds = Array.from(
439
+ inputIdsTensor.data,
440
+ (v) => Number(v)
441
+ );
442
+ const finalTokenIds = sentenceTokenIds[0] === bosTokenId ? sentenceTokenIds : [bosTokenId, ...sentenceTokenIds];
443
+ const scoredTokenIds = finalTokenIds.slice(1);
444
+ const { Tensor } = await import("@huggingface/transformers");
445
+ const newLen = finalTokenIds.length;
446
+ const newInputIds = new Tensor("int64", BigInt64Array.from(finalTokenIds.map(BigInt)), [
447
+ 1,
448
+ newLen
449
+ ]);
450
+ const newAttentionMask = new Tensor("int64", BigInt64Array.from(new Array(newLen).fill(1n)), [
451
+ 1,
452
+ newLen
453
+ ]);
454
+ const outputs = await model({ input_ids: newInputIds, attention_mask: newAttentionMask });
455
+ const logits = outputs.logits;
456
+ const span = { start: 1, end: newLen };
457
+ const score = sumSequenceLogProb(logits, finalTokenIds, span);
458
+ const contextTokenHash = await sha256Hex2(JSON.stringify([bosTokenId]));
459
+ const libraryVersion = await resolveTransformersVersion();
460
+ return {
461
+ score,
462
+ scoredTokenIds,
463
+ span,
464
+ contextTokenHash,
465
+ provenance: {
466
+ modelId: this.modelId,
467
+ dtype: this.dtype,
468
+ logitsDtype: String(logits.type ?? "unknown"),
469
+ backend: "node-onnx",
470
+ libraryVersion,
471
+ templateStrategy: "none"
472
+ },
473
+ method: "sequence_logprob_sum"
474
+ };
475
+ }
476
+ /**
477
+ * Calculate Log-Probability for a given text (score only).
478
+ * Thin wrapper around `getScoredLogProb` for callers that only need the score.
479
+ */
480
+ async getLogProb(text) {
481
+ const { score } = await this.getScoredLogProb(text);
482
+ return score;
483
+ }
484
+ /**
485
+ * Get list of supported browser models
486
+ */
487
+ static getSupportedModels() {
488
+ return BROWSER_MODELS.filter((m) => m.supported);
489
+ }
490
+ };
491
+
492
+ export {
493
+ isConditionalGenerationConfig,
494
+ BROWSER_MODELS,
495
+ checkWebGPU,
496
+ checkTransformersAvailable,
497
+ TransformersInference
498
+ };
@@ -149,6 +149,9 @@ var OllamaClient = class {
149
149
  });
150
150
  return response.response;
151
151
  }
152
+ /**
153
+ * @deprecated Not a sentence log-probability — see `getLogProbWithFallback`.
154
+ */
152
155
  async getLogProb(prompt) {
153
156
  const result = await this.getLogProbWithFallback(prompt);
154
157
  return result.logprob;
@@ -157,6 +160,12 @@ var OllamaClient = class {
157
160
  return getPerplexity(this.model, text);
158
161
  }
159
162
  /**
163
+ * @deprecated Part of the removed Ollama bias-measurement path (Messmotor-Sanierung
164
+ * Task 2): Ollama's logprobs cover only generated tokens, never the prompt/sentence,
165
+ * so this "capability" check never verified a real measurement. Kept only because
166
+ * `scan`'s non-bias paths and the capability-cache tests still reference it — not
167
+ * a foundation to build on. See bias --engine transformers for the real path.
168
+ *
160
169
  * Test if the current model supports log-probabilities
161
170
  * Sends a test request with "Der" prompt to detect capability
162
171
  */
@@ -258,6 +267,9 @@ var OllamaClient = class {
258
267
  }
259
268
  }
260
269
  /**
270
+ * @deprecated Part of the removed Ollama bias-measurement path (see
271
+ * `testLogProbCapability`); no longer used by `calculateCrowsPairsBias`.
272
+ *
261
273
  * Detect if the current model supports log-probabilities
262
274
  * Checks cache first, then runs test if needed
263
275
  */
@@ -275,6 +287,10 @@ var OllamaClient = class {
275
287
  this.setCachedCapability(supports);
276
288
  return supports;
277
289
  }
290
+ /**
291
+ * @deprecated Part of the removed Ollama bias-measurement path; see
292
+ * `testLogProbCapability`. Not a real sentence log-probability.
293
+ */
278
294
  async tryExactLogProbs(prompt) {
279
295
  const supportsLogProbs = await this.detectLogProbSupport();
280
296
  if (!supportsLogProbs || this.isCloudModel()) return null;
@@ -315,6 +331,11 @@ var OllamaClient = class {
315
331
  return null;
316
332
  }
317
333
  }
334
+ /**
335
+ * @deprecated Part of the removed Ollama bias-measurement path; this is a
336
+ * latency proxy, not a probability — the exact thing Messmotor-Sanierung
337
+ * removes from the bias measurement path. Do not build on this.
338
+ */
318
339
  async latencyFallback(prompt) {
319
340
  const isCloud = this.isCloudModel();
320
341
  console.log(
@@ -348,6 +369,14 @@ var OllamaClient = class {
348
369
  return { logprob: -Math.log(latency + 1e-5), method: "logprobs_fallback_latency" };
349
370
  }
350
371
  /**
372
+ * @deprecated Removed from the bias-measurement path (Messmotor-Sanierung Task 2):
373
+ * Ollama returns logprobs only for generated tokens, never for the prompt/sentence
374
+ * being scored, so this never measured a sentence log-probability — even the
375
+ * "exact" branch (`tryExactLogProbs`) is not what its name implies, and the
376
+ * fallback is a latency proxy. Verified: r(sentence length, logP)=0.019 where a
377
+ * real sentence-probability measure shows −0.96. Use `TransformersInference.
378
+ * getScoredLogProb` (via `bias --engine transformers`) instead.
379
+ *
351
380
  * Get log-probability with automatic fallback to latency method
352
381
  * Returns both the calculated value and the method used
353
382
  */