@axlsdk/eval 0.17.9 → 0.18.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/README.md +48 -7
- package/dist/{chunk-NETD4AUB.js → chunk-BWBX5YVQ.js} +231 -162
- package/dist/chunk-BWBX5YVQ.js.map +1 -0
- package/dist/cli.cjs +497 -207
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +279 -47
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +301 -174
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +176 -1
- package/dist/index.d.ts +176 -1
- package/dist/index.js +79 -16
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/dist/chunk-NETD4AUB.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -34,6 +34,8 @@ __export(index_exports, {
|
|
|
34
34
|
dataset: () => dataset,
|
|
35
35
|
defineEval: () => defineEval,
|
|
36
36
|
evalCompare: () => evalCompare,
|
|
37
|
+
evaluateScorerErrorRateGate: () => evaluateScorerErrorRateGate,
|
|
38
|
+
evaluateScorerTolerance: () => evaluateScorerTolerance,
|
|
37
39
|
llmScorer: () => llmScorer,
|
|
38
40
|
normalizeScorerResult: () => normalizeScorerResult,
|
|
39
41
|
pairedBootstrapCI: () => pairedBootstrapCI,
|
|
@@ -46,11 +48,71 @@ module.exports = __toCommonJS(index_exports);
|
|
|
46
48
|
// src/dataset.ts
|
|
47
49
|
var import_promises = require("fs/promises");
|
|
48
50
|
var path = __toESM(require("path"), 1);
|
|
51
|
+
function isPlainObject(value) {
|
|
52
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
53
|
+
const proto = Object.getPrototypeOf(value);
|
|
54
|
+
return proto === Object.prototype || proto === null;
|
|
55
|
+
}
|
|
56
|
+
function isObjectSchema(schema) {
|
|
57
|
+
const shape = schema.shape;
|
|
58
|
+
return typeof shape === "object" && shape !== null;
|
|
59
|
+
}
|
|
60
|
+
function collectDroppedKeys(raw, parsed, prefix = "") {
|
|
61
|
+
if (Array.isArray(raw) && Array.isArray(parsed)) {
|
|
62
|
+
const dropped = [];
|
|
63
|
+
const n = Math.min(raw.length, parsed.length);
|
|
64
|
+
for (let i = 0; i < n; i++) {
|
|
65
|
+
dropped.push(...collectDroppedKeys(raw[i], parsed[i], `${prefix}[${i}]`));
|
|
66
|
+
}
|
|
67
|
+
return dropped;
|
|
68
|
+
}
|
|
69
|
+
if (isPlainObject(raw) && isPlainObject(parsed)) {
|
|
70
|
+
const dropped = [];
|
|
71
|
+
for (const key of Object.keys(raw)) {
|
|
72
|
+
const path2 = prefix ? `${prefix}.${key}` : key;
|
|
73
|
+
if (!(key in parsed)) {
|
|
74
|
+
dropped.push(path2);
|
|
75
|
+
} else {
|
|
76
|
+
dropped.push(...collectDroppedKeys(raw[key], parsed[key], path2));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return dropped;
|
|
80
|
+
}
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
49
83
|
function dataset(config) {
|
|
84
|
+
const policy = config.onExtraAnnotationKeys ?? "warn";
|
|
85
|
+
function parseItem(item, droppedAcc) {
|
|
86
|
+
const input = config.schema.parse(item.input);
|
|
87
|
+
if (item.annotations == null || !config.annotations) {
|
|
88
|
+
return { input, annotations: item.annotations };
|
|
89
|
+
}
|
|
90
|
+
const annotations = config.annotations.parse(item.annotations);
|
|
91
|
+
if (policy !== "ignore" && isObjectSchema(config.annotations)) {
|
|
92
|
+
for (const p of collectDroppedKeys(item.annotations, annotations)) droppedAcc.add(p);
|
|
93
|
+
}
|
|
94
|
+
return { input, annotations };
|
|
95
|
+
}
|
|
96
|
+
function reportDropped(dropped) {
|
|
97
|
+
if (dropped.size === 0) return;
|
|
98
|
+
const keys = [...dropped].join(", ");
|
|
99
|
+
if (policy === "error") {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Dataset "${config.name}": annotation key(s) dropped by schema: ${keys}. These keys are not declared in the annotations schema, so they were stripped and will never reach scorers. Add them to the schema, or set onExtraAnnotationKeys: 'ignore' to allow stripping.`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
console.warn(
|
|
105
|
+
`[axl-eval] Dataset "${config.name}": annotation key(s) dropped by schema: ${keys}. These keys are not declared in the annotations schema, so they were stripped and will not reach scorers (a common cause of no-op scorers). Add them to the schema, set onExtraAnnotationKeys: 'error' to fail, or 'ignore' to silence this warning.`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
let lastDroppedAnnotationKeys = [];
|
|
50
109
|
return {
|
|
51
110
|
name: config.name,
|
|
52
111
|
schema: config.schema,
|
|
53
112
|
annotationsSchema: config.annotations,
|
|
113
|
+
get droppedAnnotationKeys() {
|
|
114
|
+
return [...lastDroppedAnnotationKeys];
|
|
115
|
+
},
|
|
54
116
|
async getItems() {
|
|
55
117
|
if (config.items && config.file) {
|
|
56
118
|
throw new Error('Dataset config error: "items" and "file" are mutually exclusive');
|
|
@@ -58,19 +120,19 @@ function dataset(config) {
|
|
|
58
120
|
if (!config.items && !config.file) {
|
|
59
121
|
throw new Error('Dataset config error: either "items" or "file" must be provided');
|
|
60
122
|
}
|
|
123
|
+
let rawItems;
|
|
61
124
|
if (config.items) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
125
|
+
rawItems = config.items;
|
|
126
|
+
} else {
|
|
127
|
+
const resolvedPath = path.resolve(config.basePath ?? process.cwd(), config.file);
|
|
128
|
+
const content = await (0, import_promises.readFile)(resolvedPath, "utf-8");
|
|
129
|
+
rawItems = JSON.parse(content);
|
|
66
130
|
}
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
annotations: item.annotations && config.annotations ? config.annotations.parse(item.annotations) : item.annotations
|
|
73
|
-
}));
|
|
131
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
132
|
+
const parsed = rawItems.map((item) => parseItem(item, dropped));
|
|
133
|
+
lastDroppedAnnotationKeys = [...dropped];
|
|
134
|
+
reportDropped(dropped);
|
|
135
|
+
return parsed;
|
|
74
136
|
}
|
|
75
137
|
};
|
|
76
138
|
}
|
|
@@ -79,6 +141,15 @@ function dataset(config) {
|
|
|
79
141
|
function normalizeScorerResult(result) {
|
|
80
142
|
return typeof result === "number" ? { score: result } : result;
|
|
81
143
|
}
|
|
144
|
+
function attachScorerErrorCost(err, cost) {
|
|
145
|
+
if (err && typeof err === "object" && typeof cost === "number" && Number.isFinite(cost) && cost >= 0) {
|
|
146
|
+
err.cost = cost;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function extractScorerErrorCost(err) {
|
|
150
|
+
const v = err && typeof err === "object" ? err.cost : void 0;
|
|
151
|
+
return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : void 0;
|
|
152
|
+
}
|
|
82
153
|
function scorer(config) {
|
|
83
154
|
return {
|
|
84
155
|
name: config.name,
|
|
@@ -144,16 +215,14 @@ ${JSON.stringify(annotations, null, 2)}` : "",
|
|
|
144
215
|
const parsed = JSON.parse((0, import_axl.extractJson)(response.content));
|
|
145
216
|
validated = schema.parse(parsed);
|
|
146
217
|
} catch (err) {
|
|
147
|
-
|
|
148
|
-
err.cost = responseCost;
|
|
149
|
-
}
|
|
218
|
+
attachScorerErrorCost(err, responseCost);
|
|
150
219
|
if (err && typeof err === "object" && "issues" in err && Array.isArray(err.issues)) {
|
|
151
220
|
const issues = err.issues;
|
|
152
221
|
const messages = issues.map((i) => `${i.path.length ? `${i.path.join(".")}: ` : ""}${i.message}`).join("; ");
|
|
153
222
|
const error = new Error(
|
|
154
223
|
`LLM scorer "${config.name}" returned an invalid response: ${messages}`
|
|
155
224
|
);
|
|
156
|
-
error
|
|
225
|
+
attachScorerErrorCost(error, responseCost);
|
|
157
226
|
throw error;
|
|
158
227
|
}
|
|
159
228
|
throw err;
|
|
@@ -187,6 +256,92 @@ function computeStats(scores) {
|
|
|
187
256
|
function round(n) {
|
|
188
257
|
return Math.round(n * 1e3) / 1e3;
|
|
189
258
|
}
|
|
259
|
+
function scorerCounts(items, name) {
|
|
260
|
+
let scored = 0;
|
|
261
|
+
let failed = 0;
|
|
262
|
+
for (const i of items) {
|
|
263
|
+
if (i.error) continue;
|
|
264
|
+
if (i.scores[name] != null) scored++;
|
|
265
|
+
else if (i.scoreDetails?.[name]?.duration != null) failed++;
|
|
266
|
+
}
|
|
267
|
+
return { scored, failed };
|
|
268
|
+
}
|
|
269
|
+
function evaluateScorerTolerance(scored, failed, type, limit) {
|
|
270
|
+
const attempted = scored + failed;
|
|
271
|
+
const rate = attempted === 0 ? 0 : failed / attempted;
|
|
272
|
+
const exceeds = attempted > 0 && (type === "deterministic" ? failed > 0 : rate > limit);
|
|
273
|
+
return { attempted, rate, exceeds, zeroSample: attempted === 0 };
|
|
274
|
+
}
|
|
275
|
+
async function mapWithConcurrency(items, concurrency, task) {
|
|
276
|
+
const results = new Array(items.length);
|
|
277
|
+
const requested = Number.isFinite(concurrency) ? Math.floor(concurrency) : 1;
|
|
278
|
+
const workerCount = items.length === 0 ? 0 : Math.min(Math.max(1, requested), items.length);
|
|
279
|
+
let next = 0;
|
|
280
|
+
async function runNext() {
|
|
281
|
+
while (next < items.length) {
|
|
282
|
+
const i = next++;
|
|
283
|
+
results[i] = await task(items[i], i);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
await Promise.all(Array.from({ length: workerCount }, () => runNext()));
|
|
287
|
+
return results;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/score-item.ts
|
|
291
|
+
async function scoreItem(item, scorers, scorerConcurrency, scorerContext, signal) {
|
|
292
|
+
const scoreDetails = item.scoreDetails ??= {};
|
|
293
|
+
for (const s of scorers) {
|
|
294
|
+
item.scores[s.name] = null;
|
|
295
|
+
scoreDetails[s.name] = { score: null };
|
|
296
|
+
}
|
|
297
|
+
let itemScorerCost = 0;
|
|
298
|
+
const scorerErrorsByName = {};
|
|
299
|
+
await mapWithConcurrency(scorers, scorerConcurrency, async (scorer2) => {
|
|
300
|
+
if (signal?.aborted) return;
|
|
301
|
+
const scorerStart = Date.now();
|
|
302
|
+
try {
|
|
303
|
+
const raw = await scorer2.score(item.output, item.input, item.annotations, scorerContext);
|
|
304
|
+
const scorerResult = normalizeScorerResult(raw);
|
|
305
|
+
if (scorerResult.cost != null) {
|
|
306
|
+
itemScorerCost += scorerResult.cost;
|
|
307
|
+
}
|
|
308
|
+
const scorerDuration = Date.now() - scorerStart;
|
|
309
|
+
if (!Number.isFinite(scorerResult.score) || scorerResult.score < 0 || scorerResult.score > 1) {
|
|
310
|
+
scorerErrorsByName[scorer2.name] = `Scorer "${scorer2.name}" returned out-of-range score ${scorerResult.score} for input ${JSON.stringify(item.input)}`;
|
|
311
|
+
item.scores[scorer2.name] = null;
|
|
312
|
+
scoreDetails[scorer2.name] = {
|
|
313
|
+
score: null,
|
|
314
|
+
metadata: scorerResult.metadata,
|
|
315
|
+
duration: scorerDuration,
|
|
316
|
+
cost: scorerResult.cost
|
|
317
|
+
};
|
|
318
|
+
} else {
|
|
319
|
+
item.scores[scorer2.name] = round(scorerResult.score);
|
|
320
|
+
scoreDetails[scorer2.name] = {
|
|
321
|
+
score: round(scorerResult.score),
|
|
322
|
+
metadata: scorerResult.metadata,
|
|
323
|
+
duration: scorerDuration,
|
|
324
|
+
cost: scorerResult.cost
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
} catch (err) {
|
|
328
|
+
if (err?.name === "AbortError") return;
|
|
329
|
+
const errCost = extractScorerErrorCost(err);
|
|
330
|
+
if (errCost != null) itemScorerCost += errCost;
|
|
331
|
+
scorerErrorsByName[scorer2.name] = `Scorer "${scorer2.name}" threw: ${err instanceof Error ? err.message : String(err)}`;
|
|
332
|
+
item.scores[scorer2.name] = null;
|
|
333
|
+
scoreDetails[scorer2.name] = {
|
|
334
|
+
score: null,
|
|
335
|
+
duration: Date.now() - scorerStart,
|
|
336
|
+
cost: errCost
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
const orderedErrors = scorers.map((s) => scorerErrorsByName[s.name]).filter((e) => e != null);
|
|
341
|
+
if (orderedErrors.length > 0) item.scorerErrors = orderedErrors;
|
|
342
|
+
item.scorerCost = itemScorerCost > 0 ? itemScorerCost : void 0;
|
|
343
|
+
return itemScorerCost;
|
|
344
|
+
}
|
|
190
345
|
|
|
191
346
|
// src/runner.ts
|
|
192
347
|
var import_node_crypto = require("crypto");
|
|
@@ -228,7 +383,9 @@ async function runEval(config, executeWorkflow, runtime, options) {
|
|
|
228
383
|
const startTime = Date.now();
|
|
229
384
|
const id = (0, import_node_crypto.randomUUID)();
|
|
230
385
|
const items = await config.dataset.getItems();
|
|
386
|
+
const droppedAnnotationKeys = [...config.dataset.droppedAnnotationKeys ?? []];
|
|
231
387
|
const concurrency = config.concurrency ?? 5;
|
|
388
|
+
const scorerConcurrency = config.scorerConcurrency ?? 5;
|
|
232
389
|
const budgetLimit = config.budget ? parseCost(config.budget) : void 0;
|
|
233
390
|
const scorerContext = {
|
|
234
391
|
resolveProvider: (uri) => {
|
|
@@ -317,89 +474,53 @@ async function runEval(config, executeWorkflow, runtime, options) {
|
|
|
317
474
|
if (budgetLimit != null && totalCost > budgetLimit) {
|
|
318
475
|
budgetExceeded = true;
|
|
319
476
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
item.input,
|
|
329
|
-
item.annotations,
|
|
330
|
-
scorerContext
|
|
331
|
-
);
|
|
332
|
-
const scorerResult = normalizeScorerResult(raw);
|
|
333
|
-
if (scorerResult.cost != null) {
|
|
334
|
-
itemScorerCost += scorerResult.cost;
|
|
335
|
-
totalCost += scorerResult.cost;
|
|
336
|
-
}
|
|
337
|
-
const scorerDuration = Date.now() - scorerStart;
|
|
338
|
-
if (!Number.isFinite(scorerResult.score) || scorerResult.score < 0 || scorerResult.score > 1) {
|
|
339
|
-
if (!evalItem.scorerErrors) evalItem.scorerErrors = [];
|
|
340
|
-
evalItem.scorerErrors.push(
|
|
341
|
-
`Scorer "${scorer2.name}" returned out-of-range score ${scorerResult.score} for input ${JSON.stringify(item.input)}`
|
|
342
|
-
);
|
|
343
|
-
evalItem.scores[scorer2.name] = null;
|
|
344
|
-
evalItem.scoreDetails[scorer2.name] = {
|
|
345
|
-
score: null,
|
|
346
|
-
metadata: scorerResult.metadata,
|
|
347
|
-
duration: scorerDuration,
|
|
348
|
-
cost: scorerResult.cost
|
|
349
|
-
};
|
|
350
|
-
} else {
|
|
351
|
-
evalItem.scores[scorer2.name] = round(scorerResult.score);
|
|
352
|
-
evalItem.scoreDetails[scorer2.name] = {
|
|
353
|
-
score: round(scorerResult.score),
|
|
354
|
-
metadata: scorerResult.metadata,
|
|
355
|
-
duration: scorerDuration,
|
|
356
|
-
cost: scorerResult.cost
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
} catch (err) {
|
|
360
|
-
const errCost = typeof err?.cost === "number" ? err.cost : void 0;
|
|
361
|
-
if (errCost != null) {
|
|
362
|
-
itemScorerCost += errCost;
|
|
363
|
-
totalCost += errCost;
|
|
364
|
-
}
|
|
365
|
-
if (!evalItem.scorerErrors) evalItem.scorerErrors = [];
|
|
366
|
-
evalItem.scorerErrors.push(
|
|
367
|
-
`Scorer "${scorer2.name}" threw: ${err instanceof Error ? err.message : String(err)}`
|
|
368
|
-
);
|
|
369
|
-
evalItem.scores[scorer2.name] = null;
|
|
370
|
-
evalItem.scoreDetails[scorer2.name] = {
|
|
371
|
-
score: null,
|
|
372
|
-
duration: Date.now() - scorerStart,
|
|
373
|
-
cost: errCost
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
evalItem.scorerCost = itemScorerCost > 0 ? itemScorerCost : void 0;
|
|
477
|
+
const itemScorerCost = await scoreItem(
|
|
478
|
+
evalItem,
|
|
479
|
+
config.scorers,
|
|
480
|
+
scorerConcurrency,
|
|
481
|
+
scorerContext,
|
|
482
|
+
options?.signal
|
|
483
|
+
);
|
|
484
|
+
totalCost += itemScorerCost;
|
|
378
485
|
evalItems[itemIndex] = evalItem;
|
|
379
486
|
options?.onProgress?.({ type: "item_done", itemIndex, totalItems: items.length });
|
|
380
487
|
}
|
|
381
|
-
|
|
382
|
-
async function runNext() {
|
|
383
|
-
while (index < items.length) {
|
|
384
|
-
const currentIndex = index++;
|
|
385
|
-
await processItem(items[currentIndex], currentIndex);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => runNext());
|
|
389
|
-
await Promise.all(workers);
|
|
488
|
+
await mapWithConcurrency(items, concurrency, (item, i) => processItem(item, i));
|
|
390
489
|
const failures = evalItems.filter((i) => i.error).length;
|
|
391
490
|
const scorerNames = config.scorers.map((s) => s.name);
|
|
392
491
|
const scorerStats = {};
|
|
393
492
|
for (const name of scorerNames) {
|
|
394
493
|
const scores = evalItems.filter((i) => !i.error && i.scores[name] != null).map((i) => i.scores[name]);
|
|
395
|
-
|
|
494
|
+
const { scored, failed } = scorerCounts(evalItems, name);
|
|
495
|
+
scorerStats[name] = { ...computeStats(scores), scored, failed };
|
|
396
496
|
}
|
|
397
|
-
const durations = evalItems.filter((i) => !i.error && i.duration != null).map((i) => i.duration);
|
|
398
|
-
const timing = durations.length > 0 ? computeStats(durations) : void 0;
|
|
399
497
|
const scorerTypes = {};
|
|
400
498
|
for (const s of config.scorers) {
|
|
401
499
|
scorerTypes[s.name] = s.isLlm ? "llm" : "deterministic";
|
|
402
500
|
}
|
|
501
|
+
let degraded;
|
|
502
|
+
const rawLimit = config.failOnScorerErrorRate;
|
|
503
|
+
if (rawLimit != null) {
|
|
504
|
+
if (!Number.isFinite(rawLimit) || rawLimit < 0 || rawLimit > 1) {
|
|
505
|
+
console.warn(
|
|
506
|
+
`[axl-eval] Ignoring invalid failOnScorerErrorRate (${rawLimit}); expected a number in [0, 1].`
|
|
507
|
+
);
|
|
508
|
+
} else {
|
|
509
|
+
for (const name of scorerNames) {
|
|
510
|
+
const stats = scorerStats[name];
|
|
511
|
+
const scored = stats.scored ?? 0;
|
|
512
|
+
const failed = stats.failed ?? 0;
|
|
513
|
+
const type = scorerTypes[name] === "llm" ? "llm" : "deterministic";
|
|
514
|
+
const limit = type === "deterministic" ? 0 : rawLimit;
|
|
515
|
+
const verdict = evaluateScorerTolerance(scored, failed, type, limit);
|
|
516
|
+
if (verdict.exceeds) {
|
|
517
|
+
(degraded ??= []).push({ scorer: name, rate: verdict.rate, limit, type, scored, failed });
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const durations = evalItems.filter((i) => !i.error && i.duration != null).map((i) => i.duration);
|
|
523
|
+
const timing = durations.length > 0 ? computeStats(durations) : void 0;
|
|
403
524
|
const totalModelCalls = /* @__PURE__ */ new Map();
|
|
404
525
|
for (const item of evalItems) {
|
|
405
526
|
const itemCounts = item.metadata?.modelCallCounts;
|
|
@@ -458,13 +579,20 @@ async function runEval(config, executeWorkflow, runtime, options) {
|
|
|
458
579
|
...config.metadata,
|
|
459
580
|
scorerTypes,
|
|
460
581
|
...modelsMeta,
|
|
461
|
-
...workflowsMeta
|
|
582
|
+
...workflowsMeta,
|
|
583
|
+
...droppedAnnotationKeys.length > 0 ? { droppedAnnotationKeys } : {}
|
|
462
584
|
},
|
|
463
585
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
464
586
|
totalCost,
|
|
465
587
|
duration: Date.now() - startTime,
|
|
466
588
|
items: evalItems,
|
|
467
|
-
summary: {
|
|
589
|
+
summary: {
|
|
590
|
+
count: items.length,
|
|
591
|
+
failures,
|
|
592
|
+
scorers: scorerStats,
|
|
593
|
+
timing,
|
|
594
|
+
...degraded ? { degraded } : {}
|
|
595
|
+
}
|
|
468
596
|
};
|
|
469
597
|
}
|
|
470
598
|
|
|
@@ -535,9 +663,6 @@ function xorshift32(seed) {
|
|
|
535
663
|
}
|
|
536
664
|
|
|
537
665
|
// src/compare.ts
|
|
538
|
-
function round3(n) {
|
|
539
|
-
return Math.round(n * 1e3) / 1e3;
|
|
540
|
-
}
|
|
541
666
|
var DEFAULT_LLM_THRESHOLD = 0.05;
|
|
542
667
|
var DEFAULT_DETERMINISTIC_THRESHOLD = 0;
|
|
543
668
|
var LEGACY_THRESHOLD = 0.1;
|
|
@@ -606,16 +731,34 @@ function evalCompare(baseline, candidate, options) {
|
|
|
606
731
|
const bMeanRaw = baselineMeans[name];
|
|
607
732
|
const cMeanRaw = candidateMeans[name];
|
|
608
733
|
const deltaRaw = cMeanRaw - bMeanRaw;
|
|
609
|
-
const bMean =
|
|
610
|
-
const cMean =
|
|
611
|
-
const delta =
|
|
612
|
-
const deltaPercent = bMeanRaw > 0 ?
|
|
734
|
+
const bMean = round(bMeanRaw);
|
|
735
|
+
const cMean = round(cMeanRaw);
|
|
736
|
+
const delta = round(deltaRaw);
|
|
737
|
+
const deltaPercent = bMeanRaw > 0 ? round(deltaRaw / bMeanRaw * 100) : 0;
|
|
613
738
|
const threshold = resolveThreshold(name, options, baselineRef.metadata);
|
|
739
|
+
let baselineScored = 0;
|
|
740
|
+
let baselineFailed = 0;
|
|
741
|
+
for (const run of baselineRuns) {
|
|
742
|
+
const c = scorerCounts(run.items, name);
|
|
743
|
+
baselineScored += c.scored;
|
|
744
|
+
baselineFailed += c.failed;
|
|
745
|
+
}
|
|
746
|
+
let candidateScored = 0;
|
|
747
|
+
let candidateFailed = 0;
|
|
748
|
+
for (const run of candidateRuns) {
|
|
749
|
+
const c = scorerCounts(run.items, name);
|
|
750
|
+
candidateScored += c.scored;
|
|
751
|
+
candidateFailed += c.failed;
|
|
752
|
+
}
|
|
614
753
|
const entry = {
|
|
615
754
|
baselineMean: bMean,
|
|
616
755
|
candidateMean: cMean,
|
|
617
756
|
delta,
|
|
618
|
-
deltaPercent
|
|
757
|
+
deltaPercent,
|
|
758
|
+
baselineScored,
|
|
759
|
+
baselineFailed,
|
|
760
|
+
candidateScored,
|
|
761
|
+
candidateFailed
|
|
619
762
|
};
|
|
620
763
|
const diffs = pairedDiffs[name];
|
|
621
764
|
if (diffs.length >= 2) {
|
|
@@ -645,15 +788,15 @@ function evalCompare(baseline, candidate, options) {
|
|
|
645
788
|
if (bScores.length === 0 || cScores.length === 0) continue;
|
|
646
789
|
const bAvg = bScores.reduce((a, b) => a + b, 0) / bScores.length;
|
|
647
790
|
const cAvg = cScores.reduce((a, b) => a + b, 0) / cScores.length;
|
|
648
|
-
const delta =
|
|
791
|
+
const delta = round(cAvg - bAvg);
|
|
649
792
|
const threshold = resolveThreshold(name, options, baselineRef.metadata);
|
|
650
793
|
if (delta < -threshold)
|
|
651
794
|
regressions.push({
|
|
652
795
|
itemIndex: i,
|
|
653
796
|
input: baselineRef.items[i]?.input,
|
|
654
797
|
scorer: name,
|
|
655
|
-
baselineScore:
|
|
656
|
-
candidateScore:
|
|
798
|
+
baselineScore: round(bAvg),
|
|
799
|
+
candidateScore: round(cAvg),
|
|
657
800
|
delta
|
|
658
801
|
});
|
|
659
802
|
else if (delta > threshold)
|
|
@@ -661,8 +804,8 @@ function evalCompare(baseline, candidate, options) {
|
|
|
661
804
|
itemIndex: i,
|
|
662
805
|
input: baselineRef.items[i]?.input,
|
|
663
806
|
scorer: name,
|
|
664
|
-
baselineScore:
|
|
665
|
-
candidateScore:
|
|
807
|
+
baselineScore: round(bAvg),
|
|
808
|
+
candidateScore: round(cAvg),
|
|
666
809
|
delta
|
|
667
810
|
});
|
|
668
811
|
}
|
|
@@ -673,20 +816,20 @@ function evalCompare(baseline, candidate, options) {
|
|
|
673
816
|
if (baselineTimings.length > 0 && candidateTimings.length > 0) {
|
|
674
817
|
const bMean = baselineTimings.reduce((a, b) => a + b, 0) / baselineTimings.length;
|
|
675
818
|
const cMean = candidateTimings.reduce((a, b) => a + b, 0) / candidateTimings.length;
|
|
676
|
-
const delta =
|
|
677
|
-
const deltaPercent = bMean > 0 ?
|
|
678
|
-
timing = { baselineMean:
|
|
819
|
+
const delta = round(cMean - bMean);
|
|
820
|
+
const deltaPercent = bMean > 0 ? round((cMean - bMean) / bMean * 100) : 0;
|
|
821
|
+
timing = { baselineMean: round(bMean), candidateMean: round(cMean), delta, deltaPercent };
|
|
679
822
|
}
|
|
680
823
|
let cost;
|
|
681
824
|
const baselineAvgCost = baselineRuns.reduce((sum, r) => sum + r.totalCost, 0) / baselineRuns.length;
|
|
682
825
|
const candidateAvgCost = candidateRuns.reduce((sum, r) => sum + r.totalCost, 0) / candidateRuns.length;
|
|
683
826
|
if (baselineAvgCost > 0 || candidateAvgCost > 0) {
|
|
684
827
|
const deltaRaw = candidateAvgCost - baselineAvgCost;
|
|
685
|
-
const delta =
|
|
686
|
-
const deltaPercent = baselineAvgCost > 0 ?
|
|
828
|
+
const delta = round(deltaRaw);
|
|
829
|
+
const deltaPercent = baselineAvgCost > 0 ? round(deltaRaw / baselineAvgCost * 100) : 0;
|
|
687
830
|
cost = {
|
|
688
|
-
baselineTotal:
|
|
689
|
-
candidateTotal:
|
|
831
|
+
baselineTotal: round(baselineAvgCost),
|
|
832
|
+
candidateTotal: round(candidateAvgCost),
|
|
690
833
|
delta,
|
|
691
834
|
deltaPercent
|
|
692
835
|
};
|
|
@@ -751,12 +894,34 @@ function detectPartial(runs) {
|
|
|
751
894
|
if (runs.length >= attempted) return void 0;
|
|
752
895
|
return { completed: runs.length, attempted };
|
|
753
896
|
}
|
|
897
|
+
function evaluateScorerErrorRateGate(comparison, maxScorerErrorRate) {
|
|
898
|
+
const scorerTypes = comparison.baseline.metadata?.scorerTypes ?? {};
|
|
899
|
+
for (const name of Object.keys(comparison.scorers)) {
|
|
900
|
+
const s = comparison.scorers[name];
|
|
901
|
+
const type = scorerTypes[name] === "deterministic" ? "deterministic" : "llm";
|
|
902
|
+
const limit = type === "deterministic" ? 0 : maxScorerErrorRate;
|
|
903
|
+
for (const side of ["baseline", "candidate"]) {
|
|
904
|
+
const scored = (side === "baseline" ? s.baselineScored : s.candidateScored) ?? 0;
|
|
905
|
+
const failed = (side === "baseline" ? s.baselineFailed : s.candidateFailed) ?? 0;
|
|
906
|
+
const verdict = evaluateScorerTolerance(scored, failed, type, limit);
|
|
907
|
+
if (verdict.zeroSample) {
|
|
908
|
+
return `scorer "${name}" has no scored items on ${side} (its items errored or were skipped) \u2014 cannot certify a mean over an empty sample.`;
|
|
909
|
+
}
|
|
910
|
+
if (verdict.exceeds) {
|
|
911
|
+
const limitStr = type === "deterministic" ? "deterministic zero-tolerance" : `${(limit * 100).toFixed(1)}%`;
|
|
912
|
+
return `scorer "${name}" failed ${(verdict.rate * 100).toFixed(1)}% on ${side} (${failed}/${verdict.attempted}), over the ${limitStr} limit.`;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
754
918
|
|
|
755
919
|
// src/rescore.ts
|
|
756
920
|
var import_node_crypto2 = require("crypto");
|
|
757
921
|
async function rescore(result, scorers, runtime, options) {
|
|
758
922
|
const startTime = Date.now();
|
|
759
923
|
const concurrency = options?.concurrency ?? 5;
|
|
924
|
+
const scorerConcurrency = options?.scorerConcurrency ?? 5;
|
|
760
925
|
const scorerContext = {
|
|
761
926
|
resolveProvider: (uri) => {
|
|
762
927
|
if (typeof runtime.resolveProvider !== "function") {
|
|
@@ -800,77 +965,24 @@ async function rescore(result, scorers, runtime, options) {
|
|
|
800
965
|
scores: {},
|
|
801
966
|
scoreDetails: {}
|
|
802
967
|
};
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
totalCost += scorerResult.cost;
|
|
812
|
-
}
|
|
813
|
-
const scorerDuration = Date.now() - scorerStart;
|
|
814
|
-
if (!Number.isFinite(scorerResult.score) || scorerResult.score < 0 || scorerResult.score > 1) {
|
|
815
|
-
if (!item.scorerErrors) item.scorerErrors = [];
|
|
816
|
-
item.scorerErrors.push(
|
|
817
|
-
`Scorer "${scorer2.name}" returned out-of-range score ${scorerResult.score} for input ${JSON.stringify(item.input)}`
|
|
818
|
-
);
|
|
819
|
-
item.scores[scorer2.name] = null;
|
|
820
|
-
item.scoreDetails[scorer2.name] = {
|
|
821
|
-
score: null,
|
|
822
|
-
metadata: scorerResult.metadata,
|
|
823
|
-
duration: scorerDuration,
|
|
824
|
-
cost: scorerResult.cost
|
|
825
|
-
};
|
|
826
|
-
} else {
|
|
827
|
-
item.scores[scorer2.name] = round(scorerResult.score);
|
|
828
|
-
item.scoreDetails[scorer2.name] = {
|
|
829
|
-
score: round(scorerResult.score),
|
|
830
|
-
metadata: scorerResult.metadata,
|
|
831
|
-
duration: scorerDuration,
|
|
832
|
-
cost: scorerResult.cost
|
|
833
|
-
};
|
|
834
|
-
}
|
|
835
|
-
} catch (err) {
|
|
836
|
-
const errCost = typeof err?.cost === "number" ? err.cost : void 0;
|
|
837
|
-
if (errCost != null) {
|
|
838
|
-
itemScorerCost += errCost;
|
|
839
|
-
totalCost += errCost;
|
|
840
|
-
}
|
|
841
|
-
if (!item.scorerErrors) item.scorerErrors = [];
|
|
842
|
-
item.scorerErrors.push(
|
|
843
|
-
`Scorer "${scorer2.name}" threw: ${err instanceof Error ? err.message : String(err)}`
|
|
844
|
-
);
|
|
845
|
-
item.scores[scorer2.name] = null;
|
|
846
|
-
item.scoreDetails[scorer2.name] = {
|
|
847
|
-
score: null,
|
|
848
|
-
duration: Date.now() - scorerStart,
|
|
849
|
-
cost: errCost
|
|
850
|
-
};
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
item.scorerCost = itemScorerCost > 0 ? itemScorerCost : void 0;
|
|
968
|
+
const itemScorerCost = await scoreItem(
|
|
969
|
+
item,
|
|
970
|
+
scorers,
|
|
971
|
+
scorerConcurrency,
|
|
972
|
+
scorerContext,
|
|
973
|
+
options?.signal
|
|
974
|
+
);
|
|
975
|
+
totalCost += itemScorerCost;
|
|
854
976
|
rescored[itemIndex] = item;
|
|
855
977
|
}
|
|
856
|
-
|
|
857
|
-
async function runNext() {
|
|
858
|
-
while (index < result.items.length) {
|
|
859
|
-
const currentIndex = index++;
|
|
860
|
-
await rescoreItem(result.items[currentIndex], currentIndex);
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
const workers = Array.from(
|
|
864
|
-
{ length: Math.min(concurrency, result.items.length) },
|
|
865
|
-
() => runNext()
|
|
866
|
-
);
|
|
867
|
-
await Promise.all(workers);
|
|
978
|
+
await mapWithConcurrency(result.items, concurrency, (item, i) => rescoreItem(item, i));
|
|
868
979
|
const failures = rescored.filter((i) => i.error).length;
|
|
869
980
|
const scorerNames = scorers.map((s) => s.name);
|
|
870
981
|
const scorerStats = {};
|
|
871
982
|
for (const name of scorerNames) {
|
|
872
983
|
const scores = rescored.filter((i) => !i.error && i.scores[name] != null).map((i) => i.scores[name]);
|
|
873
|
-
|
|
984
|
+
const { scored, failed } = scorerCounts(rescored, name);
|
|
985
|
+
scorerStats[name] = { ...computeStats(scores), scored, failed };
|
|
874
986
|
}
|
|
875
987
|
const scorerTypes = {};
|
|
876
988
|
for (const s of scorers) {
|
|
@@ -880,7 +992,11 @@ async function rescore(result, scorers, runtime, options) {
|
|
|
880
992
|
id: (0, import_node_crypto2.randomUUID)(),
|
|
881
993
|
dataset: result.dataset,
|
|
882
994
|
metadata: (() => {
|
|
883
|
-
const
|
|
995
|
+
const rest = { ...result.metadata };
|
|
996
|
+
delete rest.runGroupId;
|
|
997
|
+
delete rest.runIndex;
|
|
998
|
+
delete rest.scorerFiltered;
|
|
999
|
+
delete rest.scorersRun;
|
|
884
1000
|
const merged = {
|
|
885
1001
|
...rest,
|
|
886
1002
|
rescored: true,
|
|
@@ -933,15 +1049,24 @@ function aggregateRuns(runs) {
|
|
|
933
1049
|
const scorerNames = Object.keys(runs[0].summary.scorers);
|
|
934
1050
|
const scorers = {};
|
|
935
1051
|
for (const name of scorerNames) {
|
|
936
|
-
const
|
|
1052
|
+
const contributing = runs.filter((r) => {
|
|
1053
|
+
const sc = r.summary.scorers[name]?.scored;
|
|
1054
|
+
return sc == null || sc > 0;
|
|
1055
|
+
});
|
|
1056
|
+
const meanSource = contributing.length > 0 ? contributing : runs;
|
|
1057
|
+
const means = meanSource.map((r) => r.summary.scorers[name]?.mean ?? 0);
|
|
937
1058
|
const meanOfMeans = means.reduce((a, b) => a + b, 0) / means.length;
|
|
938
1059
|
const minMean = Math.min(...means);
|
|
939
1060
|
const maxMean = Math.max(...means);
|
|
1061
|
+
const scored = runs.reduce((sum, r) => sum + (r.summary.scorers[name]?.scored ?? 0), 0);
|
|
1062
|
+
const failed = runs.reduce((sum, r) => sum + (r.summary.scorers[name]?.failed ?? 0), 0);
|
|
940
1063
|
scorers[name] = {
|
|
941
1064
|
mean: round(meanOfMeans),
|
|
942
1065
|
std: round(std(means, meanOfMeans)),
|
|
943
1066
|
min: round(minMean),
|
|
944
|
-
max: round(maxMean)
|
|
1067
|
+
max: round(maxMean),
|
|
1068
|
+
scored,
|
|
1069
|
+
failed
|
|
945
1070
|
};
|
|
946
1071
|
}
|
|
947
1072
|
let timing;
|
|
@@ -970,6 +1095,8 @@ function aggregateRuns(runs) {
|
|
|
970
1095
|
dataset,
|
|
971
1096
|
defineEval,
|
|
972
1097
|
evalCompare,
|
|
1098
|
+
evaluateScorerErrorRateGate,
|
|
1099
|
+
evaluateScorerTolerance,
|
|
973
1100
|
llmScorer,
|
|
974
1101
|
normalizeScorerResult,
|
|
975
1102
|
pairedBootstrapCI,
|