@axlsdk/eval 0.17.8 → 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 +74 -7
- package/dist/{chunk-NPKIP4SB.js → chunk-BWBX5YVQ.js} +245 -164
- package/dist/chunk-BWBX5YVQ.js.map +1 -0
- package/dist/cli.cjs +537 -214
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +305 -52
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +327 -177
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +199 -18
- package/dist/index.d.ts +199 -18
- package/dist/index.js +91 -17
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/dist/chunk-NPKIP4SB.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,
|
|
@@ -125,7 +196,18 @@ ${JSON.stringify(annotations, null, 2)}` : "",
|
|
|
125
196
|
{ role: "system", content: config.system },
|
|
126
197
|
{ role: "user", content: prompt }
|
|
127
198
|
],
|
|
128
|
-
{
|
|
199
|
+
{
|
|
200
|
+
model,
|
|
201
|
+
temperature: config.temperature ?? 0.2,
|
|
202
|
+
maxTokens: config.maxTokens,
|
|
203
|
+
effort: config.effort,
|
|
204
|
+
thinkingBudget: config.thinkingBudget,
|
|
205
|
+
includeThoughts: config.includeThoughts,
|
|
206
|
+
stop: config.stop,
|
|
207
|
+
providerOptions: config.providerOptions,
|
|
208
|
+
responseFormat: { type: "json_object" },
|
|
209
|
+
signal: context.signal
|
|
210
|
+
}
|
|
129
211
|
);
|
|
130
212
|
const responseCost = response.cost;
|
|
131
213
|
let validated;
|
|
@@ -133,16 +215,14 @@ ${JSON.stringify(annotations, null, 2)}` : "",
|
|
|
133
215
|
const parsed = JSON.parse((0, import_axl.extractJson)(response.content));
|
|
134
216
|
validated = schema.parse(parsed);
|
|
135
217
|
} catch (err) {
|
|
136
|
-
|
|
137
|
-
err.cost = responseCost;
|
|
138
|
-
}
|
|
218
|
+
attachScorerErrorCost(err, responseCost);
|
|
139
219
|
if (err && typeof err === "object" && "issues" in err && Array.isArray(err.issues)) {
|
|
140
220
|
const issues = err.issues;
|
|
141
221
|
const messages = issues.map((i) => `${i.path.length ? `${i.path.join(".")}: ` : ""}${i.message}`).join("; ");
|
|
142
222
|
const error = new Error(
|
|
143
223
|
`LLM scorer "${config.name}" returned an invalid response: ${messages}`
|
|
144
224
|
);
|
|
145
|
-
error
|
|
225
|
+
attachScorerErrorCost(error, responseCost);
|
|
146
226
|
throw error;
|
|
147
227
|
}
|
|
148
228
|
throw err;
|
|
@@ -176,6 +256,92 @@ function computeStats(scores) {
|
|
|
176
256
|
function round(n) {
|
|
177
257
|
return Math.round(n * 1e3) / 1e3;
|
|
178
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
|
+
}
|
|
179
345
|
|
|
180
346
|
// src/runner.ts
|
|
181
347
|
var import_node_crypto = require("crypto");
|
|
@@ -217,7 +383,9 @@ async function runEval(config, executeWorkflow, runtime, options) {
|
|
|
217
383
|
const startTime = Date.now();
|
|
218
384
|
const id = (0, import_node_crypto.randomUUID)();
|
|
219
385
|
const items = await config.dataset.getItems();
|
|
386
|
+
const droppedAnnotationKeys = [...config.dataset.droppedAnnotationKeys ?? []];
|
|
220
387
|
const concurrency = config.concurrency ?? 5;
|
|
388
|
+
const scorerConcurrency = config.scorerConcurrency ?? 5;
|
|
221
389
|
const budgetLimit = config.budget ? parseCost(config.budget) : void 0;
|
|
222
390
|
const scorerContext = {
|
|
223
391
|
resolveProvider: (uri) => {
|
|
@@ -227,7 +395,8 @@ async function runEval(config, executeWorkflow, runtime, options) {
|
|
|
227
395
|
);
|
|
228
396
|
}
|
|
229
397
|
return runtime.resolveProvider(uri);
|
|
230
|
-
}
|
|
398
|
+
},
|
|
399
|
+
signal: options?.signal
|
|
231
400
|
};
|
|
232
401
|
const evalItems = new Array(items.length);
|
|
233
402
|
let totalCost = 0;
|
|
@@ -305,89 +474,53 @@ async function runEval(config, executeWorkflow, runtime, options) {
|
|
|
305
474
|
if (budgetLimit != null && totalCost > budgetLimit) {
|
|
306
475
|
budgetExceeded = true;
|
|
307
476
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
item.input,
|
|
317
|
-
item.annotations,
|
|
318
|
-
scorerContext
|
|
319
|
-
);
|
|
320
|
-
const scorerResult = normalizeScorerResult(raw);
|
|
321
|
-
if (scorerResult.cost != null) {
|
|
322
|
-
itemScorerCost += scorerResult.cost;
|
|
323
|
-
totalCost += scorerResult.cost;
|
|
324
|
-
}
|
|
325
|
-
const scorerDuration = Date.now() - scorerStart;
|
|
326
|
-
if (!Number.isFinite(scorerResult.score) || scorerResult.score < 0 || scorerResult.score > 1) {
|
|
327
|
-
if (!evalItem.scorerErrors) evalItem.scorerErrors = [];
|
|
328
|
-
evalItem.scorerErrors.push(
|
|
329
|
-
`Scorer "${scorer2.name}" returned out-of-range score ${scorerResult.score} for input ${JSON.stringify(item.input)}`
|
|
330
|
-
);
|
|
331
|
-
evalItem.scores[scorer2.name] = null;
|
|
332
|
-
evalItem.scoreDetails[scorer2.name] = {
|
|
333
|
-
score: null,
|
|
334
|
-
metadata: scorerResult.metadata,
|
|
335
|
-
duration: scorerDuration,
|
|
336
|
-
cost: scorerResult.cost
|
|
337
|
-
};
|
|
338
|
-
} else {
|
|
339
|
-
evalItem.scores[scorer2.name] = round(scorerResult.score);
|
|
340
|
-
evalItem.scoreDetails[scorer2.name] = {
|
|
341
|
-
score: round(scorerResult.score),
|
|
342
|
-
metadata: scorerResult.metadata,
|
|
343
|
-
duration: scorerDuration,
|
|
344
|
-
cost: scorerResult.cost
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
} catch (err) {
|
|
348
|
-
const errCost = typeof err?.cost === "number" ? err.cost : void 0;
|
|
349
|
-
if (errCost != null) {
|
|
350
|
-
itemScorerCost += errCost;
|
|
351
|
-
totalCost += errCost;
|
|
352
|
-
}
|
|
353
|
-
if (!evalItem.scorerErrors) evalItem.scorerErrors = [];
|
|
354
|
-
evalItem.scorerErrors.push(
|
|
355
|
-
`Scorer "${scorer2.name}" threw: ${err instanceof Error ? err.message : String(err)}`
|
|
356
|
-
);
|
|
357
|
-
evalItem.scores[scorer2.name] = null;
|
|
358
|
-
evalItem.scoreDetails[scorer2.name] = {
|
|
359
|
-
score: null,
|
|
360
|
-
duration: Date.now() - scorerStart,
|
|
361
|
-
cost: errCost
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
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;
|
|
366
485
|
evalItems[itemIndex] = evalItem;
|
|
367
486
|
options?.onProgress?.({ type: "item_done", itemIndex, totalItems: items.length });
|
|
368
487
|
}
|
|
369
|
-
|
|
370
|
-
async function runNext() {
|
|
371
|
-
while (index < items.length) {
|
|
372
|
-
const currentIndex = index++;
|
|
373
|
-
await processItem(items[currentIndex], currentIndex);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => runNext());
|
|
377
|
-
await Promise.all(workers);
|
|
488
|
+
await mapWithConcurrency(items, concurrency, (item, i) => processItem(item, i));
|
|
378
489
|
const failures = evalItems.filter((i) => i.error).length;
|
|
379
490
|
const scorerNames = config.scorers.map((s) => s.name);
|
|
380
491
|
const scorerStats = {};
|
|
381
492
|
for (const name of scorerNames) {
|
|
382
493
|
const scores = evalItems.filter((i) => !i.error && i.scores[name] != null).map((i) => i.scores[name]);
|
|
383
|
-
|
|
494
|
+
const { scored, failed } = scorerCounts(evalItems, name);
|
|
495
|
+
scorerStats[name] = { ...computeStats(scores), scored, failed };
|
|
384
496
|
}
|
|
385
|
-
const durations = evalItems.filter((i) => !i.error && i.duration != null).map((i) => i.duration);
|
|
386
|
-
const timing = durations.length > 0 ? computeStats(durations) : void 0;
|
|
387
497
|
const scorerTypes = {};
|
|
388
498
|
for (const s of config.scorers) {
|
|
389
499
|
scorerTypes[s.name] = s.isLlm ? "llm" : "deterministic";
|
|
390
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;
|
|
391
524
|
const totalModelCalls = /* @__PURE__ */ new Map();
|
|
392
525
|
for (const item of evalItems) {
|
|
393
526
|
const itemCounts = item.metadata?.modelCallCounts;
|
|
@@ -446,13 +579,20 @@ async function runEval(config, executeWorkflow, runtime, options) {
|
|
|
446
579
|
...config.metadata,
|
|
447
580
|
scorerTypes,
|
|
448
581
|
...modelsMeta,
|
|
449
|
-
...workflowsMeta
|
|
582
|
+
...workflowsMeta,
|
|
583
|
+
...droppedAnnotationKeys.length > 0 ? { droppedAnnotationKeys } : {}
|
|
450
584
|
},
|
|
451
585
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
452
586
|
totalCost,
|
|
453
587
|
duration: Date.now() - startTime,
|
|
454
588
|
items: evalItems,
|
|
455
|
-
summary: {
|
|
589
|
+
summary: {
|
|
590
|
+
count: items.length,
|
|
591
|
+
failures,
|
|
592
|
+
scorers: scorerStats,
|
|
593
|
+
timing,
|
|
594
|
+
...degraded ? { degraded } : {}
|
|
595
|
+
}
|
|
456
596
|
};
|
|
457
597
|
}
|
|
458
598
|
|
|
@@ -523,9 +663,6 @@ function xorshift32(seed) {
|
|
|
523
663
|
}
|
|
524
664
|
|
|
525
665
|
// src/compare.ts
|
|
526
|
-
function round3(n) {
|
|
527
|
-
return Math.round(n * 1e3) / 1e3;
|
|
528
|
-
}
|
|
529
666
|
var DEFAULT_LLM_THRESHOLD = 0.05;
|
|
530
667
|
var DEFAULT_DETERMINISTIC_THRESHOLD = 0;
|
|
531
668
|
var LEGACY_THRESHOLD = 0.1;
|
|
@@ -594,16 +731,34 @@ function evalCompare(baseline, candidate, options) {
|
|
|
594
731
|
const bMeanRaw = baselineMeans[name];
|
|
595
732
|
const cMeanRaw = candidateMeans[name];
|
|
596
733
|
const deltaRaw = cMeanRaw - bMeanRaw;
|
|
597
|
-
const bMean =
|
|
598
|
-
const cMean =
|
|
599
|
-
const delta =
|
|
600
|
-
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;
|
|
601
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
|
+
}
|
|
602
753
|
const entry = {
|
|
603
754
|
baselineMean: bMean,
|
|
604
755
|
candidateMean: cMean,
|
|
605
756
|
delta,
|
|
606
|
-
deltaPercent
|
|
757
|
+
deltaPercent,
|
|
758
|
+
baselineScored,
|
|
759
|
+
baselineFailed,
|
|
760
|
+
candidateScored,
|
|
761
|
+
candidateFailed
|
|
607
762
|
};
|
|
608
763
|
const diffs = pairedDiffs[name];
|
|
609
764
|
if (diffs.length >= 2) {
|
|
@@ -633,15 +788,15 @@ function evalCompare(baseline, candidate, options) {
|
|
|
633
788
|
if (bScores.length === 0 || cScores.length === 0) continue;
|
|
634
789
|
const bAvg = bScores.reduce((a, b) => a + b, 0) / bScores.length;
|
|
635
790
|
const cAvg = cScores.reduce((a, b) => a + b, 0) / cScores.length;
|
|
636
|
-
const delta =
|
|
791
|
+
const delta = round(cAvg - bAvg);
|
|
637
792
|
const threshold = resolveThreshold(name, options, baselineRef.metadata);
|
|
638
793
|
if (delta < -threshold)
|
|
639
794
|
regressions.push({
|
|
640
795
|
itemIndex: i,
|
|
641
796
|
input: baselineRef.items[i]?.input,
|
|
642
797
|
scorer: name,
|
|
643
|
-
baselineScore:
|
|
644
|
-
candidateScore:
|
|
798
|
+
baselineScore: round(bAvg),
|
|
799
|
+
candidateScore: round(cAvg),
|
|
645
800
|
delta
|
|
646
801
|
});
|
|
647
802
|
else if (delta > threshold)
|
|
@@ -649,8 +804,8 @@ function evalCompare(baseline, candidate, options) {
|
|
|
649
804
|
itemIndex: i,
|
|
650
805
|
input: baselineRef.items[i]?.input,
|
|
651
806
|
scorer: name,
|
|
652
|
-
baselineScore:
|
|
653
|
-
candidateScore:
|
|
807
|
+
baselineScore: round(bAvg),
|
|
808
|
+
candidateScore: round(cAvg),
|
|
654
809
|
delta
|
|
655
810
|
});
|
|
656
811
|
}
|
|
@@ -661,20 +816,20 @@ function evalCompare(baseline, candidate, options) {
|
|
|
661
816
|
if (baselineTimings.length > 0 && candidateTimings.length > 0) {
|
|
662
817
|
const bMean = baselineTimings.reduce((a, b) => a + b, 0) / baselineTimings.length;
|
|
663
818
|
const cMean = candidateTimings.reduce((a, b) => a + b, 0) / candidateTimings.length;
|
|
664
|
-
const delta =
|
|
665
|
-
const deltaPercent = bMean > 0 ?
|
|
666
|
-
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 };
|
|
667
822
|
}
|
|
668
823
|
let cost;
|
|
669
824
|
const baselineAvgCost = baselineRuns.reduce((sum, r) => sum + r.totalCost, 0) / baselineRuns.length;
|
|
670
825
|
const candidateAvgCost = candidateRuns.reduce((sum, r) => sum + r.totalCost, 0) / candidateRuns.length;
|
|
671
826
|
if (baselineAvgCost > 0 || candidateAvgCost > 0) {
|
|
672
827
|
const deltaRaw = candidateAvgCost - baselineAvgCost;
|
|
673
|
-
const delta =
|
|
674
|
-
const deltaPercent = baselineAvgCost > 0 ?
|
|
828
|
+
const delta = round(deltaRaw);
|
|
829
|
+
const deltaPercent = baselineAvgCost > 0 ? round(deltaRaw / baselineAvgCost * 100) : 0;
|
|
675
830
|
cost = {
|
|
676
|
-
baselineTotal:
|
|
677
|
-
candidateTotal:
|
|
831
|
+
baselineTotal: round(baselineAvgCost),
|
|
832
|
+
candidateTotal: round(candidateAvgCost),
|
|
678
833
|
delta,
|
|
679
834
|
deltaPercent
|
|
680
835
|
};
|
|
@@ -739,12 +894,34 @@ function detectPartial(runs) {
|
|
|
739
894
|
if (runs.length >= attempted) return void 0;
|
|
740
895
|
return { completed: runs.length, attempted };
|
|
741
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
|
+
}
|
|
742
918
|
|
|
743
919
|
// src/rescore.ts
|
|
744
920
|
var import_node_crypto2 = require("crypto");
|
|
745
921
|
async function rescore(result, scorers, runtime, options) {
|
|
746
922
|
const startTime = Date.now();
|
|
747
923
|
const concurrency = options?.concurrency ?? 5;
|
|
924
|
+
const scorerConcurrency = options?.scorerConcurrency ?? 5;
|
|
748
925
|
const scorerContext = {
|
|
749
926
|
resolveProvider: (uri) => {
|
|
750
927
|
if (typeof runtime.resolveProvider !== "function") {
|
|
@@ -753,11 +930,22 @@ async function rescore(result, scorers, runtime, options) {
|
|
|
753
930
|
);
|
|
754
931
|
}
|
|
755
932
|
return runtime.resolveProvider(uri);
|
|
756
|
-
}
|
|
933
|
+
},
|
|
934
|
+
signal: options?.signal
|
|
757
935
|
};
|
|
758
936
|
const rescored = new Array(result.items.length);
|
|
759
937
|
let totalCost = 0;
|
|
760
938
|
async function rescoreItem(original, itemIndex) {
|
|
939
|
+
if (options?.signal?.aborted) {
|
|
940
|
+
rescored[itemIndex] = {
|
|
941
|
+
input: original.input,
|
|
942
|
+
annotations: original.annotations,
|
|
943
|
+
output: original.output,
|
|
944
|
+
error: "cancelled",
|
|
945
|
+
scores: {}
|
|
946
|
+
};
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
761
949
|
if (original.error) {
|
|
762
950
|
rescored[itemIndex] = {
|
|
763
951
|
input: original.input,
|
|
@@ -777,77 +965,24 @@ async function rescore(result, scorers, runtime, options) {
|
|
|
777
965
|
scores: {},
|
|
778
966
|
scoreDetails: {}
|
|
779
967
|
};
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
totalCost += scorerResult.cost;
|
|
789
|
-
}
|
|
790
|
-
const scorerDuration = Date.now() - scorerStart;
|
|
791
|
-
if (!Number.isFinite(scorerResult.score) || scorerResult.score < 0 || scorerResult.score > 1) {
|
|
792
|
-
if (!item.scorerErrors) item.scorerErrors = [];
|
|
793
|
-
item.scorerErrors.push(
|
|
794
|
-
`Scorer "${scorer2.name}" returned out-of-range score ${scorerResult.score} for input ${JSON.stringify(item.input)}`
|
|
795
|
-
);
|
|
796
|
-
item.scores[scorer2.name] = null;
|
|
797
|
-
item.scoreDetails[scorer2.name] = {
|
|
798
|
-
score: null,
|
|
799
|
-
metadata: scorerResult.metadata,
|
|
800
|
-
duration: scorerDuration,
|
|
801
|
-
cost: scorerResult.cost
|
|
802
|
-
};
|
|
803
|
-
} else {
|
|
804
|
-
item.scores[scorer2.name] = round(scorerResult.score);
|
|
805
|
-
item.scoreDetails[scorer2.name] = {
|
|
806
|
-
score: round(scorerResult.score),
|
|
807
|
-
metadata: scorerResult.metadata,
|
|
808
|
-
duration: scorerDuration,
|
|
809
|
-
cost: scorerResult.cost
|
|
810
|
-
};
|
|
811
|
-
}
|
|
812
|
-
} catch (err) {
|
|
813
|
-
const errCost = typeof err?.cost === "number" ? err.cost : void 0;
|
|
814
|
-
if (errCost != null) {
|
|
815
|
-
itemScorerCost += errCost;
|
|
816
|
-
totalCost += errCost;
|
|
817
|
-
}
|
|
818
|
-
if (!item.scorerErrors) item.scorerErrors = [];
|
|
819
|
-
item.scorerErrors.push(
|
|
820
|
-
`Scorer "${scorer2.name}" threw: ${err instanceof Error ? err.message : String(err)}`
|
|
821
|
-
);
|
|
822
|
-
item.scores[scorer2.name] = null;
|
|
823
|
-
item.scoreDetails[scorer2.name] = {
|
|
824
|
-
score: null,
|
|
825
|
-
duration: Date.now() - scorerStart,
|
|
826
|
-
cost: errCost
|
|
827
|
-
};
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
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;
|
|
831
976
|
rescored[itemIndex] = item;
|
|
832
977
|
}
|
|
833
|
-
|
|
834
|
-
async function runNext() {
|
|
835
|
-
while (index < result.items.length) {
|
|
836
|
-
const currentIndex = index++;
|
|
837
|
-
await rescoreItem(result.items[currentIndex], currentIndex);
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
const workers = Array.from(
|
|
841
|
-
{ length: Math.min(concurrency, result.items.length) },
|
|
842
|
-
() => runNext()
|
|
843
|
-
);
|
|
844
|
-
await Promise.all(workers);
|
|
978
|
+
await mapWithConcurrency(result.items, concurrency, (item, i) => rescoreItem(item, i));
|
|
845
979
|
const failures = rescored.filter((i) => i.error).length;
|
|
846
980
|
const scorerNames = scorers.map((s) => s.name);
|
|
847
981
|
const scorerStats = {};
|
|
848
982
|
for (const name of scorerNames) {
|
|
849
983
|
const scores = rescored.filter((i) => !i.error && i.scores[name] != null).map((i) => i.scores[name]);
|
|
850
|
-
|
|
984
|
+
const { scored, failed } = scorerCounts(rescored, name);
|
|
985
|
+
scorerStats[name] = { ...computeStats(scores), scored, failed };
|
|
851
986
|
}
|
|
852
987
|
const scorerTypes = {};
|
|
853
988
|
for (const s of scorers) {
|
|
@@ -857,7 +992,11 @@ async function rescore(result, scorers, runtime, options) {
|
|
|
857
992
|
id: (0, import_node_crypto2.randomUUID)(),
|
|
858
993
|
dataset: result.dataset,
|
|
859
994
|
metadata: (() => {
|
|
860
|
-
const
|
|
995
|
+
const rest = { ...result.metadata };
|
|
996
|
+
delete rest.runGroupId;
|
|
997
|
+
delete rest.runIndex;
|
|
998
|
+
delete rest.scorerFiltered;
|
|
999
|
+
delete rest.scorersRun;
|
|
861
1000
|
const merged = {
|
|
862
1001
|
...rest,
|
|
863
1002
|
rescored: true,
|
|
@@ -910,15 +1049,24 @@ function aggregateRuns(runs) {
|
|
|
910
1049
|
const scorerNames = Object.keys(runs[0].summary.scorers);
|
|
911
1050
|
const scorers = {};
|
|
912
1051
|
for (const name of scorerNames) {
|
|
913
|
-
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);
|
|
914
1058
|
const meanOfMeans = means.reduce((a, b) => a + b, 0) / means.length;
|
|
915
1059
|
const minMean = Math.min(...means);
|
|
916
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);
|
|
917
1063
|
scorers[name] = {
|
|
918
1064
|
mean: round(meanOfMeans),
|
|
919
1065
|
std: round(std(means, meanOfMeans)),
|
|
920
1066
|
min: round(minMean),
|
|
921
|
-
max: round(maxMean)
|
|
1067
|
+
max: round(maxMean),
|
|
1068
|
+
scored,
|
|
1069
|
+
failed
|
|
922
1070
|
};
|
|
923
1071
|
}
|
|
924
1072
|
let timing;
|
|
@@ -947,6 +1095,8 @@ function aggregateRuns(runs) {
|
|
|
947
1095
|
dataset,
|
|
948
1096
|
defineEval,
|
|
949
1097
|
evalCompare,
|
|
1098
|
+
evaluateScorerErrorRateGate,
|
|
1099
|
+
evaluateScorerTolerance,
|
|
950
1100
|
llmScorer,
|
|
951
1101
|
normalizeScorerResult,
|
|
952
1102
|
pairedBootstrapCI,
|