@axlsdk/eval 0.23.3 → 0.24.1
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 +110 -9
- package/dist/chunk-HKEAPM4B.js +1799 -0
- package/dist/chunk-HKEAPM4B.js.map +1 -0
- package/dist/cli.cjs +1518 -537
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +277 -57
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1010 -168
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +546 -10
- package/dist/index.d.ts +546 -10
- package/dist/index.js +29 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-AFUNZE7W.js +0 -966
- package/dist/chunk-AFUNZE7W.js.map +0 -1
|
@@ -0,0 +1,1799 @@
|
|
|
1
|
+
// src/bootstrap.ts
|
|
2
|
+
function pairedBootstrapCI(differences, options) {
|
|
3
|
+
const n = differences.length;
|
|
4
|
+
if (n === 0) return { lower: 0, upper: 0, mean: 0, pRegression: 0, pImprovement: 0 };
|
|
5
|
+
const nResamples = options?.nResamples ?? 1e3;
|
|
6
|
+
const alpha = options?.alpha ?? 0.05;
|
|
7
|
+
const mean = differences.reduce((a, b) => a + b, 0) / n;
|
|
8
|
+
if (n === 1)
|
|
9
|
+
return {
|
|
10
|
+
lower: differences[0],
|
|
11
|
+
upper: differences[0],
|
|
12
|
+
mean,
|
|
13
|
+
pRegression: mean < 0 ? 1 : 0,
|
|
14
|
+
pImprovement: mean > 0 ? 1 : 0
|
|
15
|
+
};
|
|
16
|
+
if (nResamples <= 0)
|
|
17
|
+
return {
|
|
18
|
+
lower: round(mean),
|
|
19
|
+
upper: round(mean),
|
|
20
|
+
mean: round(mean),
|
|
21
|
+
pRegression: mean < 0 ? 1 : 0,
|
|
22
|
+
pImprovement: mean > 0 ? 1 : 0
|
|
23
|
+
};
|
|
24
|
+
const rng = options?.seed != null ? xorshift32(options.seed) : () => Math.random();
|
|
25
|
+
const resampleMeans = new Array(nResamples);
|
|
26
|
+
for (let r = 0; r < nResamples; r++) {
|
|
27
|
+
let sum = 0;
|
|
28
|
+
for (let i = 0; i < n; i++) {
|
|
29
|
+
sum += differences[Math.floor(rng() * n)];
|
|
30
|
+
}
|
|
31
|
+
resampleMeans[r] = sum / n;
|
|
32
|
+
}
|
|
33
|
+
resampleMeans.sort((a, b) => a - b);
|
|
34
|
+
const lowerIdx = Math.floor(alpha / 2 * nResamples);
|
|
35
|
+
const upperIdx = Math.floor((1 - alpha / 2) * nResamples) - 1;
|
|
36
|
+
let belowZero = 0;
|
|
37
|
+
for (let i = 0; i < nResamples; i++) {
|
|
38
|
+
if (resampleMeans[i] < 0) belowZero++;
|
|
39
|
+
else break;
|
|
40
|
+
}
|
|
41
|
+
let aboveZero = 0;
|
|
42
|
+
for (let i = nResamples - 1; i >= 0; i--) {
|
|
43
|
+
if (resampleMeans[i] > 0) aboveZero++;
|
|
44
|
+
else break;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
lower: round(resampleMeans[Math.max(0, lowerIdx)]),
|
|
48
|
+
upper: round(resampleMeans[Math.min(nResamples - 1, upperIdx)]),
|
|
49
|
+
mean: round(mean),
|
|
50
|
+
pRegression: round(belowZero / nResamples),
|
|
51
|
+
pImprovement: round(aboveZero / nResamples)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function round(n) {
|
|
55
|
+
return Math.round(n * 1e4) / 1e4;
|
|
56
|
+
}
|
|
57
|
+
function xorshift32(seed) {
|
|
58
|
+
let state = seed | 0 || 1;
|
|
59
|
+
return () => {
|
|
60
|
+
state ^= state << 13;
|
|
61
|
+
state ^= state >> 17;
|
|
62
|
+
state ^= state << 5;
|
|
63
|
+
return (state >>> 0) / 4294967296;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/utils.ts
|
|
68
|
+
function computeStats(scores) {
|
|
69
|
+
if (scores.length === 0) return { mean: 0, min: 0, max: 0, p50: 0, p95: 0 };
|
|
70
|
+
const sorted = [...scores].sort((a, b) => a - b);
|
|
71
|
+
const mean = sorted.reduce((a, b) => a + b, 0) / sorted.length;
|
|
72
|
+
const min = sorted[0];
|
|
73
|
+
const max = sorted[sorted.length - 1];
|
|
74
|
+
const p50 = sorted[Math.floor(sorted.length * 0.5)];
|
|
75
|
+
const p95 = sorted[Math.floor(sorted.length * 0.95)];
|
|
76
|
+
return { mean: round2(mean), min: round2(min), max: round2(max), p50: round2(p50), p95: round2(p95) };
|
|
77
|
+
}
|
|
78
|
+
function round2(n) {
|
|
79
|
+
return Math.round(n * 1e3) / 1e3;
|
|
80
|
+
}
|
|
81
|
+
function formatPercent(rate) {
|
|
82
|
+
return `${Number((rate * 100).toFixed(1))}%`;
|
|
83
|
+
}
|
|
84
|
+
var DEFAULT_ITEM_ERROR_RATE_LIMIT = 0.05;
|
|
85
|
+
function isErrorRateLimit(value) {
|
|
86
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
87
|
+
}
|
|
88
|
+
function evaluateItemErrorRate(items, count, limit) {
|
|
89
|
+
const failed = items.failed;
|
|
90
|
+
const attempted = count - items.cancelled - items.budget_skipped - items.budget_interrupted;
|
|
91
|
+
const rate = attempted > 0 ? failed / attempted : 0;
|
|
92
|
+
return { failed, attempted, rate, limit, exceeded: attempted > 0 && rate > limit };
|
|
93
|
+
}
|
|
94
|
+
function scorerCounts(items, name) {
|
|
95
|
+
let scored = 0;
|
|
96
|
+
let failed = 0;
|
|
97
|
+
let skipped = 0;
|
|
98
|
+
for (const i of items) {
|
|
99
|
+
if (i.error) continue;
|
|
100
|
+
const detail = i.scoreDetails?.[name];
|
|
101
|
+
if (detail?.outcome !== void 0) {
|
|
102
|
+
if (detail.outcome === "scored") scored++;
|
|
103
|
+
else if (detail.outcome === "failed") failed++;
|
|
104
|
+
else if (detail.outcome === "skipped") skipped++;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (i.scores[name] != null) scored++;
|
|
108
|
+
else if (detail?.skipped === true) skipped++;
|
|
109
|
+
else if (detail?.duration != null) failed++;
|
|
110
|
+
}
|
|
111
|
+
return { scored, failed, skipped };
|
|
112
|
+
}
|
|
113
|
+
function evaluateScorerTolerance(scored, failed, type, limit) {
|
|
114
|
+
const attempted = scored + failed;
|
|
115
|
+
const rate = attempted === 0 ? 0 : failed / attempted;
|
|
116
|
+
const exceeds = attempted > 0 && (type === "deterministic" ? failed > 0 : rate > limit);
|
|
117
|
+
return { attempted, rate, exceeds, zeroSample: attempted === 0 };
|
|
118
|
+
}
|
|
119
|
+
async function mapWithConcurrency(items, concurrency, task) {
|
|
120
|
+
const results = new Array(items.length);
|
|
121
|
+
const requested = Number.isFinite(concurrency) ? Math.floor(concurrency) : 1;
|
|
122
|
+
const workerCount = items.length === 0 ? 0 : Math.min(Math.max(1, requested), items.length);
|
|
123
|
+
let next = 0;
|
|
124
|
+
async function runNext() {
|
|
125
|
+
while (next < items.length) {
|
|
126
|
+
const i = next++;
|
|
127
|
+
results[i] = await task(items[i], i);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
await Promise.all(Array.from({ length: workerCount }, () => runNext()));
|
|
131
|
+
return results;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/accounting.ts
|
|
135
|
+
import { AxlError, isAdmissionDeniedError } from "@axlsdk/axl";
|
|
136
|
+
var USAGE_KEYS = [
|
|
137
|
+
"inputTokens",
|
|
138
|
+
"outputTokens",
|
|
139
|
+
"reasoningTokens",
|
|
140
|
+
"cachedTokens",
|
|
141
|
+
"cacheWriteTokens",
|
|
142
|
+
"audioSeconds"
|
|
143
|
+
];
|
|
144
|
+
function zeroUsage() {
|
|
145
|
+
return {
|
|
146
|
+
inputTokens: 0,
|
|
147
|
+
outputTokens: 0,
|
|
148
|
+
reasoningTokens: 0,
|
|
149
|
+
cachedTokens: 0,
|
|
150
|
+
cacheWriteTokens: 0,
|
|
151
|
+
audioSeconds: 0
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function emptyAccounting() {
|
|
155
|
+
return {
|
|
156
|
+
version: 1,
|
|
157
|
+
currency: "USD",
|
|
158
|
+
knownCost: 0,
|
|
159
|
+
completeness: "complete",
|
|
160
|
+
reasons: {},
|
|
161
|
+
usage: zeroUsage(),
|
|
162
|
+
operations: { total: 0, settled: 0, unknown: 0, denied: 0, byKind: {} },
|
|
163
|
+
breakdown: { generation: 0, judging: 0, external: 0 },
|
|
164
|
+
provenance: {}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function uninstrumentedAccounting() {
|
|
168
|
+
return { ...emptyAccounting(), completeness: "incomplete", reasons: { uninstrumented: 1 } };
|
|
169
|
+
}
|
|
170
|
+
function isInstrumented(runtime) {
|
|
171
|
+
return typeof runtime?.trackOutcome === "function";
|
|
172
|
+
}
|
|
173
|
+
async function trackScope(runtime, fn, options) {
|
|
174
|
+
if (!isInstrumented(runtime)) {
|
|
175
|
+
try {
|
|
176
|
+
return {
|
|
177
|
+
status: "fulfilled",
|
|
178
|
+
value: await fn(),
|
|
179
|
+
accounting: uninstrumentedAccounting(),
|
|
180
|
+
instrumented: false
|
|
181
|
+
};
|
|
182
|
+
} catch (error) {
|
|
183
|
+
return {
|
|
184
|
+
status: "rejected",
|
|
185
|
+
error,
|
|
186
|
+
accounting: uninstrumentedAccounting(),
|
|
187
|
+
instrumented: false
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const outcome = await runtime.trackOutcome(fn, options);
|
|
192
|
+
return { ...outcome, instrumented: true };
|
|
193
|
+
}
|
|
194
|
+
function parseBudget(budget) {
|
|
195
|
+
const match = typeof budget === "string" ? budget.match(/^\$?(\d+(?:\.\d+)?)$/) : null;
|
|
196
|
+
if (!match) {
|
|
197
|
+
throw new AxlError(
|
|
198
|
+
"INVALID_BUDGET",
|
|
199
|
+
`Invalid budget "${String(budget)}": expected a non-negative USD amount like "$1" or "0.50".`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
const limit = Number(match[1]);
|
|
203
|
+
if (!Number.isFinite(limit)) {
|
|
204
|
+
throw new AxlError("INVALID_BUDGET", `Invalid budget "${budget}": not a finite amount.`);
|
|
205
|
+
}
|
|
206
|
+
return limit;
|
|
207
|
+
}
|
|
208
|
+
function usableCost(value) {
|
|
209
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
210
|
+
}
|
|
211
|
+
function isAdmissionDenied(err) {
|
|
212
|
+
return isAdmissionDeniedError(err);
|
|
213
|
+
}
|
|
214
|
+
function readAccounting(result) {
|
|
215
|
+
if (result?.accounting) return result.accounting;
|
|
216
|
+
const rescored = result?.metadata?.rescored === true;
|
|
217
|
+
return {
|
|
218
|
+
...emptyAccounting(),
|
|
219
|
+
knownCost: usableCost(result?.totalCost),
|
|
220
|
+
completeness: "unverified",
|
|
221
|
+
scope: rescored ? "rescore" : "run"
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function refusedWork(coverage) {
|
|
225
|
+
if (!coverage) return 0;
|
|
226
|
+
return refusedIn(coverage.items) + Object.values(coverage.scorers ?? {}).reduce((n, s) => n + refusedIn(s), 0);
|
|
227
|
+
}
|
|
228
|
+
function refusedIn(counts) {
|
|
229
|
+
return atLeastZero(counts?.budget_skipped) + atLeastZero(counts?.budget_interrupted);
|
|
230
|
+
}
|
|
231
|
+
function atLeastZero(value) {
|
|
232
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
233
|
+
}
|
|
234
|
+
function isBudgetStopped(summary) {
|
|
235
|
+
if (summary.budget?.status !== "closed") return false;
|
|
236
|
+
return refusedWork(summary.coverage) > 0;
|
|
237
|
+
}
|
|
238
|
+
function addUsage(target, source) {
|
|
239
|
+
if (!source) return;
|
|
240
|
+
for (const key of USAGE_KEYS) {
|
|
241
|
+
const value = source[key];
|
|
242
|
+
if (typeof value === "number" && Number.isFinite(value)) target[key] += value;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function addCounts(target, source) {
|
|
246
|
+
if (!source) return;
|
|
247
|
+
for (const [key, count] of Object.entries(source)) {
|
|
248
|
+
if (typeof count === "number" && Number.isFinite(count)) {
|
|
249
|
+
target[key] = (target[key] ?? 0) + count;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function aggregateAccounting(inputs) {
|
|
254
|
+
const out = emptyAccounting();
|
|
255
|
+
let anyUnverified = false;
|
|
256
|
+
let anyIncomplete = false;
|
|
257
|
+
for (const input of inputs) {
|
|
258
|
+
if (!input) continue;
|
|
259
|
+
if (input.completeness === "unverified") anyUnverified = true;
|
|
260
|
+
else if (input.completeness === "incomplete") anyIncomplete = true;
|
|
261
|
+
out.knownCost += usableCost(input.knownCost);
|
|
262
|
+
addUsage(out.usage, input.usage);
|
|
263
|
+
addCounts(out.reasons, input.reasons);
|
|
264
|
+
addCounts(out.operations.byKind, input.operations?.byKind);
|
|
265
|
+
addCounts(out.provenance, input.provenance);
|
|
266
|
+
out.operations.total += input.operations?.total ?? 0;
|
|
267
|
+
out.operations.settled += input.operations?.settled ?? 0;
|
|
268
|
+
out.operations.unknown += input.operations?.unknown ?? 0;
|
|
269
|
+
out.operations.denied += input.operations?.denied ?? 0;
|
|
270
|
+
out.breakdown.generation += usableCost(input.breakdown?.generation);
|
|
271
|
+
out.breakdown.judging += usableCost(input.breakdown?.judging);
|
|
272
|
+
out.breakdown.external += usableCost(input.breakdown?.external);
|
|
273
|
+
}
|
|
274
|
+
out.completeness = anyUnverified ? "unverified" : anyIncomplete ? "incomplete" : "complete";
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/scorer.ts
|
|
279
|
+
function normalizeScorerResult(result) {
|
|
280
|
+
return typeof result === "number" ? { score: result } : result;
|
|
281
|
+
}
|
|
282
|
+
function attachScorerErrorCost(err, cost) {
|
|
283
|
+
if (err && typeof err === "object" && typeof cost === "number" && Number.isFinite(cost) && cost >= 0) {
|
|
284
|
+
err.cost = cost;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function extractScorerErrorCost(err) {
|
|
288
|
+
const v = err && typeof err === "object" ? err.cost : void 0;
|
|
289
|
+
return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : void 0;
|
|
290
|
+
}
|
|
291
|
+
function scorer(config) {
|
|
292
|
+
return {
|
|
293
|
+
name: config.name,
|
|
294
|
+
description: config.description,
|
|
295
|
+
isLlm: false,
|
|
296
|
+
score: config.score,
|
|
297
|
+
applies: config.applies
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/diagnostics.ts
|
|
302
|
+
var DEFAULT_COPY_MAX_BYTES = 16 * 1024 * 1024;
|
|
303
|
+
function resolveCaptureLimits(option) {
|
|
304
|
+
if (option === void 0 || option === false) return void 0;
|
|
305
|
+
if (option === true) return {};
|
|
306
|
+
return {
|
|
307
|
+
...option.maxRecordBytes !== void 0 ? { maxRecordBytes: option.maxRecordBytes } : {},
|
|
308
|
+
...option.maxRunBytes !== void 0 ? { maxRunBytes: option.maxRunBytes } : {},
|
|
309
|
+
...option.maxQueueBytes !== void 0 ? { maxQueueBytes: option.maxQueueBytes } : {}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function toDiagnosticManifest(manifest) {
|
|
313
|
+
return {
|
|
314
|
+
version: 1,
|
|
315
|
+
artifactId: manifest.artifactId,
|
|
316
|
+
fidelity: "runtime_request",
|
|
317
|
+
status: manifest.status,
|
|
318
|
+
...manifest.reason !== void 0 ? { reason: manifest.reason } : {},
|
|
319
|
+
records: manifest.records,
|
|
320
|
+
bytes: manifest.bytes,
|
|
321
|
+
redaction: manifest.redaction,
|
|
322
|
+
...manifest.expiresAt !== void 0 ? { expiresAt: manifest.expiresAt } : {}
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function unavailableManifest(artifactId, reason) {
|
|
326
|
+
return {
|
|
327
|
+
version: 1,
|
|
328
|
+
artifactId,
|
|
329
|
+
fidelity: "runtime_request",
|
|
330
|
+
status: "unavailable",
|
|
331
|
+
reason,
|
|
332
|
+
records: 0,
|
|
333
|
+
bytes: 0,
|
|
334
|
+
redaction: "none"
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
var DEFAULT_SIDECAR_MAX_BYTES = 16 * 1024 * 1024;
|
|
338
|
+
var DEFAULT_SIDECAR_MAX_RECORD_BYTES = 4 * 1024 * 1024;
|
|
339
|
+
function validateRequestSidecar(text, options) {
|
|
340
|
+
if (typeof text !== "string") {
|
|
341
|
+
return { ok: false, reason: "requests sidecar must be a JSONL string" };
|
|
342
|
+
}
|
|
343
|
+
const maxBytes = options?.maxBytes ?? DEFAULT_SIDECAR_MAX_BYTES;
|
|
344
|
+
const maxRecordBytes = options?.maxRecordBytes ?? DEFAULT_SIDECAR_MAX_RECORD_BYTES;
|
|
345
|
+
const bytes = Buffer.byteLength(text, "utf-8");
|
|
346
|
+
if (bytes > maxBytes) {
|
|
347
|
+
return { ok: false, reason: `requests sidecar exceeds ${maxBytes} bytes` };
|
|
348
|
+
}
|
|
349
|
+
const lines = text.split("\n").filter((line) => line.trim() !== "");
|
|
350
|
+
for (let i = 0; i < lines.length; i++) {
|
|
351
|
+
const lineBytes = Buffer.byteLength(lines[i], "utf-8");
|
|
352
|
+
if (lineBytes > maxRecordBytes) {
|
|
353
|
+
return {
|
|
354
|
+
ok: false,
|
|
355
|
+
reason: `requests sidecar line ${i + 1} exceeds ${maxRecordBytes} bytes`
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
let parsed;
|
|
359
|
+
try {
|
|
360
|
+
parsed = JSON.parse(lines[i]);
|
|
361
|
+
} catch {
|
|
362
|
+
return { ok: false, reason: `requests sidecar line ${i + 1} is not valid JSON` };
|
|
363
|
+
}
|
|
364
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
365
|
+
return { ok: false, reason: `requests sidecar line ${i + 1} is not an object` };
|
|
366
|
+
}
|
|
367
|
+
const record = parsed;
|
|
368
|
+
if (record.v !== 1) {
|
|
369
|
+
return {
|
|
370
|
+
ok: false,
|
|
371
|
+
reason: `requests sidecar line ${i + 1} declares unsupported codec version ${String(record.v)}`
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
if (typeof record.operationId !== "string" || record.operationId === "") {
|
|
375
|
+
return { ok: false, reason: `requests sidecar line ${i + 1} has no operationId` };
|
|
376
|
+
}
|
|
377
|
+
if (record.phase !== "start" && record.phase !== "attempt" && record.phase !== "end") {
|
|
378
|
+
return { ok: false, reason: `requests sidecar line ${i + 1} has an unknown phase` };
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return { ok: true, lines, bytes };
|
|
382
|
+
}
|
|
383
|
+
function parseRequestRecords(lines) {
|
|
384
|
+
return lines.map((line) => JSON.parse(line));
|
|
385
|
+
}
|
|
386
|
+
function serializeRequestRecords(records) {
|
|
387
|
+
return records.map((record) => JSON.stringify(record)).join("\n") + (records.length ? "\n" : "");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// src/runner.ts
|
|
391
|
+
import { AdmissionController, AxlError as AxlError2, ProviderError, RequestCaptureChannel } from "@axlsdk/axl";
|
|
392
|
+
|
|
393
|
+
// src/score-item.ts
|
|
394
|
+
function describeInput(input) {
|
|
395
|
+
let rendered;
|
|
396
|
+
try {
|
|
397
|
+
rendered = JSON.stringify(input) ?? String(input);
|
|
398
|
+
} catch {
|
|
399
|
+
rendered = "[unserializable input]";
|
|
400
|
+
}
|
|
401
|
+
return rendered.length > 200 ? `${rendered.slice(0, 200)}\u2026` : rendered;
|
|
402
|
+
}
|
|
403
|
+
async function scoreItem(item, scorers, context) {
|
|
404
|
+
const scoreDetails = item.scoreDetails ??= {};
|
|
405
|
+
for (const s of scorers) {
|
|
406
|
+
item.scores[s.name] = null;
|
|
407
|
+
scoreDetails[s.name] = { score: null, outcome: "cancelled" };
|
|
408
|
+
}
|
|
409
|
+
const { runtime, scorerContext, signal, admission } = context;
|
|
410
|
+
const scorerErrorsByName = {};
|
|
411
|
+
await mapWithConcurrency(scorers, context.scorerConcurrency, async (scorer2) => {
|
|
412
|
+
const record = (detail, outcome2) => {
|
|
413
|
+
scoreDetails[scorer2.name] = { ...detail, outcome: outcome2 };
|
|
414
|
+
};
|
|
415
|
+
if (signal?.aborted) {
|
|
416
|
+
record({ score: null }, "cancelled");
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (admission?.closed && scorer2.isLlm) {
|
|
420
|
+
record({ score: null }, "budget_skipped");
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const scorerStart = Date.now();
|
|
424
|
+
let applicable = true;
|
|
425
|
+
try {
|
|
426
|
+
applicable = !scorer2.applies || scorer2.applies(item.output, item.input, item.annotations);
|
|
427
|
+
} catch (err) {
|
|
428
|
+
scorerErrorsByName[scorer2.name] = `Scorer "${scorer2.name}" threw: ${err instanceof Error ? err.message : String(err)}`;
|
|
429
|
+
record({ score: null, duration: Date.now() - scorerStart }, "failed");
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (!applicable) {
|
|
433
|
+
record({ score: null, skipped: true }, "skipped");
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const outcome = await trackScope(
|
|
437
|
+
runtime,
|
|
438
|
+
async () => scorer2.score(item.output, item.input, item.annotations, scorerContext),
|
|
439
|
+
// The scorer stamp is merged OVER the enclosing item's correlation, so a
|
|
440
|
+
// judge's records keep the case index and add the judge's name -- which is
|
|
441
|
+
// what lets `scoreDetails[name].diagnostics` point at this judge's own
|
|
442
|
+
// calls rather than at the whole item's.
|
|
443
|
+
{ purpose: "judging", captureCorrelation: { scorer: scorer2.name } }
|
|
444
|
+
);
|
|
445
|
+
const duration = Date.now() - scorerStart;
|
|
446
|
+
const { accounting } = outcome;
|
|
447
|
+
const measured = accounting.operations.total > 0 || accounting.operations.denied > 0;
|
|
448
|
+
const withCost = (detail, callerCost) => ({
|
|
449
|
+
...detail,
|
|
450
|
+
...measured ? { accounting, cost: accounting.knownCost } : {},
|
|
451
|
+
...!measured && !outcome.instrumented && callerCost != null ? { cost: callerCost } : {}
|
|
452
|
+
});
|
|
453
|
+
if (outcome.status === "rejected") {
|
|
454
|
+
const err = outcome.error;
|
|
455
|
+
if (err?.name === "AbortError") {
|
|
456
|
+
record(withCost({ score: null, duration }, void 0), "cancelled");
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (isAdmissionDenied(err)) {
|
|
460
|
+
context.onClosure?.("operation");
|
|
461
|
+
record(withCost({ score: null, duration }, void 0), "budget_interrupted");
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
scorerErrorsByName[scorer2.name] = `Scorer "${scorer2.name}" threw: ${err instanceof Error ? err.message : String(err)}`;
|
|
465
|
+
record(withCost({ score: null, duration }, extractScorerErrorCost(err)), "failed");
|
|
466
|
+
if (admission?.closed) context.onClosure?.("scorer");
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
try {
|
|
470
|
+
const scorerResult = normalizeScorerResult(outcome.value);
|
|
471
|
+
const callerCost = typeof scorerResult.cost === "number" && Number.isFinite(scorerResult.cost) && scorerResult.cost >= 0 ? scorerResult.cost : void 0;
|
|
472
|
+
if (!Number.isFinite(scorerResult.score) || scorerResult.score < 0 || scorerResult.score > 1) {
|
|
473
|
+
scorerErrorsByName[scorer2.name] = `Scorer "${scorer2.name}" returned out-of-range score ${scorerResult.score} for input ${describeInput(item.input)}`;
|
|
474
|
+
record(
|
|
475
|
+
withCost({ score: null, metadata: scorerResult.metadata, duration }, callerCost),
|
|
476
|
+
"failed"
|
|
477
|
+
);
|
|
478
|
+
} else {
|
|
479
|
+
item.scores[scorer2.name] = round2(scorerResult.score);
|
|
480
|
+
record(
|
|
481
|
+
withCost(
|
|
482
|
+
{
|
|
483
|
+
score: round2(scorerResult.score),
|
|
484
|
+
metadata: scorerResult.metadata,
|
|
485
|
+
duration
|
|
486
|
+
},
|
|
487
|
+
callerCost
|
|
488
|
+
),
|
|
489
|
+
"scored"
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
} catch (err) {
|
|
493
|
+
scorerErrorsByName[scorer2.name] = `Scorer "${scorer2.name}" threw: ${err instanceof Error ? err.message : String(err)}`;
|
|
494
|
+
record(withCost({ score: null, duration }, void 0), "failed");
|
|
495
|
+
}
|
|
496
|
+
if (admission?.closed) context.onClosure?.("scorer");
|
|
497
|
+
});
|
|
498
|
+
const orderedErrors = scorers.map((s) => scorerErrorsByName[s.name]).filter((e) => e != null);
|
|
499
|
+
if (orderedErrors.length > 0) item.scorerErrors = orderedErrors;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// src/runner.ts
|
|
503
|
+
import { randomUUID } from "crypto";
|
|
504
|
+
var RESERVED_METADATA_KEYS = [
|
|
505
|
+
"models",
|
|
506
|
+
"modelCallCounts",
|
|
507
|
+
"workflows",
|
|
508
|
+
"workflowCallCounts",
|
|
509
|
+
"tokens",
|
|
510
|
+
"agentCalls"
|
|
511
|
+
];
|
|
512
|
+
var ITEM_OUTCOMES = [
|
|
513
|
+
"completed",
|
|
514
|
+
"failed",
|
|
515
|
+
"cancelled",
|
|
516
|
+
"budget_skipped",
|
|
517
|
+
"budget_interrupted"
|
|
518
|
+
];
|
|
519
|
+
var SCORER_OUTCOMES = [
|
|
520
|
+
"scored",
|
|
521
|
+
"failed",
|
|
522
|
+
"skipped",
|
|
523
|
+
"cancelled",
|
|
524
|
+
"budget_skipped",
|
|
525
|
+
"budget_interrupted"
|
|
526
|
+
];
|
|
527
|
+
function extractUserCost(result, label = "executeWorkflow") {
|
|
528
|
+
if (result === null || typeof result !== "object") return void 0;
|
|
529
|
+
const raw = result.cost;
|
|
530
|
+
if (raw === void 0) return void 0;
|
|
531
|
+
if (typeof raw === "number" && Number.isFinite(raw) && raw >= 0) return raw;
|
|
532
|
+
console.warn(
|
|
533
|
+
`[axl-eval] Ignoring invalid \`cost\` from ${label} return: expected non-negative finite number, got ${typeof raw === "number" ? String(raw) : typeof raw}. Measured spend is unaffected.`
|
|
534
|
+
);
|
|
535
|
+
return void 0;
|
|
536
|
+
}
|
|
537
|
+
function extractUserMetadata(result, label = "executeWorkflow") {
|
|
538
|
+
if (result === null || typeof result !== "object") return void 0;
|
|
539
|
+
const meta = result.metadata;
|
|
540
|
+
if (meta === void 0) return void 0;
|
|
541
|
+
if (meta === null || typeof meta !== "object" || Array.isArray(meta)) {
|
|
542
|
+
console.warn(
|
|
543
|
+
`[axl-eval] Ignoring invalid \`metadata\` from ${label} return: expected plain object, got ${meta === null ? "null" : Array.isArray(meta) ? "array" : typeof meta}.`
|
|
544
|
+
);
|
|
545
|
+
return void 0;
|
|
546
|
+
}
|
|
547
|
+
const proto = Object.getPrototypeOf(meta);
|
|
548
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
549
|
+
console.warn(
|
|
550
|
+
`[axl-eval] Ignoring invalid \`metadata\` from ${label} return: expected plain object, got ${proto?.constructor?.name ?? "exotic object"}.`
|
|
551
|
+
);
|
|
552
|
+
return void 0;
|
|
553
|
+
}
|
|
554
|
+
return meta;
|
|
555
|
+
}
|
|
556
|
+
function splitCallerMetadata(user) {
|
|
557
|
+
if (!user) return {};
|
|
558
|
+
let reserved;
|
|
559
|
+
const safe = {};
|
|
560
|
+
for (const [key, value] of Object.entries(user)) {
|
|
561
|
+
if (RESERVED_METADATA_KEYS.includes(key)) {
|
|
562
|
+
(reserved ??= {})[key] = value;
|
|
563
|
+
} else {
|
|
564
|
+
safe[key] = value;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return { reserved, ...Object.keys(safe).length > 0 ? { safe } : {} };
|
|
568
|
+
}
|
|
569
|
+
function isAbortError(err) {
|
|
570
|
+
return err?.name === "AbortError";
|
|
571
|
+
}
|
|
572
|
+
async function stageCapture(runtime, ownerId, option) {
|
|
573
|
+
const limits = resolveCaptureLimits(option);
|
|
574
|
+
if (!limits) return void 0;
|
|
575
|
+
const staged = await runtime.stageDiagnosticArtifact({ kind: "eval", id: ownerId });
|
|
576
|
+
return {
|
|
577
|
+
artifactId: staged.artifactId,
|
|
578
|
+
channel: new RequestCaptureChannel({
|
|
579
|
+
sink: staged.sink,
|
|
580
|
+
...limits,
|
|
581
|
+
// Redaction is the RUNTIME's policy, not the caller's: a run cannot opt
|
|
582
|
+
// out of compliance mode by asking for diagnostics.
|
|
583
|
+
redact: runtime.isRedactEnabled()
|
|
584
|
+
})
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
async function finishCapture(runtime, capture) {
|
|
588
|
+
const status = await capture.channel.close();
|
|
589
|
+
try {
|
|
590
|
+
return await runtime.finalizeDiagnosticArtifact(
|
|
591
|
+
capture.artifactId,
|
|
592
|
+
status.status,
|
|
593
|
+
status.reason,
|
|
594
|
+
// What the channel ACTUALLY applied. The store sees only scrubbed bytes
|
|
595
|
+
// and cannot tell, so a manifest without this reports every compliance-
|
|
596
|
+
// mode artifact as unredacted.
|
|
597
|
+
status.redaction
|
|
598
|
+
);
|
|
599
|
+
} catch {
|
|
600
|
+
return void 0;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
async function abandonCapture(runtime, capture) {
|
|
604
|
+
if (!capture) return;
|
|
605
|
+
await capture.channel.close().catch(() => void 0);
|
|
606
|
+
await runtime.rollbackDiagnosticArtifact(capture.artifactId).catch(() => void 0);
|
|
607
|
+
}
|
|
608
|
+
function attachOperationRefs(items, channel) {
|
|
609
|
+
const byItem = /* @__PURE__ */ new Map();
|
|
610
|
+
const byScorer = /* @__PURE__ */ new Map();
|
|
611
|
+
for (const entry of channel.operations()) {
|
|
612
|
+
if (entry.caseIndex === void 0) continue;
|
|
613
|
+
const ref = {
|
|
614
|
+
operationId: entry.operationId,
|
|
615
|
+
kind: entry.kind,
|
|
616
|
+
...entry.turn !== void 0 ? { turn: entry.turn } : {},
|
|
617
|
+
...entry.attempt !== void 0 ? { attempt: entry.attempt } : {},
|
|
618
|
+
status: entry.status
|
|
619
|
+
};
|
|
620
|
+
if (entry.scorer !== void 0) {
|
|
621
|
+
const forItem = byScorer.get(entry.caseIndex) ?? /* @__PURE__ */ new Map();
|
|
622
|
+
const list = forItem.get(entry.scorer) ?? [];
|
|
623
|
+
list.push(ref);
|
|
624
|
+
forItem.set(entry.scorer, list);
|
|
625
|
+
byScorer.set(entry.caseIndex, forItem);
|
|
626
|
+
} else {
|
|
627
|
+
const list = byItem.get(entry.caseIndex) ?? [];
|
|
628
|
+
list.push(ref);
|
|
629
|
+
byItem.set(entry.caseIndex, list);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
for (const [caseIndex, operations] of byItem) {
|
|
633
|
+
const item = items[caseIndex];
|
|
634
|
+
if (!item) continue;
|
|
635
|
+
item.diagnostics = { operations: [...item.diagnostics?.operations ?? [], ...operations] };
|
|
636
|
+
}
|
|
637
|
+
for (const [caseIndex, forItem] of byScorer) {
|
|
638
|
+
for (const [scorer2, operations] of forItem) {
|
|
639
|
+
const detail = items[caseIndex]?.scoreDetails?.[scorer2];
|
|
640
|
+
if (detail) detail.diagnostics = { operations };
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
var MAX_CAUSE_DEPTH = 8;
|
|
645
|
+
function isProviderErrorLike(value) {
|
|
646
|
+
if (value instanceof ProviderError) return true;
|
|
647
|
+
if (typeof value !== "object" || value === null) return false;
|
|
648
|
+
const candidate = value;
|
|
649
|
+
return candidate.code === "PROVIDER_ERROR" && candidate.name === "ProviderError";
|
|
650
|
+
}
|
|
651
|
+
function describeItemFailure(thrown) {
|
|
652
|
+
let current = thrown;
|
|
653
|
+
for (let depth = 0; depth <= MAX_CAUSE_DEPTH; depth++) {
|
|
654
|
+
if (typeof current !== "object" || current === null) break;
|
|
655
|
+
if (isProviderErrorLike(current)) {
|
|
656
|
+
const { name: name2, provider, status, retryable, requestId } = current;
|
|
657
|
+
return {
|
|
658
|
+
name: typeof name2 === "string" ? name2 : "ProviderError",
|
|
659
|
+
...typeof provider === "string" ? { provider } : {},
|
|
660
|
+
...typeof status === "number" && Number.isFinite(status) ? { status } : {},
|
|
661
|
+
...typeof retryable === "boolean" ? { retryable } : {},
|
|
662
|
+
...typeof requestId === "string" ? { requestId } : {}
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
current = current.cause;
|
|
666
|
+
}
|
|
667
|
+
const name = thrown?.name;
|
|
668
|
+
return typeof thrown === "object" && thrown !== null && typeof name === "string" && name ? { name } : void 0;
|
|
669
|
+
}
|
|
670
|
+
async function runEval(config, executeWorkflow, runtime, options) {
|
|
671
|
+
const startTime = Date.now();
|
|
672
|
+
const id = randomUUID();
|
|
673
|
+
const itemErrorRateLimit = config.failOnItemErrorRate ?? DEFAULT_ITEM_ERROR_RATE_LIMIT;
|
|
674
|
+
if (!isErrorRateLimit(itemErrorRateLimit)) {
|
|
675
|
+
throw new AxlError2(
|
|
676
|
+
"INVALID_ITEM_ERROR_RATE",
|
|
677
|
+
`Invalid failOnItemErrorRate (${String(itemErrorRateLimit)}): expected a number in [0, 1]; 1 disables the gate.`
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
const admission = config.budget != null ? new AdmissionController({ limit: parseBudget(config.budget) }) : void 0;
|
|
681
|
+
const capture = await stageCapture(runtime, id, options?.captureRequests);
|
|
682
|
+
let items;
|
|
683
|
+
try {
|
|
684
|
+
items = await config.dataset.getItems();
|
|
685
|
+
} catch (error) {
|
|
686
|
+
await abandonCapture(runtime, capture);
|
|
687
|
+
throw error;
|
|
688
|
+
}
|
|
689
|
+
const droppedAnnotationKeys = [...config.dataset.droppedAnnotationKeys ?? []];
|
|
690
|
+
const concurrency = config.concurrency ?? 5;
|
|
691
|
+
const scorerConcurrency = config.scorerConcurrency ?? 5;
|
|
692
|
+
let closedBy;
|
|
693
|
+
const noteClosure = (source) => {
|
|
694
|
+
if (closedBy === void 0 && admission?.closed) closedBy = source;
|
|
695
|
+
};
|
|
696
|
+
const scorerContext = {
|
|
697
|
+
resolveProvider: (uri) => {
|
|
698
|
+
if (typeof runtime.resolveProvider !== "function") {
|
|
699
|
+
throw new Error(
|
|
700
|
+
`LLM scorers require a runtime with resolveProvider(). Ensure you are using a real AxlRuntime instance, not a mock.`
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
return runtime.resolveProvider(uri);
|
|
704
|
+
},
|
|
705
|
+
signal: options?.signal
|
|
706
|
+
};
|
|
707
|
+
const evalItems = new Array(items.length);
|
|
708
|
+
const callerReported = { costItems: 0, costTotal: 0, metadataItems: 0 };
|
|
709
|
+
const runCallSamples = /* @__PURE__ */ new Map();
|
|
710
|
+
function absorbTiming(rollup, evalItem) {
|
|
711
|
+
if (!rollup) return;
|
|
712
|
+
const perItem = {};
|
|
713
|
+
for (const [model, bucket] of Object.entries(rollup)) {
|
|
714
|
+
const sums = {
|
|
715
|
+
calls: bucket.calls,
|
|
716
|
+
queuedMs: bucket.queuedMs,
|
|
717
|
+
retryMs: bucket.retryMs,
|
|
718
|
+
wireMs: bucket.wireMs,
|
|
719
|
+
...bucket.firstTokenMs !== void 0 && bucket.firstTokenCalls !== void 0 ? { firstTokenMs: bucket.firstTokenMs, firstTokenCalls: bucket.firstTokenCalls } : {}
|
|
720
|
+
};
|
|
721
|
+
perItem[model] = sums;
|
|
722
|
+
const samples = bucket.samples;
|
|
723
|
+
if (!samples) continue;
|
|
724
|
+
const pooled = runCallSamples.get(model) ?? [];
|
|
725
|
+
for (const sample of samples) pooled.push(sample);
|
|
726
|
+
runCallSamples.set(model, pooled);
|
|
727
|
+
}
|
|
728
|
+
evalItem.timing = perItem;
|
|
729
|
+
}
|
|
730
|
+
function stub(item, outcome, error) {
|
|
731
|
+
return {
|
|
732
|
+
input: item.input,
|
|
733
|
+
annotations: item.annotations,
|
|
734
|
+
output: null,
|
|
735
|
+
error,
|
|
736
|
+
outcome,
|
|
737
|
+
accounting: emptyAccounting(),
|
|
738
|
+
cost: 0,
|
|
739
|
+
scorerCost: 0,
|
|
740
|
+
scores: {}
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
async function processItem(item, itemIndex) {
|
|
744
|
+
if (options?.signal?.aborted) {
|
|
745
|
+
evalItems[itemIndex] = stub(item, "cancelled", "Cancelled");
|
|
746
|
+
options?.onProgress?.({ type: "item_done", itemIndex, totalItems: items.length });
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
if (admission?.closed) {
|
|
750
|
+
noteClosure("case");
|
|
751
|
+
evalItems[itemIndex] = stub(item, "budget_skipped", "Budget exceeded");
|
|
752
|
+
options?.onProgress?.({ type: "item_done", itemIndex, totalItems: items.length });
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const evalItem = {
|
|
756
|
+
input: item.input,
|
|
757
|
+
annotations: item.annotations,
|
|
758
|
+
output: null,
|
|
759
|
+
scores: {}
|
|
760
|
+
};
|
|
761
|
+
const itemStart = Date.now();
|
|
762
|
+
const itemOutcome = await trackScope(
|
|
763
|
+
runtime,
|
|
764
|
+
async () => {
|
|
765
|
+
const generation = await trackScope(
|
|
766
|
+
runtime,
|
|
767
|
+
async () => executeWorkflow(item.input, runtime),
|
|
768
|
+
{
|
|
769
|
+
purpose: "generation",
|
|
770
|
+
captureTraces: options?.captureTraces === true,
|
|
771
|
+
captureTimingSamples: true
|
|
772
|
+
}
|
|
773
|
+
);
|
|
774
|
+
evalItem.duration = Date.now() - itemStart;
|
|
775
|
+
absorbTiming(generation.modelTiming, evalItem);
|
|
776
|
+
if (generation.traces && generation.traces.length > 0) {
|
|
777
|
+
evalItem.traces = generation.traces;
|
|
778
|
+
}
|
|
779
|
+
if (generation.status === "rejected") {
|
|
780
|
+
const err = generation.error;
|
|
781
|
+
if (isAdmissionDenied(err)) {
|
|
782
|
+
noteClosure("operation");
|
|
783
|
+
evalItem.outcome = "budget_interrupted";
|
|
784
|
+
evalItem.error = "Budget interrupted";
|
|
785
|
+
} else if (isAbortError(err)) {
|
|
786
|
+
evalItem.outcome = "cancelled";
|
|
787
|
+
evalItem.error = "Cancelled";
|
|
788
|
+
} else {
|
|
789
|
+
evalItem.outcome = "failed";
|
|
790
|
+
const failure = describeItemFailure(err);
|
|
791
|
+
if (failure) evalItem.failure = failure;
|
|
792
|
+
evalItem.error = err instanceof Error ? err.message : String(err);
|
|
793
|
+
if (admission?.closed) noteClosure("case");
|
|
794
|
+
}
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
const result = generation.value;
|
|
798
|
+
evalItem.outcome = "completed";
|
|
799
|
+
evalItem.output = result.output;
|
|
800
|
+
const callerCost = extractUserCost(result);
|
|
801
|
+
if (callerCost !== void 0) {
|
|
802
|
+
(evalItem.callerReport ??= {}).cost = callerCost;
|
|
803
|
+
callerReported.costItems += 1;
|
|
804
|
+
callerReported.costTotal += callerCost;
|
|
805
|
+
}
|
|
806
|
+
const { reserved, safe } = splitCallerMetadata(extractUserMetadata(result));
|
|
807
|
+
if (reserved) {
|
|
808
|
+
(evalItem.callerReport ??= {}).metadata = reserved;
|
|
809
|
+
callerReported.metadataItems += 1;
|
|
810
|
+
}
|
|
811
|
+
if (generation.metadata || safe) {
|
|
812
|
+
evalItem.metadata = { ...generation.metadata, ...safe };
|
|
813
|
+
}
|
|
814
|
+
if (admission?.closed) noteClosure("case");
|
|
815
|
+
await scoreItem(evalItem, config.scorers, {
|
|
816
|
+
runtime,
|
|
817
|
+
scorerContext,
|
|
818
|
+
scorerConcurrency,
|
|
819
|
+
signal: options?.signal,
|
|
820
|
+
admission,
|
|
821
|
+
onClosure: noteClosure
|
|
822
|
+
});
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
purpose: "generation",
|
|
826
|
+
// Every record produced anywhere under this item — workflow turns,
|
|
827
|
+
// nested asks, tool continuations, judges — is stamped with the case
|
|
828
|
+
// index, which is how the result can point an item at its own requests.
|
|
829
|
+
...capture ? { captureCorrelation: { caseIndex: itemIndex } } : {}
|
|
830
|
+
}
|
|
831
|
+
);
|
|
832
|
+
if (itemOutcome.status === "rejected") throw itemOutcome.error;
|
|
833
|
+
evalItem.accounting = itemOutcome.accounting;
|
|
834
|
+
evalItem.cost = itemOutcome.accounting.breakdown.generation;
|
|
835
|
+
evalItem.scorerCost = itemOutcome.accounting.breakdown.judging;
|
|
836
|
+
if (itemOutcome.accounting.completeness !== "complete") evalItem.unpriced = true;
|
|
837
|
+
evalItems[itemIndex] = evalItem;
|
|
838
|
+
options?.onProgress?.({ type: "item_done", itemIndex, totalItems: items.length });
|
|
839
|
+
}
|
|
840
|
+
const runOutcome = await trackScope(
|
|
841
|
+
runtime,
|
|
842
|
+
async () => {
|
|
843
|
+
await mapWithConcurrency(items, concurrency, (item, i) => processItem(item, i));
|
|
844
|
+
},
|
|
845
|
+
// The capture channel is declared ONCE, at the run scope: its byte budget
|
|
846
|
+
// and pending queue are per RUN, and every nested item/scorer scope
|
|
847
|
+
// inherits it rather than opening a competing budget of its own.
|
|
848
|
+
{ admission, ...capture ? { capture: capture.channel } : {} }
|
|
849
|
+
);
|
|
850
|
+
if (runOutcome.status === "rejected") {
|
|
851
|
+
await abandonCapture(runtime, capture);
|
|
852
|
+
throw runOutcome.error;
|
|
853
|
+
}
|
|
854
|
+
let diagnostics;
|
|
855
|
+
if (capture) {
|
|
856
|
+
try {
|
|
857
|
+
attachOperationRefs(evalItems, capture.channel);
|
|
858
|
+
} catch (error) {
|
|
859
|
+
await abandonCapture(runtime, capture);
|
|
860
|
+
throw error;
|
|
861
|
+
}
|
|
862
|
+
const manifest = await finishCapture(runtime, capture);
|
|
863
|
+
diagnostics = manifest ? toDiagnosticManifest(manifest) : (
|
|
864
|
+
// An artifact that was never sealed is never committed, so publishing
|
|
865
|
+
// its id would point a reader at bytes the sweeper is about to reclaim.
|
|
866
|
+
unavailableManifest("", "the diagnostic artifact could not be finalized")
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
const accounting = {
|
|
870
|
+
...runOutcome.accounting,
|
|
871
|
+
scope: "run",
|
|
872
|
+
...admission ? {
|
|
873
|
+
budget: {
|
|
874
|
+
...admission.snapshot(),
|
|
875
|
+
...closedBy ? { closedBy } : {}
|
|
876
|
+
}
|
|
877
|
+
} : {},
|
|
878
|
+
...callerReported.costItems > 0 || callerReported.metadataItems > 0 ? { callerReported } : {}
|
|
879
|
+
};
|
|
880
|
+
const failures = evalItems.filter((i) => i.error).length;
|
|
881
|
+
const scorerNames = config.scorers.map((s) => s.name);
|
|
882
|
+
const scorerStats = {};
|
|
883
|
+
for (const name of scorerNames) {
|
|
884
|
+
const scores = evalItems.filter((i) => !i.error && i.scores[name] != null).map((i) => i.scores[name]);
|
|
885
|
+
const { scored, failed, skipped } = scorerCounts(evalItems, name);
|
|
886
|
+
scorerStats[name] = { ...computeStats(scores), scored, failed, skipped };
|
|
887
|
+
}
|
|
888
|
+
const coverage = buildCoverage(evalItems, scorerNames);
|
|
889
|
+
const scorerTypes = {};
|
|
890
|
+
for (const s of config.scorers) {
|
|
891
|
+
scorerTypes[s.name] = s.isLlm ? "llm" : "deterministic";
|
|
892
|
+
}
|
|
893
|
+
let degraded;
|
|
894
|
+
const rawLimit = config.failOnScorerErrorRate;
|
|
895
|
+
if (rawLimit != null) {
|
|
896
|
+
if (!Number.isFinite(rawLimit) || rawLimit < 0 || rawLimit > 1) {
|
|
897
|
+
console.warn(
|
|
898
|
+
`[axl-eval] Ignoring invalid failOnScorerErrorRate (${rawLimit}); expected a number in [0, 1].`
|
|
899
|
+
);
|
|
900
|
+
} else {
|
|
901
|
+
for (const name of scorerNames) {
|
|
902
|
+
const stats = scorerStats[name];
|
|
903
|
+
const scored = stats.scored ?? 0;
|
|
904
|
+
const failed = stats.failed ?? 0;
|
|
905
|
+
const type = scorerTypes[name] === "llm" ? "llm" : "deterministic";
|
|
906
|
+
const limit = type === "deterministic" ? 0 : rawLimit;
|
|
907
|
+
const verdict = evaluateScorerTolerance(scored, failed, type, limit);
|
|
908
|
+
if (verdict.exceeds) {
|
|
909
|
+
(degraded ??= []).push({ scorer: name, rate: verdict.rate, limit, type, scored, failed });
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
const itemErrorRate = evaluateItemErrorRate(coverage.items, items.length, itemErrorRateLimit);
|
|
915
|
+
const durations = evalItems.filter((i) => !i.error && i.duration != null).map((i) => i.duration);
|
|
916
|
+
const timing = durations.length > 0 ? computeStats(durations) : void 0;
|
|
917
|
+
const modelTiming = buildModelTiming(runCallSamples);
|
|
918
|
+
const totalModelCalls = /* @__PURE__ */ new Map();
|
|
919
|
+
for (const item of evalItems) {
|
|
920
|
+
const itemCounts = item.metadata?.modelCallCounts;
|
|
921
|
+
if (itemCounts && typeof itemCounts === "object") {
|
|
922
|
+
for (const [m, count] of Object.entries(itemCounts)) {
|
|
923
|
+
if (typeof count === "number")
|
|
924
|
+
totalModelCalls.set(m, (totalModelCalls.get(m) ?? 0) + count);
|
|
925
|
+
}
|
|
926
|
+
} else {
|
|
927
|
+
const itemModels = item.metadata?.models;
|
|
928
|
+
if (Array.isArray(itemModels)) {
|
|
929
|
+
for (const m of itemModels) {
|
|
930
|
+
if (typeof m === "string") totalModelCalls.set(m, (totalModelCalls.get(m) ?? 0) + 1);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
const modelsMeta = {};
|
|
936
|
+
if (totalModelCalls.size > 0) {
|
|
937
|
+
const sorted = [...totalModelCalls.entries()].sort((a, b) => b[1] - a[1]);
|
|
938
|
+
modelsMeta.models = sorted.map(([m]) => m);
|
|
939
|
+
modelsMeta.modelCounts = Object.fromEntries(sorted);
|
|
940
|
+
}
|
|
941
|
+
const totalWorkflowCalls = /* @__PURE__ */ new Map();
|
|
942
|
+
for (const item of evalItems) {
|
|
943
|
+
const itemCounts = item.metadata?.workflowCallCounts;
|
|
944
|
+
if (itemCounts && typeof itemCounts === "object") {
|
|
945
|
+
for (const [w, count] of Object.entries(itemCounts)) {
|
|
946
|
+
if (typeof count === "number")
|
|
947
|
+
totalWorkflowCalls.set(w, (totalWorkflowCalls.get(w) ?? 0) + count);
|
|
948
|
+
}
|
|
949
|
+
} else {
|
|
950
|
+
const itemWorkflows = item.metadata?.workflows;
|
|
951
|
+
if (Array.isArray(itemWorkflows)) {
|
|
952
|
+
for (const w of itemWorkflows) {
|
|
953
|
+
if (typeof w === "string")
|
|
954
|
+
totalWorkflowCalls.set(w, (totalWorkflowCalls.get(w) ?? 0) + 1);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
const workflowsMeta = {};
|
|
960
|
+
if (totalWorkflowCalls.size > 0) {
|
|
961
|
+
const sorted = [...totalWorkflowCalls.entries()].sort((a, b) => b[1] - a[1]);
|
|
962
|
+
workflowsMeta.workflows = sorted.map(([w]) => w);
|
|
963
|
+
workflowsMeta.workflowCounts = Object.fromEntries(sorted);
|
|
964
|
+
} else if (config.workflow) {
|
|
965
|
+
workflowsMeta.workflows = [config.workflow];
|
|
966
|
+
workflowsMeta.workflowCounts = { [config.workflow]: items.length };
|
|
967
|
+
}
|
|
968
|
+
options?.onProgress?.({ type: "run_done", totalItems: items.length, failures });
|
|
969
|
+
return {
|
|
970
|
+
id,
|
|
971
|
+
dataset: config.dataset.name,
|
|
972
|
+
metadata: {
|
|
973
|
+
...config.metadata,
|
|
974
|
+
scorerTypes,
|
|
975
|
+
...modelsMeta,
|
|
976
|
+
...workflowsMeta,
|
|
977
|
+
...droppedAnnotationKeys.length > 0 ? { droppedAnnotationKeys } : {}
|
|
978
|
+
},
|
|
979
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
980
|
+
totalCost: accounting.knownCost,
|
|
981
|
+
...accounting.completeness !== "complete" ? { unpriced: true } : {},
|
|
982
|
+
accounting,
|
|
983
|
+
...diagnostics ? { diagnostics } : {},
|
|
984
|
+
duration: Date.now() - startTime,
|
|
985
|
+
items: evalItems,
|
|
986
|
+
summary: {
|
|
987
|
+
count: items.length,
|
|
988
|
+
failures,
|
|
989
|
+
coverage,
|
|
990
|
+
scorers: scorerStats,
|
|
991
|
+
timing,
|
|
992
|
+
...modelTiming ? { modelTiming } : {},
|
|
993
|
+
...degraded ? { degraded } : {},
|
|
994
|
+
...itemErrorRate.failed > 0 ? { itemErrorRate } : {}
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
function buildCoverage(items, scorerNames) {
|
|
999
|
+
const coverage = {
|
|
1000
|
+
items: Object.fromEntries(ITEM_OUTCOMES.map((o) => [o, 0])),
|
|
1001
|
+
scorers: {}
|
|
1002
|
+
};
|
|
1003
|
+
for (const name of scorerNames) {
|
|
1004
|
+
coverage.scorers[name] = Object.fromEntries(SCORER_OUTCOMES.map((o) => [o, 0]));
|
|
1005
|
+
}
|
|
1006
|
+
for (const item of items) {
|
|
1007
|
+
const outcome = item.outcome ?? (item.error ? "failed" : "completed");
|
|
1008
|
+
if (coverage.items[outcome] !== void 0) coverage.items[outcome] += 1;
|
|
1009
|
+
for (const name of scorerNames) {
|
|
1010
|
+
const detail = item.scoreDetails?.[name];
|
|
1011
|
+
if (!detail?.outcome) continue;
|
|
1012
|
+
const bucket = coverage.scorers[name];
|
|
1013
|
+
if (bucket[detail.outcome] !== void 0) bucket[detail.outcome] += 1;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return coverage;
|
|
1017
|
+
}
|
|
1018
|
+
function buildModelTiming(runCallSamples) {
|
|
1019
|
+
if (runCallSamples.size === 0) return void 0;
|
|
1020
|
+
return Object.fromEntries(
|
|
1021
|
+
[...runCallSamples.entries()].map(([model, samples]) => {
|
|
1022
|
+
const firstToken = samples.map((s) => s.firstTokenMs).filter((v) => v != null);
|
|
1023
|
+
return [
|
|
1024
|
+
model,
|
|
1025
|
+
{
|
|
1026
|
+
calls: samples.length,
|
|
1027
|
+
wireMs: computeStats(samples.map((s) => s.wireMs)),
|
|
1028
|
+
queuedMs: computeStats(samples.map((s) => s.queuedMs)),
|
|
1029
|
+
retryMs: computeStats(samples.map((s) => s.retryMs)),
|
|
1030
|
+
// Custom providers may omit the optional field; that call adds 0.
|
|
1031
|
+
rateLimitRetries: samples.reduce((sum, s) => sum + (s.rateLimitRetries ?? 0), 0),
|
|
1032
|
+
...firstToken.length > 0 ? { firstTokenMs: computeStats(firstToken), firstTokenCalls: firstToken.length } : {}
|
|
1033
|
+
}
|
|
1034
|
+
];
|
|
1035
|
+
})
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// src/compare.ts
|
|
1040
|
+
function certifyCost(baselineRuns, candidateRuns, scorerNames) {
|
|
1041
|
+
for (const [side, runs] of [
|
|
1042
|
+
["baseline", baselineRuns],
|
|
1043
|
+
["candidate", candidateRuns]
|
|
1044
|
+
]) {
|
|
1045
|
+
for (const run of runs) {
|
|
1046
|
+
const accounting = readAccounting(run);
|
|
1047
|
+
if (accounting.completeness === "unverified") {
|
|
1048
|
+
return `${side} run ${run.id} carries no accounting (unverified) \u2014 its total is not a measured figure.`;
|
|
1049
|
+
}
|
|
1050
|
+
if (accounting.completeness === "incomplete") {
|
|
1051
|
+
const reasons = Object.keys(accounting.reasons).join(", ") || "unknown spend";
|
|
1052
|
+
return `${side} run ${run.id} has incomplete accounting (${reasons}) \u2014 its total is a lower bound.`;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
const scopes = /* @__PURE__ */ new Map();
|
|
1057
|
+
for (const [side, runs] of [
|
|
1058
|
+
["baseline", baselineRuns],
|
|
1059
|
+
["candidate", candidateRuns]
|
|
1060
|
+
]) {
|
|
1061
|
+
for (const run of runs) {
|
|
1062
|
+
const scope = readAccounting(run).scope;
|
|
1063
|
+
const seen = scopes.get(side);
|
|
1064
|
+
if (seen !== void 0 && seen !== scope) {
|
|
1065
|
+
return `${side} mixes accounting scopes ("${seen}" and "${scope}") \u2014 a rescore total covers judging only and cannot be averaged with a full run.`;
|
|
1066
|
+
}
|
|
1067
|
+
scopes.set(side, scope);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
const baselineScope = scopes.get("baseline");
|
|
1071
|
+
const candidateScope = scopes.get("candidate");
|
|
1072
|
+
if (baselineScope !== candidateScope) {
|
|
1073
|
+
return `accounting scope differs (baseline "${baselineScope}" vs candidate "${candidateScope}") \u2014 a rescore total covers judging only.`;
|
|
1074
|
+
}
|
|
1075
|
+
const runCount = Math.min(baselineRuns.length, candidateRuns.length);
|
|
1076
|
+
for (let r = 0; r < runCount; r++) {
|
|
1077
|
+
const bRun = baselineRuns[r];
|
|
1078
|
+
const cRun = candidateRuns[r];
|
|
1079
|
+
if (bRun.items.length !== cRun.items.length) {
|
|
1080
|
+
return `item counts differ (baseline ${bRun.items.length} vs candidate ${cRun.items.length}) \u2014 the runs did different amounts of work.`;
|
|
1081
|
+
}
|
|
1082
|
+
for (let i = 0; i < bRun.items.length; i++) {
|
|
1083
|
+
const bItem = bRun.items[i];
|
|
1084
|
+
const cItem = cRun.items[i];
|
|
1085
|
+
const bRan = !bItem.error;
|
|
1086
|
+
const cRan = !cItem.error;
|
|
1087
|
+
if (bRan !== cRan) {
|
|
1088
|
+
const stopped = bRan ? cItem : bItem;
|
|
1089
|
+
return `case coverage differs at item ${i}: ${bRan ? "candidate" : "baseline"} did not complete it (${stopped.outcome ?? stopped.error}) \u2014 the cheaper side did less work.`;
|
|
1090
|
+
}
|
|
1091
|
+
if (!bRan) continue;
|
|
1092
|
+
for (const name of scorerNames) {
|
|
1093
|
+
const bScored = bItem.scores[name] != null;
|
|
1094
|
+
const cScored = cItem.scores[name] != null;
|
|
1095
|
+
if (bScored !== cScored) {
|
|
1096
|
+
return `scorer coverage differs for "${name}" at item ${i}: only ${bScored ? "baseline" : "candidate"} produced a score.`;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return null;
|
|
1102
|
+
}
|
|
1103
|
+
var DEFAULT_LLM_THRESHOLD = 0.05;
|
|
1104
|
+
var DEFAULT_DETERMINISTIC_THRESHOLD = 0;
|
|
1105
|
+
var LEGACY_THRESHOLD = 0.1;
|
|
1106
|
+
function resolveThreshold(scorerName, options, metadata) {
|
|
1107
|
+
if (typeof options?.thresholds === "number") return options.thresholds;
|
|
1108
|
+
if (options?.thresholds && typeof options.thresholds === "object") {
|
|
1109
|
+
const perScorer = options.thresholds[scorerName];
|
|
1110
|
+
if (perScorer != null) return perScorer;
|
|
1111
|
+
}
|
|
1112
|
+
const scorerTypes = metadata.scorerTypes;
|
|
1113
|
+
if (scorerTypes && scorerName in scorerTypes) {
|
|
1114
|
+
return scorerTypes[scorerName] === "llm" ? DEFAULT_LLM_THRESHOLD : DEFAULT_DETERMINISTIC_THRESHOLD;
|
|
1115
|
+
}
|
|
1116
|
+
return LEGACY_THRESHOLD;
|
|
1117
|
+
}
|
|
1118
|
+
function evalCompare(baseline, candidate, options) {
|
|
1119
|
+
const baselineRunsRaw = Array.isArray(baseline) ? baseline : [baseline];
|
|
1120
|
+
const candidateRunsRaw = Array.isArray(candidate) ? candidate : [candidate];
|
|
1121
|
+
if (baselineRunsRaw.length === 0 || candidateRunsRaw.length === 0) {
|
|
1122
|
+
throw new Error("Cannot compare empty eval result arrays");
|
|
1123
|
+
}
|
|
1124
|
+
const runCount = Math.min(baselineRunsRaw.length, candidateRunsRaw.length);
|
|
1125
|
+
const baselineRuns = baselineRunsRaw.slice(0, runCount);
|
|
1126
|
+
const candidateRuns = candidateRunsRaw.slice(0, runCount);
|
|
1127
|
+
const baselineRef = baselineRunsRaw[0];
|
|
1128
|
+
const candidateRef = candidateRunsRaw[0];
|
|
1129
|
+
if (baselineRef.dataset !== candidateRef.dataset) {
|
|
1130
|
+
throw new Error(
|
|
1131
|
+
`Cannot compare evals from different datasets: "${baselineRef.dataset}" vs "${candidateRef.dataset}"`
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
const baselineScorerNames = Object.keys(baselineRef.summary.scorers).sort();
|
|
1135
|
+
const candidateScorerNames = Object.keys(candidateRef.summary.scorers).sort();
|
|
1136
|
+
if (JSON.stringify(baselineScorerNames) !== JSON.stringify(candidateScorerNames)) {
|
|
1137
|
+
throw new Error(`Cannot compare evals with different scorers`);
|
|
1138
|
+
}
|
|
1139
|
+
const pairedDiffs = {};
|
|
1140
|
+
for (const name of baselineScorerNames) {
|
|
1141
|
+
pairedDiffs[name] = [];
|
|
1142
|
+
}
|
|
1143
|
+
for (let r = 0; r < runCount; r++) {
|
|
1144
|
+
const bRun = baselineRuns[r];
|
|
1145
|
+
const cRun = candidateRuns[r];
|
|
1146
|
+
const minLength = Math.min(bRun.items.length, cRun.items.length);
|
|
1147
|
+
for (let i = 0; i < minLength; i++) {
|
|
1148
|
+
const bItem = bRun.items[i];
|
|
1149
|
+
const cItem = cRun.items[i];
|
|
1150
|
+
if (bItem.error || cItem.error) continue;
|
|
1151
|
+
for (const name of baselineScorerNames) {
|
|
1152
|
+
const bScore = bItem.scores[name];
|
|
1153
|
+
const cScore = cItem.scores[name];
|
|
1154
|
+
if (bScore != null && cScore != null) {
|
|
1155
|
+
pairedDiffs[name].push(cScore - bScore);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
const baselineMeans = {};
|
|
1161
|
+
const candidateMeans = {};
|
|
1162
|
+
for (const name of baselineScorerNames) {
|
|
1163
|
+
baselineMeans[name] = baselineRuns.reduce((sum, r) => sum + (r.summary.scorers[name]?.mean ?? 0), 0) / baselineRuns.length;
|
|
1164
|
+
candidateMeans[name] = candidateRuns.reduce((sum, r) => sum + (r.summary.scorers[name]?.mean ?? 0), 0) / candidateRuns.length;
|
|
1165
|
+
}
|
|
1166
|
+
const scorers = {};
|
|
1167
|
+
for (const name of baselineScorerNames) {
|
|
1168
|
+
const bMeanRaw = baselineMeans[name];
|
|
1169
|
+
const cMeanRaw = candidateMeans[name];
|
|
1170
|
+
const deltaRaw = cMeanRaw - bMeanRaw;
|
|
1171
|
+
const bMean = round2(bMeanRaw);
|
|
1172
|
+
const cMean = round2(cMeanRaw);
|
|
1173
|
+
const delta = round2(deltaRaw);
|
|
1174
|
+
const deltaPercent = bMeanRaw > 0 ? round2(deltaRaw / bMeanRaw * 100) : 0;
|
|
1175
|
+
const threshold = resolveThreshold(name, options, baselineRef.metadata);
|
|
1176
|
+
let baselineScored = 0;
|
|
1177
|
+
let baselineFailed = 0;
|
|
1178
|
+
let baselineSkipped = 0;
|
|
1179
|
+
for (const run of baselineRuns) {
|
|
1180
|
+
const c = scorerCounts(run.items, name);
|
|
1181
|
+
baselineScored += c.scored;
|
|
1182
|
+
baselineFailed += c.failed;
|
|
1183
|
+
baselineSkipped += c.skipped;
|
|
1184
|
+
}
|
|
1185
|
+
let candidateScored = 0;
|
|
1186
|
+
let candidateFailed = 0;
|
|
1187
|
+
let candidateSkipped = 0;
|
|
1188
|
+
for (const run of candidateRuns) {
|
|
1189
|
+
const c = scorerCounts(run.items, name);
|
|
1190
|
+
candidateScored += c.scored;
|
|
1191
|
+
candidateFailed += c.failed;
|
|
1192
|
+
candidateSkipped += c.skipped;
|
|
1193
|
+
}
|
|
1194
|
+
const entry = {
|
|
1195
|
+
baselineMean: bMean,
|
|
1196
|
+
candidateMean: cMean,
|
|
1197
|
+
delta,
|
|
1198
|
+
deltaPercent,
|
|
1199
|
+
baselineScored,
|
|
1200
|
+
baselineFailed,
|
|
1201
|
+
candidateScored,
|
|
1202
|
+
candidateFailed,
|
|
1203
|
+
baselineSkipped,
|
|
1204
|
+
candidateSkipped
|
|
1205
|
+
};
|
|
1206
|
+
const diffs = pairedDiffs[name];
|
|
1207
|
+
entry.n = diffs.length;
|
|
1208
|
+
if (diffs.length >= 2) {
|
|
1209
|
+
const ci = pairedBootstrapCI(diffs);
|
|
1210
|
+
entry.ci = { lower: ci.lower, upper: ci.upper };
|
|
1211
|
+
entry.pRegression = ci.pRegression;
|
|
1212
|
+
entry.pImprovement = ci.pImprovement;
|
|
1213
|
+
const ciExcludesZero = ci.lower > 0 || ci.upper < 0;
|
|
1214
|
+
entry.significant = ciExcludesZero && Math.abs(ci.mean) >= threshold;
|
|
1215
|
+
}
|
|
1216
|
+
scorers[name] = entry;
|
|
1217
|
+
}
|
|
1218
|
+
const regressions = [];
|
|
1219
|
+
const improvements = [];
|
|
1220
|
+
const itemCount = Math.min(
|
|
1221
|
+
...baselineRuns.map((r) => r.items.length),
|
|
1222
|
+
...candidateRuns.map((r) => r.items.length)
|
|
1223
|
+
);
|
|
1224
|
+
for (let i = 0; i < itemCount; i++) {
|
|
1225
|
+
const hasBaselineError = baselineRuns.some((r) => r.items[i]?.error);
|
|
1226
|
+
const hasCandidateError = candidateRuns.some((r) => r.items[i]?.error);
|
|
1227
|
+
if (hasBaselineError || hasCandidateError) continue;
|
|
1228
|
+
for (const name of baselineScorerNames) {
|
|
1229
|
+
const bScores = baselineRuns.map((r) => r.items[i]?.scores[name]).filter((s) => s != null);
|
|
1230
|
+
const cScores = candidateRuns.map((r) => r.items[i]?.scores[name]).filter((s) => s != null);
|
|
1231
|
+
if (bScores.length === 0 || cScores.length === 0) continue;
|
|
1232
|
+
const bAvg = bScores.reduce((a, b) => a + b, 0) / bScores.length;
|
|
1233
|
+
const cAvg = cScores.reduce((a, b) => a + b, 0) / cScores.length;
|
|
1234
|
+
const delta = round2(cAvg - bAvg);
|
|
1235
|
+
const threshold = resolveThreshold(name, options, baselineRef.metadata);
|
|
1236
|
+
if (delta < -threshold)
|
|
1237
|
+
regressions.push({
|
|
1238
|
+
itemIndex: i,
|
|
1239
|
+
input: baselineRef.items[i]?.input,
|
|
1240
|
+
scorer: name,
|
|
1241
|
+
baselineScore: round2(bAvg),
|
|
1242
|
+
candidateScore: round2(cAvg),
|
|
1243
|
+
delta
|
|
1244
|
+
});
|
|
1245
|
+
else if (delta > threshold)
|
|
1246
|
+
improvements.push({
|
|
1247
|
+
itemIndex: i,
|
|
1248
|
+
input: baselineRef.items[i]?.input,
|
|
1249
|
+
scorer: name,
|
|
1250
|
+
baselineScore: round2(bAvg),
|
|
1251
|
+
candidateScore: round2(cAvg),
|
|
1252
|
+
delta
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
let timing;
|
|
1257
|
+
const baselineTimings = baselineRuns.filter((r) => r.summary.timing).map((r) => r.summary.timing.mean);
|
|
1258
|
+
const candidateTimings = candidateRuns.filter((r) => r.summary.timing).map((r) => r.summary.timing.mean);
|
|
1259
|
+
if (baselineTimings.length > 0 && candidateTimings.length > 0) {
|
|
1260
|
+
const bMean = baselineTimings.reduce((a, b) => a + b, 0) / baselineTimings.length;
|
|
1261
|
+
const cMean = candidateTimings.reduce((a, b) => a + b, 0) / candidateTimings.length;
|
|
1262
|
+
const delta = round2(cMean - bMean);
|
|
1263
|
+
const deltaPercent = bMean > 0 ? round2((cMean - bMean) / bMean * 100) : 0;
|
|
1264
|
+
timing = { baselineMean: round2(bMean), candidateMean: round2(cMean), delta, deltaPercent };
|
|
1265
|
+
}
|
|
1266
|
+
let cost;
|
|
1267
|
+
const baselineAvgCost = baselineRuns.reduce((sum, r) => sum + readAccounting(r).knownCost, 0) / baselineRuns.length;
|
|
1268
|
+
const candidateAvgCost = candidateRuns.reduce((sum, r) => sum + readAccounting(r).knownCost, 0) / candidateRuns.length;
|
|
1269
|
+
const hasAccounting = [...baselineRuns, ...candidateRuns].some((r) => r.accounting != null);
|
|
1270
|
+
if (baselineAvgCost > 0 || candidateAvgCost > 0 || hasAccounting) {
|
|
1271
|
+
const deltaRaw = candidateAvgCost - baselineAvgCost;
|
|
1272
|
+
const delta = round2(deltaRaw);
|
|
1273
|
+
const deltaPercent = baselineAvgCost > 0 ? round2(deltaRaw / baselineAvgCost * 100) : null;
|
|
1274
|
+
const verdict = certifyCost(baselineRuns, candidateRuns, baselineScorerNames);
|
|
1275
|
+
cost = {
|
|
1276
|
+
baselineTotal: round2(baselineAvgCost),
|
|
1277
|
+
candidateTotal: round2(candidateAvgCost),
|
|
1278
|
+
delta,
|
|
1279
|
+
deltaPercent,
|
|
1280
|
+
certified: verdict === null,
|
|
1281
|
+
...verdict !== null ? { reason: verdict } : {}
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
const parts = [];
|
|
1285
|
+
for (const name of baselineScorerNames) {
|
|
1286
|
+
const s = scorers[name];
|
|
1287
|
+
if (Math.abs(s.delta) > 1e-3) {
|
|
1288
|
+
const direction = s.delta > 0 ? "improves" : "regresses";
|
|
1289
|
+
const sign = s.delta > 0 ? "+" : "";
|
|
1290
|
+
let sigLabel = "";
|
|
1291
|
+
if (s.significant === true) sigLabel = " (significant)";
|
|
1292
|
+
else if (s.significant === false) sigLabel = " (not significant)";
|
|
1293
|
+
parts.push(
|
|
1294
|
+
`'${name}' ${direction} by ${sign}${s.deltaPercent.toFixed(1)}% (${s.baselineMean.toFixed(2)} -> ${s.candidateMean.toFixed(2)})${sigLabel}`
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (timing && Math.abs(timing.deltaPercent) > 1) {
|
|
1299
|
+
const dir = timing.delta > 0 ? "slower" : "faster";
|
|
1300
|
+
parts.push(`${Math.abs(timing.deltaPercent).toFixed(0)}% ${dir}`);
|
|
1301
|
+
}
|
|
1302
|
+
if (cost?.certified && cost.deltaPercent !== null && Math.abs(cost.deltaPercent) > 1) {
|
|
1303
|
+
const dir = cost.delta > 0 ? "more expensive" : "cheaper";
|
|
1304
|
+
parts.push(`${Math.abs(cost.deltaPercent).toFixed(0)}% ${dir}`);
|
|
1305
|
+
}
|
|
1306
|
+
const summaryStr = `candidate ${parts.length > 0 ? parts.join(", ") : "no meaningful changes"} with ${regressions.length} regressions and ${improvements.length} improvements`;
|
|
1307
|
+
const baselinePartial = detectPartial(baselineRunsRaw);
|
|
1308
|
+
const candidatePartial = detectPartial(candidateRunsRaw);
|
|
1309
|
+
return {
|
|
1310
|
+
baseline: {
|
|
1311
|
+
id: baselineRef.id,
|
|
1312
|
+
metadata: baselineRef.metadata,
|
|
1313
|
+
runCount,
|
|
1314
|
+
...baselinePartial ? { partial: baselinePartial } : {}
|
|
1315
|
+
},
|
|
1316
|
+
candidate: {
|
|
1317
|
+
id: candidateRef.id,
|
|
1318
|
+
metadata: candidateRef.metadata,
|
|
1319
|
+
runCount,
|
|
1320
|
+
...candidatePartial ? { partial: candidatePartial } : {}
|
|
1321
|
+
},
|
|
1322
|
+
scorers,
|
|
1323
|
+
timing,
|
|
1324
|
+
cost,
|
|
1325
|
+
regressions,
|
|
1326
|
+
improvements,
|
|
1327
|
+
summary: summaryStr
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
function detectPartial(runs) {
|
|
1331
|
+
if (runs.length === 0) return void 0;
|
|
1332
|
+
let attempted;
|
|
1333
|
+
for (const r of runs) {
|
|
1334
|
+
const v = r.metadata?.batchAttempted;
|
|
1335
|
+
if (typeof v === "number" && Number.isFinite(v)) {
|
|
1336
|
+
attempted = v;
|
|
1337
|
+
break;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
if (attempted === void 0) return void 0;
|
|
1341
|
+
if (runs.length >= attempted) return void 0;
|
|
1342
|
+
return { completed: runs.length, attempted };
|
|
1343
|
+
}
|
|
1344
|
+
function evaluateScorerErrorRateGate(comparison, maxScorerErrorRate) {
|
|
1345
|
+
const scorerTypes = comparison.baseline.metadata?.scorerTypes ?? {};
|
|
1346
|
+
for (const name of Object.keys(comparison.scorers)) {
|
|
1347
|
+
const s = comparison.scorers[name];
|
|
1348
|
+
const type = scorerTypes[name] === "deterministic" ? "deterministic" : "llm";
|
|
1349
|
+
const limit = type === "deterministic" ? 0 : maxScorerErrorRate;
|
|
1350
|
+
for (const side of ["baseline", "candidate"]) {
|
|
1351
|
+
const scored = (side === "baseline" ? s.baselineScored : s.candidateScored) ?? 0;
|
|
1352
|
+
const failed = (side === "baseline" ? s.baselineFailed : s.candidateFailed) ?? 0;
|
|
1353
|
+
const skipped = (side === "baseline" ? s.baselineSkipped : s.candidateSkipped) ?? 0;
|
|
1354
|
+
const verdict = evaluateScorerTolerance(scored, failed, type, limit);
|
|
1355
|
+
if (verdict.zeroSample) {
|
|
1356
|
+
const cause = skipped > 0 ? `every one of its items was N/A (the \`applies\` predicate skipped all ${skipped})` : `its items errored or produced no score`;
|
|
1357
|
+
return `scorer "${name}" has no scored items on ${side} (${cause}) \u2014 cannot certify a mean over an empty sample.`;
|
|
1358
|
+
}
|
|
1359
|
+
if (verdict.exceeds) {
|
|
1360
|
+
const limitStr = type === "deterministic" ? "deterministic zero-tolerance" : `${(limit * 100).toFixed(1)}%`;
|
|
1361
|
+
return `scorer "${name}" failed ${(verdict.rate * 100).toFixed(1)}% on ${side} (${failed}/${verdict.attempted}), over the ${limitStr} limit.`;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
return null;
|
|
1366
|
+
}
|
|
1367
|
+
function compareItemErrorRates(baseline, candidate, maxItemErrorRate = DEFAULT_ITEM_ERROR_RATE_LIMIT) {
|
|
1368
|
+
if (!isErrorRateLimit(maxItemErrorRate)) {
|
|
1369
|
+
throw new Error(
|
|
1370
|
+
`Invalid maxItemErrorRate (${String(maxItemErrorRate)}): expected a number in [0, 1].`
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
const baselineRuns = Array.isArray(baseline) ? baseline : [baseline];
|
|
1374
|
+
const candidateRuns = Array.isArray(candidate) ? candidate : [candidate];
|
|
1375
|
+
const runCount = Math.min(baselineRuns.length, candidateRuns.length);
|
|
1376
|
+
const rates = [];
|
|
1377
|
+
for (const [side, runs] of [
|
|
1378
|
+
["baseline", baselineRuns],
|
|
1379
|
+
["candidate", candidateRuns]
|
|
1380
|
+
]) {
|
|
1381
|
+
for (let r = 0; r < runCount; r++) {
|
|
1382
|
+
const run = runs[r];
|
|
1383
|
+
const coverage = run.summary.coverage?.items;
|
|
1384
|
+
const legacy = coverage === void 0;
|
|
1385
|
+
const verdict = legacy ? evaluateItemErrorRate(
|
|
1386
|
+
buildCoverage(run.items, []).items,
|
|
1387
|
+
run.items.length,
|
|
1388
|
+
maxItemErrorRate
|
|
1389
|
+
) : evaluateItemErrorRate(coverage, run.summary.count, maxItemErrorRate);
|
|
1390
|
+
rates.push({ side, runIndex: r, runId: run.id, legacy, ...verdict });
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
return rates;
|
|
1394
|
+
}
|
|
1395
|
+
function describeItemErrorRate(rate, multiRun) {
|
|
1396
|
+
const which = multiRun ? ` run ${rate.runIndex + 1} (${rate.runId})` : ` (${rate.runId})`;
|
|
1397
|
+
const legacy = rate.legacy ? ", derived from the items of a pre-0.24 artifact" : "";
|
|
1398
|
+
return `${rate.side}${which} item error rate ${formatPercent(rate.rate)} (${rate.failed}/${rate.attempted} attempted items failed${legacy})`;
|
|
1399
|
+
}
|
|
1400
|
+
function evaluateItemErrorRateGate(baseline, candidate, maxItemErrorRate = DEFAULT_ITEM_ERROR_RATE_LIMIT) {
|
|
1401
|
+
const rates = compareItemErrorRates(baseline, candidate, maxItemErrorRate);
|
|
1402
|
+
const multiRun = rates.some((r) => r.runIndex > 0);
|
|
1403
|
+
const over = rates.find((r) => r.exceeded);
|
|
1404
|
+
if (!over) return null;
|
|
1405
|
+
return `coverage: ${describeItemErrorRate(over, multiRun)} exceeds the ${formatPercent(maxItemErrorRate)} limit \u2014 its scores cover only the surviving items. Pass --max-item-error-rate <0..1> to accept a thinned side (1 disables the check).`;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// src/rescore.ts
|
|
1409
|
+
import { AdmissionController as AdmissionController2, RequestCaptureChannel as RequestCaptureChannel2 } from "@axlsdk/axl";
|
|
1410
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1411
|
+
var COPY_SHARE_OF_RUN_BOUND = 0.75;
|
|
1412
|
+
function describeFailure(error) {
|
|
1413
|
+
return error instanceof Error ? error.message : String(error);
|
|
1414
|
+
}
|
|
1415
|
+
function isDiagnosticsUnavailable(error) {
|
|
1416
|
+
return error?.code === "DIAGNOSTICS_UNAVAILABLE";
|
|
1417
|
+
}
|
|
1418
|
+
async function beginCapture(source, ownerId, runtime, options) {
|
|
1419
|
+
const limits = resolveCaptureLimits(options?.captureRequests);
|
|
1420
|
+
if (!limits) return {};
|
|
1421
|
+
const open = (artifactId, sink, carriedBytes) => ({
|
|
1422
|
+
artifactId,
|
|
1423
|
+
channel: new RequestCaptureChannel2({
|
|
1424
|
+
sink,
|
|
1425
|
+
...limits,
|
|
1426
|
+
carriedBytes,
|
|
1427
|
+
// Redaction is the RUNTIME's policy, not the caller's: a rescore cannot
|
|
1428
|
+
// opt out of compliance mode by asking for diagnostics.
|
|
1429
|
+
redact: runtime.isRedactEnabled()
|
|
1430
|
+
})
|
|
1431
|
+
});
|
|
1432
|
+
const sourceId = source.diagnostics?.artifactId;
|
|
1433
|
+
let stagedId;
|
|
1434
|
+
try {
|
|
1435
|
+
if (!sourceId) {
|
|
1436
|
+
const staged = await runtime.stageDiagnosticArtifact({ kind: "eval", id: ownerId });
|
|
1437
|
+
stagedId = staged.artifactId;
|
|
1438
|
+
return { capture: { ...open(staged.artifactId, staged.sink, 0), copied: false } };
|
|
1439
|
+
}
|
|
1440
|
+
const runBound = limits.maxRunBytes ?? DEFAULT_COPY_MAX_BYTES;
|
|
1441
|
+
const maxBytes = Math.max(1, Math.floor(runBound * COPY_SHARE_OF_RUN_BOUND));
|
|
1442
|
+
const copied = await runtime.copyDiagnosticArtifact(
|
|
1443
|
+
sourceId,
|
|
1444
|
+
{ kind: "eval", id: ownerId },
|
|
1445
|
+
{ maxBytes }
|
|
1446
|
+
);
|
|
1447
|
+
if (!copied) {
|
|
1448
|
+
return {
|
|
1449
|
+
degraded: unavailableManifest(
|
|
1450
|
+
"",
|
|
1451
|
+
"the source run's captured requests are no longer available"
|
|
1452
|
+
)
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
stagedId = copied.artifactId;
|
|
1456
|
+
const capture = {
|
|
1457
|
+
...open(copied.artifactId, copied.sink, copied.bytes),
|
|
1458
|
+
copied: true,
|
|
1459
|
+
carriedRedaction: copied.redaction
|
|
1460
|
+
};
|
|
1461
|
+
if (copied.truncated) {
|
|
1462
|
+
capture.copyTruncated = `the copied source records stopped at ${maxBytes} bytes, the share of the ${runBound} byte run limit reserved for them`;
|
|
1463
|
+
}
|
|
1464
|
+
return { capture };
|
|
1465
|
+
} catch (error) {
|
|
1466
|
+
if (stagedId !== void 0) {
|
|
1467
|
+
await runtime.rollbackDiagnosticArtifact(stagedId).catch(() => void 0);
|
|
1468
|
+
}
|
|
1469
|
+
if (isDiagnosticsUnavailable(error)) throw error;
|
|
1470
|
+
return {
|
|
1471
|
+
degraded: unavailableManifest(
|
|
1472
|
+
"",
|
|
1473
|
+
`the captured requests could not be carried forward: ${describeFailure(error)}`
|
|
1474
|
+
)
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
async function abandonCapture2(runtime, capture) {
|
|
1479
|
+
if (!capture) return;
|
|
1480
|
+
await capture.channel.close().catch(() => void 0);
|
|
1481
|
+
await runtime.rollbackDiagnosticArtifact(capture.artifactId).catch(() => void 0);
|
|
1482
|
+
}
|
|
1483
|
+
async function finishCapture2(runtime, capture) {
|
|
1484
|
+
const status = await capture.channel.close();
|
|
1485
|
+
const reason = [capture.copyTruncated, status.reason].filter(Boolean).join("; ") || void 0;
|
|
1486
|
+
const truncated = capture.copyTruncated !== void 0 || status.status === "truncated";
|
|
1487
|
+
const finalStatus = status.status === "unavailable" ? "unavailable" : truncated ? "truncated" : status.status;
|
|
1488
|
+
const redaction = status.redaction === "applied" && (capture.carriedRedaction ?? "applied") === "applied" ? "applied" : "none";
|
|
1489
|
+
try {
|
|
1490
|
+
const manifest = await runtime.finalizeDiagnosticArtifact(
|
|
1491
|
+
capture.artifactId,
|
|
1492
|
+
finalStatus,
|
|
1493
|
+
reason,
|
|
1494
|
+
redaction
|
|
1495
|
+
);
|
|
1496
|
+
return toDiagnosticManifest(manifest);
|
|
1497
|
+
} catch {
|
|
1498
|
+
return unavailableManifest("", "the diagnostic artifact could not be finalized");
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
async function rescore(result, scorers, runtime, options) {
|
|
1502
|
+
const startTime = Date.now();
|
|
1503
|
+
const rescoredId = randomUUID2();
|
|
1504
|
+
const concurrency = options?.concurrency ?? 5;
|
|
1505
|
+
const scorerConcurrency = options?.scorerConcurrency ?? 5;
|
|
1506
|
+
const admission = options?.budget != null ? new AdmissionController2({ limit: parseBudget(options.budget) }) : void 0;
|
|
1507
|
+
let closedBy;
|
|
1508
|
+
const noteClosure = (source) => {
|
|
1509
|
+
if (closedBy === void 0 && admission?.closed) closedBy = source;
|
|
1510
|
+
};
|
|
1511
|
+
const scorerContext = {
|
|
1512
|
+
resolveProvider: (uri) => {
|
|
1513
|
+
if (typeof runtime.resolveProvider !== "function") {
|
|
1514
|
+
throw new Error(
|
|
1515
|
+
`LLM scorers require a runtime with resolveProvider(). Ensure you are using a real AxlRuntime instance, not a mock.`
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
return runtime.resolveProvider(uri);
|
|
1519
|
+
},
|
|
1520
|
+
signal: options?.signal
|
|
1521
|
+
};
|
|
1522
|
+
const rescored = new Array(result.items.length);
|
|
1523
|
+
const { capture, degraded } = await beginCapture(result, rescoredId, runtime, options);
|
|
1524
|
+
const carryDiagnostics = capture?.copied === true;
|
|
1525
|
+
async function rescoreItem(original, itemIndex) {
|
|
1526
|
+
if (options?.signal?.aborted) {
|
|
1527
|
+
rescored[itemIndex] = {
|
|
1528
|
+
input: original.input,
|
|
1529
|
+
annotations: original.annotations,
|
|
1530
|
+
output: original.output,
|
|
1531
|
+
error: "cancelled",
|
|
1532
|
+
outcome: "cancelled",
|
|
1533
|
+
accounting: emptyAccounting(),
|
|
1534
|
+
scores: {}
|
|
1535
|
+
};
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
if (original.error) {
|
|
1539
|
+
const accounting2 = emptyAccounting();
|
|
1540
|
+
rescored[itemIndex] = {
|
|
1541
|
+
input: original.input,
|
|
1542
|
+
annotations: original.annotations,
|
|
1543
|
+
output: original.output,
|
|
1544
|
+
error: original.error,
|
|
1545
|
+
...original.outcome ? { outcome: original.outcome } : {},
|
|
1546
|
+
// The failure cause is a fact about the source run too.
|
|
1547
|
+
...original.failure ? { failure: original.failure } : {},
|
|
1548
|
+
accounting: accounting2,
|
|
1549
|
+
// Same rule as a scored item: an item carrying `accounting` always
|
|
1550
|
+
// carries both compat views, so no reader falls through to the legacy
|
|
1551
|
+
// "no accounting → trust the caller's number" branch and reports the
|
|
1552
|
+
// SOURCE run's spend as this rescore's. A passthrough did no work, so
|
|
1553
|
+
// both are a measured $0; the source's spend lives in the run-level
|
|
1554
|
+
// `accounting.source` provenance and nowhere else.
|
|
1555
|
+
cost: accounting2.breakdown.generation,
|
|
1556
|
+
scorerCost: accounting2.breakdown.judging,
|
|
1557
|
+
scores: {}
|
|
1558
|
+
};
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
const item = {
|
|
1562
|
+
input: original.input,
|
|
1563
|
+
annotations: original.annotations,
|
|
1564
|
+
output: original.output,
|
|
1565
|
+
metadata: original.metadata,
|
|
1566
|
+
traces: original.traces,
|
|
1567
|
+
outcome: "completed",
|
|
1568
|
+
// Carried forward VERBATIM, original operation ids included. The rescore
|
|
1569
|
+
// did not re-run the workflow, so these still describe the calls that
|
|
1570
|
+
// produced this output; reminting the ids would break the only link back
|
|
1571
|
+
// to the run that made them. Only carried when the source records were
|
|
1572
|
+
// actually copied into this rescore's artifact — a dangling reference is
|
|
1573
|
+
// worse than none.
|
|
1574
|
+
...carryDiagnostics && original.diagnostics ? { diagnostics: original.diagnostics } : {},
|
|
1575
|
+
scores: {},
|
|
1576
|
+
scoreDetails: {}
|
|
1577
|
+
};
|
|
1578
|
+
const itemOutcome = await trackScope(
|
|
1579
|
+
runtime,
|
|
1580
|
+
async () => {
|
|
1581
|
+
await scoreItem(item, scorers, {
|
|
1582
|
+
runtime,
|
|
1583
|
+
scorerContext,
|
|
1584
|
+
scorerConcurrency,
|
|
1585
|
+
signal: options?.signal,
|
|
1586
|
+
admission,
|
|
1587
|
+
onClosure: noteClosure
|
|
1588
|
+
});
|
|
1589
|
+
},
|
|
1590
|
+
{
|
|
1591
|
+
purpose: "judging",
|
|
1592
|
+
// Every record produced under this item is stamped with the case index,
|
|
1593
|
+
// which is how a `ScorerDetail` can point at its own judge calls.
|
|
1594
|
+
...capture ? { captureCorrelation: { caseIndex: itemIndex } } : {}
|
|
1595
|
+
}
|
|
1596
|
+
);
|
|
1597
|
+
if (itemOutcome.status === "rejected") throw itemOutcome.error;
|
|
1598
|
+
item.accounting = itemOutcome.accounting;
|
|
1599
|
+
item.cost = itemOutcome.accounting.breakdown.generation;
|
|
1600
|
+
item.scorerCost = itemOutcome.accounting.breakdown.judging;
|
|
1601
|
+
if (itemOutcome.accounting.completeness !== "complete") item.unpriced = true;
|
|
1602
|
+
rescored[itemIndex] = item;
|
|
1603
|
+
}
|
|
1604
|
+
const runOutcome = await trackScope(
|
|
1605
|
+
runtime,
|
|
1606
|
+
async () => {
|
|
1607
|
+
await mapWithConcurrency(result.items, concurrency, (item, i) => rescoreItem(item, i));
|
|
1608
|
+
},
|
|
1609
|
+
// The channel is declared ONCE, at the run scope: its byte budget and
|
|
1610
|
+
// pending queue are per rescore, and every nested item/scorer scope inherits
|
|
1611
|
+
// it rather than opening a competing budget of its own.
|
|
1612
|
+
{ purpose: "judging", admission, ...capture ? { capture: capture.channel } : {} }
|
|
1613
|
+
);
|
|
1614
|
+
if (runOutcome.status === "rejected") {
|
|
1615
|
+
await abandonCapture2(runtime, capture);
|
|
1616
|
+
throw runOutcome.error;
|
|
1617
|
+
}
|
|
1618
|
+
const sourceAccounting = result.accounting;
|
|
1619
|
+
const generation = sourceAccounting?.scope === "rescore" ? sourceAccounting.source?.generation ?? null : sourceAccounting ?? null;
|
|
1620
|
+
let diagnostics = degraded;
|
|
1621
|
+
if (capture) {
|
|
1622
|
+
try {
|
|
1623
|
+
attachOperationRefs(rescored, capture.channel);
|
|
1624
|
+
} catch (error) {
|
|
1625
|
+
await abandonCapture2(runtime, capture);
|
|
1626
|
+
throw error;
|
|
1627
|
+
}
|
|
1628
|
+
diagnostics = await finishCapture2(runtime, capture);
|
|
1629
|
+
}
|
|
1630
|
+
const accounting = {
|
|
1631
|
+
...runOutcome.accounting,
|
|
1632
|
+
scope: "rescore",
|
|
1633
|
+
// `?? null` is load-bearing: a legacy source has no accounting and must read
|
|
1634
|
+
// as "unknown generation", never as a synthesized complete record.
|
|
1635
|
+
source: { runId: result.id, generation },
|
|
1636
|
+
...admission ? { budget: { ...admission.snapshot(), ...closedBy ? { closedBy } : {} } } : {}
|
|
1637
|
+
};
|
|
1638
|
+
const failures = rescored.filter((i) => i.error).length;
|
|
1639
|
+
const scorerNames = scorers.map((s) => s.name);
|
|
1640
|
+
const scorerStats = {};
|
|
1641
|
+
for (const name of scorerNames) {
|
|
1642
|
+
const scores = rescored.filter((i) => !i.error && i.scores[name] != null).map((i) => i.scores[name]);
|
|
1643
|
+
const { scored, failed, skipped } = scorerCounts(rescored, name);
|
|
1644
|
+
scorerStats[name] = { ...computeStats(scores), scored, failed, skipped };
|
|
1645
|
+
}
|
|
1646
|
+
const scorerTypes = {};
|
|
1647
|
+
for (const s of scorers) {
|
|
1648
|
+
scorerTypes[s.name] = s.isLlm ? "llm" : "deterministic";
|
|
1649
|
+
}
|
|
1650
|
+
return {
|
|
1651
|
+
id: rescoredId,
|
|
1652
|
+
dataset: result.dataset,
|
|
1653
|
+
metadata: (() => {
|
|
1654
|
+
const rest = { ...result.metadata };
|
|
1655
|
+
delete rest.runGroupId;
|
|
1656
|
+
delete rest.runIndex;
|
|
1657
|
+
delete rest.scorerFiltered;
|
|
1658
|
+
delete rest.scorersRun;
|
|
1659
|
+
const merged = {
|
|
1660
|
+
...rest,
|
|
1661
|
+
rescored: true,
|
|
1662
|
+
originalId: result.id,
|
|
1663
|
+
scorerTypes
|
|
1664
|
+
};
|
|
1665
|
+
if (!Array.isArray(merged.workflows)) {
|
|
1666
|
+
const legacyWorkflow = result.workflow;
|
|
1667
|
+
if (typeof legacyWorkflow === "string" && legacyWorkflow) {
|
|
1668
|
+
merged.workflows = [legacyWorkflow];
|
|
1669
|
+
merged.workflowCounts = { [legacyWorkflow]: result.items.length };
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
return merged;
|
|
1673
|
+
})(),
|
|
1674
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1675
|
+
totalCost: accounting.knownCost,
|
|
1676
|
+
...accounting.completeness !== "complete" ? { unpriced: true } : {},
|
|
1677
|
+
accounting,
|
|
1678
|
+
...diagnostics ? { diagnostics } : {},
|
|
1679
|
+
duration: Date.now() - startTime,
|
|
1680
|
+
items: rescored,
|
|
1681
|
+
summary: {
|
|
1682
|
+
count: result.items.length,
|
|
1683
|
+
failures,
|
|
1684
|
+
coverage: buildCoverage(rescored, scorerNames),
|
|
1685
|
+
scorers: scorerStats,
|
|
1686
|
+
// A rescore makes no generation calls, so the source run's wall-clock
|
|
1687
|
+
// and provider latency are still the only measurements of these items.
|
|
1688
|
+
// `modelTiming` cannot be rebuilt from `item.timing` (sums, not per-call
|
|
1689
|
+
// samples): carry both forward.
|
|
1690
|
+
...result.summary.timing ? { timing: { ...result.summary.timing } } : {},
|
|
1691
|
+
...result.summary.modelTiming ? { modelTiming: structuredClone(result.summary.modelTiming) } : {}
|
|
1692
|
+
}
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
// src/multi-run.ts
|
|
1697
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1698
|
+
function std(values, mean) {
|
|
1699
|
+
if (values.length <= 1) return 0;
|
|
1700
|
+
const sumSqDiffs = values.reduce((sum, v) => sum + (v - mean) ** 2, 0);
|
|
1701
|
+
return Math.sqrt(sumSqDiffs / (values.length - 1));
|
|
1702
|
+
}
|
|
1703
|
+
function aggregateRuns(runs) {
|
|
1704
|
+
if (runs.length === 0) throw new Error("Cannot aggregate zero runs");
|
|
1705
|
+
const runGroupId = runs[0].metadata.runGroupId ?? randomUUID3();
|
|
1706
|
+
const { dataset } = runs[0];
|
|
1707
|
+
const seenWorkflows = /* @__PURE__ */ new Set();
|
|
1708
|
+
const workflows = [];
|
|
1709
|
+
for (const run of runs) {
|
|
1710
|
+
const list = run.metadata.workflows;
|
|
1711
|
+
if (Array.isArray(list)) {
|
|
1712
|
+
for (const w of list) {
|
|
1713
|
+
if (typeof w === "string" && !seenWorkflows.has(w)) {
|
|
1714
|
+
seenWorkflows.add(w);
|
|
1715
|
+
workflows.push(w);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
const totalCost = runs.reduce((sum, r) => sum + r.totalCost, 0);
|
|
1721
|
+
const totalDuration = runs.reduce((sum, r) => sum + r.duration, 0);
|
|
1722
|
+
const scorerNames = Object.keys(runs[0].summary.scorers);
|
|
1723
|
+
const scorers = {};
|
|
1724
|
+
for (const name of scorerNames) {
|
|
1725
|
+
const contributing = runs.filter((r) => {
|
|
1726
|
+
const sc = r.summary.scorers[name]?.scored;
|
|
1727
|
+
return sc == null || sc > 0;
|
|
1728
|
+
});
|
|
1729
|
+
const meanSource = contributing.length > 0 ? contributing : runs;
|
|
1730
|
+
const means = meanSource.map((r) => r.summary.scorers[name]?.mean ?? 0);
|
|
1731
|
+
const meanOfMeans = means.reduce((a, b) => a + b, 0) / means.length;
|
|
1732
|
+
const minMean = Math.min(...means);
|
|
1733
|
+
const maxMean = Math.max(...means);
|
|
1734
|
+
const scored = runs.reduce((sum, r) => sum + (r.summary.scorers[name]?.scored ?? 0), 0);
|
|
1735
|
+
const failed = runs.reduce((sum, r) => sum + (r.summary.scorers[name]?.failed ?? 0), 0);
|
|
1736
|
+
const skipped = runs.reduce((sum, r) => sum + (r.summary.scorers[name]?.skipped ?? 0), 0);
|
|
1737
|
+
scorers[name] = {
|
|
1738
|
+
mean: round2(meanOfMeans),
|
|
1739
|
+
std: round2(std(means, meanOfMeans)),
|
|
1740
|
+
min: round2(minMean),
|
|
1741
|
+
max: round2(maxMean),
|
|
1742
|
+
scored,
|
|
1743
|
+
failed,
|
|
1744
|
+
skipped
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
let timing;
|
|
1748
|
+
const timingMeans = runs.filter((r) => r.summary.timing).map((r) => r.summary.timing.mean);
|
|
1749
|
+
if (timingMeans.length === runs.length && timingMeans.length > 0) {
|
|
1750
|
+
const meanTiming = timingMeans.reduce((a, b) => a + b, 0) / timingMeans.length;
|
|
1751
|
+
timing = {
|
|
1752
|
+
mean: round2(meanTiming),
|
|
1753
|
+
std: round2(std(timingMeans, meanTiming))
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
return {
|
|
1757
|
+
runGroupId,
|
|
1758
|
+
runCount: runs.length,
|
|
1759
|
+
workflows,
|
|
1760
|
+
dataset,
|
|
1761
|
+
totalCost: round2(totalCost),
|
|
1762
|
+
accounting: aggregateAccounting(runs.map((r) => readAccounting(r))),
|
|
1763
|
+
totalDuration: round2(totalDuration),
|
|
1764
|
+
scorers,
|
|
1765
|
+
timing
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
export {
|
|
1770
|
+
pairedBootstrapCI,
|
|
1771
|
+
formatPercent,
|
|
1772
|
+
isErrorRateLimit,
|
|
1773
|
+
scorerCounts,
|
|
1774
|
+
evaluateScorerTolerance,
|
|
1775
|
+
readAccounting,
|
|
1776
|
+
refusedWork,
|
|
1777
|
+
isBudgetStopped,
|
|
1778
|
+
aggregateAccounting,
|
|
1779
|
+
normalizeScorerResult,
|
|
1780
|
+
attachScorerErrorCost,
|
|
1781
|
+
scorer,
|
|
1782
|
+
DEFAULT_COPY_MAX_BYTES,
|
|
1783
|
+
resolveCaptureLimits,
|
|
1784
|
+
toDiagnosticManifest,
|
|
1785
|
+
unavailableManifest,
|
|
1786
|
+
DEFAULT_SIDECAR_MAX_BYTES,
|
|
1787
|
+
validateRequestSidecar,
|
|
1788
|
+
parseRequestRecords,
|
|
1789
|
+
serializeRequestRecords,
|
|
1790
|
+
runEval,
|
|
1791
|
+
evalCompare,
|
|
1792
|
+
evaluateScorerErrorRateGate,
|
|
1793
|
+
compareItemErrorRates,
|
|
1794
|
+
describeItemErrorRate,
|
|
1795
|
+
evaluateItemErrorRateGate,
|
|
1796
|
+
rescore,
|
|
1797
|
+
aggregateRuns
|
|
1798
|
+
};
|
|
1799
|
+
//# sourceMappingURL=chunk-HKEAPM4B.js.map
|