@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
package/dist/index.cjs
CHANGED
|
@@ -30,18 +30,31 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
DEFAULT_COPY_MAX_BYTES: () => DEFAULT_COPY_MAX_BYTES,
|
|
34
|
+
DEFAULT_SIDECAR_MAX_BYTES: () => DEFAULT_SIDECAR_MAX_BYTES,
|
|
35
|
+
aggregateAccounting: () => aggregateAccounting,
|
|
33
36
|
aggregateRuns: () => aggregateRuns,
|
|
34
37
|
dataset: () => dataset,
|
|
35
38
|
defineEval: () => defineEval,
|
|
36
39
|
evalCompare: () => evalCompare,
|
|
40
|
+
evaluateItemErrorRateGate: () => evaluateItemErrorRateGate,
|
|
37
41
|
evaluateScorerErrorRateGate: () => evaluateScorerErrorRateGate,
|
|
38
42
|
evaluateScorerTolerance: () => evaluateScorerTolerance,
|
|
43
|
+
isBudgetStopped: () => isBudgetStopped,
|
|
39
44
|
llmScorer: () => llmScorer,
|
|
40
45
|
normalizeScorerResult: () => normalizeScorerResult,
|
|
41
46
|
pairedBootstrapCI: () => pairedBootstrapCI,
|
|
47
|
+
parseRequestRecords: () => parseRequestRecords,
|
|
48
|
+
readAccounting: () => readAccounting,
|
|
49
|
+
refusedWork: () => refusedWork,
|
|
42
50
|
rescore: () => rescore,
|
|
51
|
+
resolveCaptureLimits: () => resolveCaptureLimits,
|
|
43
52
|
runEval: () => runEval,
|
|
44
|
-
scorer: () => scorer
|
|
53
|
+
scorer: () => scorer,
|
|
54
|
+
serializeRequestRecords: () => serializeRequestRecords,
|
|
55
|
+
toDiagnosticManifest: () => toDiagnosticManifest,
|
|
56
|
+
unavailableManifest: () => unavailableManifest,
|
|
57
|
+
validateRequestSidecar: () => validateRequestSidecar
|
|
45
58
|
});
|
|
46
59
|
module.exports = __toCommonJS(index_exports);
|
|
47
60
|
|
|
@@ -249,6 +262,9 @@ function defineEval(config) {
|
|
|
249
262
|
return config;
|
|
250
263
|
}
|
|
251
264
|
|
|
265
|
+
// src/runner.ts
|
|
266
|
+
var import_axl3 = require("@axlsdk/axl");
|
|
267
|
+
|
|
252
268
|
// src/utils.ts
|
|
253
269
|
function computeStats(scores) {
|
|
254
270
|
if (scores.length === 0) return { mean: 0, min: 0, max: 0, p50: 0, p95: 0 };
|
|
@@ -263,15 +279,35 @@ function computeStats(scores) {
|
|
|
263
279
|
function round(n) {
|
|
264
280
|
return Math.round(n * 1e3) / 1e3;
|
|
265
281
|
}
|
|
282
|
+
function formatPercent(rate) {
|
|
283
|
+
return `${Number((rate * 100).toFixed(1))}%`;
|
|
284
|
+
}
|
|
285
|
+
var DEFAULT_ITEM_ERROR_RATE_LIMIT = 0.05;
|
|
286
|
+
function isErrorRateLimit(value) {
|
|
287
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
288
|
+
}
|
|
289
|
+
function evaluateItemErrorRate(items, count, limit) {
|
|
290
|
+
const failed = items.failed;
|
|
291
|
+
const attempted = count - items.cancelled - items.budget_skipped - items.budget_interrupted;
|
|
292
|
+
const rate = attempted > 0 ? failed / attempted : 0;
|
|
293
|
+
return { failed, attempted, rate, limit, exceeded: attempted > 0 && rate > limit };
|
|
294
|
+
}
|
|
266
295
|
function scorerCounts(items, name) {
|
|
267
296
|
let scored = 0;
|
|
268
297
|
let failed = 0;
|
|
269
298
|
let skipped = 0;
|
|
270
299
|
for (const i of items) {
|
|
271
300
|
if (i.error) continue;
|
|
301
|
+
const detail = i.scoreDetails?.[name];
|
|
302
|
+
if (detail?.outcome !== void 0) {
|
|
303
|
+
if (detail.outcome === "scored") scored++;
|
|
304
|
+
else if (detail.outcome === "failed") failed++;
|
|
305
|
+
else if (detail.outcome === "skipped") skipped++;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
272
308
|
if (i.scores[name] != null) scored++;
|
|
273
|
-
else if (
|
|
274
|
-
else if (
|
|
309
|
+
else if (detail?.skipped === true) skipped++;
|
|
310
|
+
else if (detail?.duration != null) failed++;
|
|
275
311
|
}
|
|
276
312
|
return { scored, failed, skipped };
|
|
277
313
|
}
|
|
@@ -296,81 +332,380 @@ async function mapWithConcurrency(items, concurrency, task) {
|
|
|
296
332
|
return results;
|
|
297
333
|
}
|
|
298
334
|
|
|
335
|
+
// src/accounting.ts
|
|
336
|
+
var import_axl2 = require("@axlsdk/axl");
|
|
337
|
+
var USAGE_KEYS = [
|
|
338
|
+
"inputTokens",
|
|
339
|
+
"outputTokens",
|
|
340
|
+
"reasoningTokens",
|
|
341
|
+
"cachedTokens",
|
|
342
|
+
"cacheWriteTokens",
|
|
343
|
+
"audioSeconds"
|
|
344
|
+
];
|
|
345
|
+
function zeroUsage() {
|
|
346
|
+
return {
|
|
347
|
+
inputTokens: 0,
|
|
348
|
+
outputTokens: 0,
|
|
349
|
+
reasoningTokens: 0,
|
|
350
|
+
cachedTokens: 0,
|
|
351
|
+
cacheWriteTokens: 0,
|
|
352
|
+
audioSeconds: 0
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function emptyAccounting() {
|
|
356
|
+
return {
|
|
357
|
+
version: 1,
|
|
358
|
+
currency: "USD",
|
|
359
|
+
knownCost: 0,
|
|
360
|
+
completeness: "complete",
|
|
361
|
+
reasons: {},
|
|
362
|
+
usage: zeroUsage(),
|
|
363
|
+
operations: { total: 0, settled: 0, unknown: 0, denied: 0, byKind: {} },
|
|
364
|
+
breakdown: { generation: 0, judging: 0, external: 0 },
|
|
365
|
+
provenance: {}
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function uninstrumentedAccounting() {
|
|
369
|
+
return { ...emptyAccounting(), completeness: "incomplete", reasons: { uninstrumented: 1 } };
|
|
370
|
+
}
|
|
371
|
+
function isInstrumented(runtime) {
|
|
372
|
+
return typeof runtime?.trackOutcome === "function";
|
|
373
|
+
}
|
|
374
|
+
async function trackScope(runtime, fn, options) {
|
|
375
|
+
if (!isInstrumented(runtime)) {
|
|
376
|
+
try {
|
|
377
|
+
return {
|
|
378
|
+
status: "fulfilled",
|
|
379
|
+
value: await fn(),
|
|
380
|
+
accounting: uninstrumentedAccounting(),
|
|
381
|
+
instrumented: false
|
|
382
|
+
};
|
|
383
|
+
} catch (error) {
|
|
384
|
+
return {
|
|
385
|
+
status: "rejected",
|
|
386
|
+
error,
|
|
387
|
+
accounting: uninstrumentedAccounting(),
|
|
388
|
+
instrumented: false
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const outcome = await runtime.trackOutcome(fn, options);
|
|
393
|
+
return { ...outcome, instrumented: true };
|
|
394
|
+
}
|
|
395
|
+
function parseBudget(budget) {
|
|
396
|
+
const match = typeof budget === "string" ? budget.match(/^\$?(\d+(?:\.\d+)?)$/) : null;
|
|
397
|
+
if (!match) {
|
|
398
|
+
throw new import_axl2.AxlError(
|
|
399
|
+
"INVALID_BUDGET",
|
|
400
|
+
`Invalid budget "${String(budget)}": expected a non-negative USD amount like "$1" or "0.50".`
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
const limit = Number(match[1]);
|
|
404
|
+
if (!Number.isFinite(limit)) {
|
|
405
|
+
throw new import_axl2.AxlError("INVALID_BUDGET", `Invalid budget "${budget}": not a finite amount.`);
|
|
406
|
+
}
|
|
407
|
+
return limit;
|
|
408
|
+
}
|
|
409
|
+
function usableCost(value) {
|
|
410
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
411
|
+
}
|
|
412
|
+
function isAdmissionDenied(err) {
|
|
413
|
+
return (0, import_axl2.isAdmissionDeniedError)(err);
|
|
414
|
+
}
|
|
415
|
+
function readAccounting(result) {
|
|
416
|
+
if (result?.accounting) return result.accounting;
|
|
417
|
+
const rescored = result?.metadata?.rescored === true;
|
|
418
|
+
return {
|
|
419
|
+
...emptyAccounting(),
|
|
420
|
+
knownCost: usableCost(result?.totalCost),
|
|
421
|
+
completeness: "unverified",
|
|
422
|
+
scope: rescored ? "rescore" : "run"
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
function refusedWork(coverage) {
|
|
426
|
+
if (!coverage) return 0;
|
|
427
|
+
return refusedIn(coverage.items) + Object.values(coverage.scorers ?? {}).reduce((n, s) => n + refusedIn(s), 0);
|
|
428
|
+
}
|
|
429
|
+
function refusedIn(counts) {
|
|
430
|
+
return atLeastZero(counts?.budget_skipped) + atLeastZero(counts?.budget_interrupted);
|
|
431
|
+
}
|
|
432
|
+
function atLeastZero(value) {
|
|
433
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
434
|
+
}
|
|
435
|
+
function isBudgetStopped(summary) {
|
|
436
|
+
if (summary.budget?.status !== "closed") return false;
|
|
437
|
+
return refusedWork(summary.coverage) > 0;
|
|
438
|
+
}
|
|
439
|
+
function addUsage(target, source) {
|
|
440
|
+
if (!source) return;
|
|
441
|
+
for (const key of USAGE_KEYS) {
|
|
442
|
+
const value = source[key];
|
|
443
|
+
if (typeof value === "number" && Number.isFinite(value)) target[key] += value;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function addCounts(target, source) {
|
|
447
|
+
if (!source) return;
|
|
448
|
+
for (const [key, count] of Object.entries(source)) {
|
|
449
|
+
if (typeof count === "number" && Number.isFinite(count)) {
|
|
450
|
+
target[key] = (target[key] ?? 0) + count;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function aggregateAccounting(inputs) {
|
|
455
|
+
const out = emptyAccounting();
|
|
456
|
+
let anyUnverified = false;
|
|
457
|
+
let anyIncomplete = false;
|
|
458
|
+
for (const input of inputs) {
|
|
459
|
+
if (!input) continue;
|
|
460
|
+
if (input.completeness === "unverified") anyUnverified = true;
|
|
461
|
+
else if (input.completeness === "incomplete") anyIncomplete = true;
|
|
462
|
+
out.knownCost += usableCost(input.knownCost);
|
|
463
|
+
addUsage(out.usage, input.usage);
|
|
464
|
+
addCounts(out.reasons, input.reasons);
|
|
465
|
+
addCounts(out.operations.byKind, input.operations?.byKind);
|
|
466
|
+
addCounts(out.provenance, input.provenance);
|
|
467
|
+
out.operations.total += input.operations?.total ?? 0;
|
|
468
|
+
out.operations.settled += input.operations?.settled ?? 0;
|
|
469
|
+
out.operations.unknown += input.operations?.unknown ?? 0;
|
|
470
|
+
out.operations.denied += input.operations?.denied ?? 0;
|
|
471
|
+
out.breakdown.generation += usableCost(input.breakdown?.generation);
|
|
472
|
+
out.breakdown.judging += usableCost(input.breakdown?.judging);
|
|
473
|
+
out.breakdown.external += usableCost(input.breakdown?.external);
|
|
474
|
+
}
|
|
475
|
+
out.completeness = anyUnverified ? "unverified" : anyIncomplete ? "incomplete" : "complete";
|
|
476
|
+
return out;
|
|
477
|
+
}
|
|
478
|
+
|
|
299
479
|
// src/score-item.ts
|
|
300
|
-
|
|
480
|
+
function describeInput(input) {
|
|
481
|
+
let rendered;
|
|
482
|
+
try {
|
|
483
|
+
rendered = JSON.stringify(input) ?? String(input);
|
|
484
|
+
} catch {
|
|
485
|
+
rendered = "[unserializable input]";
|
|
486
|
+
}
|
|
487
|
+
return rendered.length > 200 ? `${rendered.slice(0, 200)}\u2026` : rendered;
|
|
488
|
+
}
|
|
489
|
+
async function scoreItem(item, scorers, context) {
|
|
301
490
|
const scoreDetails = item.scoreDetails ??= {};
|
|
302
491
|
for (const s of scorers) {
|
|
303
492
|
item.scores[s.name] = null;
|
|
304
|
-
scoreDetails[s.name] = { score: null };
|
|
493
|
+
scoreDetails[s.name] = { score: null, outcome: "cancelled" };
|
|
305
494
|
}
|
|
306
|
-
|
|
495
|
+
const { runtime, scorerContext, signal, admission } = context;
|
|
307
496
|
const scorerErrorsByName = {};
|
|
308
|
-
await mapWithConcurrency(scorers, scorerConcurrency, async (scorer2) => {
|
|
309
|
-
|
|
497
|
+
await mapWithConcurrency(scorers, context.scorerConcurrency, async (scorer2) => {
|
|
498
|
+
const record = (detail, outcome2) => {
|
|
499
|
+
scoreDetails[scorer2.name] = { ...detail, outcome: outcome2 };
|
|
500
|
+
};
|
|
501
|
+
if (signal?.aborted) {
|
|
502
|
+
record({ score: null }, "cancelled");
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (admission?.closed && scorer2.isLlm) {
|
|
506
|
+
record({ score: null }, "budget_skipped");
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
310
509
|
const scorerStart = Date.now();
|
|
510
|
+
let applicable = true;
|
|
311
511
|
try {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
512
|
+
applicable = !scorer2.applies || scorer2.applies(item.output, item.input, item.annotations);
|
|
513
|
+
} catch (err) {
|
|
514
|
+
scorerErrorsByName[scorer2.name] = `Scorer "${scorer2.name}" threw: ${err instanceof Error ? err.message : String(err)}`;
|
|
515
|
+
record({ score: null, duration: Date.now() - scorerStart }, "failed");
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (!applicable) {
|
|
519
|
+
record({ score: null, skipped: true }, "skipped");
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const outcome = await trackScope(
|
|
523
|
+
runtime,
|
|
524
|
+
async () => scorer2.score(item.output, item.input, item.annotations, scorerContext),
|
|
525
|
+
// The scorer stamp is merged OVER the enclosing item's correlation, so a
|
|
526
|
+
// judge's records keep the case index and add the judge's name -- which is
|
|
527
|
+
// what lets `scoreDetails[name].diagnostics` point at this judge's own
|
|
528
|
+
// calls rather than at the whole item's.
|
|
529
|
+
{ purpose: "judging", captureCorrelation: { scorer: scorer2.name } }
|
|
530
|
+
);
|
|
531
|
+
const duration = Date.now() - scorerStart;
|
|
532
|
+
const { accounting } = outcome;
|
|
533
|
+
const measured = accounting.operations.total > 0 || accounting.operations.denied > 0;
|
|
534
|
+
const withCost = (detail, callerCost) => ({
|
|
535
|
+
...detail,
|
|
536
|
+
...measured ? { accounting, cost: accounting.knownCost } : {},
|
|
537
|
+
...!measured && !outcome.instrumented && callerCost != null ? { cost: callerCost } : {}
|
|
538
|
+
});
|
|
539
|
+
if (outcome.status === "rejected") {
|
|
540
|
+
const err = outcome.error;
|
|
541
|
+
if (err?.name === "AbortError") {
|
|
542
|
+
record(withCost({ score: null, duration }, void 0), "cancelled");
|
|
315
543
|
return;
|
|
316
544
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
545
|
+
if (isAdmissionDenied(err)) {
|
|
546
|
+
context.onClosure?.("operation");
|
|
547
|
+
record(withCost({ score: null, duration }, void 0), "budget_interrupted");
|
|
548
|
+
return;
|
|
321
549
|
}
|
|
322
|
-
|
|
550
|
+
scorerErrorsByName[scorer2.name] = `Scorer "${scorer2.name}" threw: ${err instanceof Error ? err.message : String(err)}`;
|
|
551
|
+
record(withCost({ score: null, duration }, extractScorerErrorCost(err)), "failed");
|
|
552
|
+
if (admission?.closed) context.onClosure?.("scorer");
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
try {
|
|
556
|
+
const scorerResult = normalizeScorerResult(outcome.value);
|
|
557
|
+
const callerCost = typeof scorerResult.cost === "number" && Number.isFinite(scorerResult.cost) && scorerResult.cost >= 0 ? scorerResult.cost : void 0;
|
|
323
558
|
if (!Number.isFinite(scorerResult.score) || scorerResult.score < 0 || scorerResult.score > 1) {
|
|
324
|
-
scorerErrorsByName[scorer2.name] = `Scorer "${scorer2.name}" returned out-of-range score ${scorerResult.score} for input ${
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
duration: scorerDuration,
|
|
330
|
-
cost: scorerResult.cost
|
|
331
|
-
};
|
|
559
|
+
scorerErrorsByName[scorer2.name] = `Scorer "${scorer2.name}" returned out-of-range score ${scorerResult.score} for input ${describeInput(item.input)}`;
|
|
560
|
+
record(
|
|
561
|
+
withCost({ score: null, metadata: scorerResult.metadata, duration }, callerCost),
|
|
562
|
+
"failed"
|
|
563
|
+
);
|
|
332
564
|
} else {
|
|
333
565
|
item.scores[scorer2.name] = round(scorerResult.score);
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
566
|
+
record(
|
|
567
|
+
withCost(
|
|
568
|
+
{
|
|
569
|
+
score: round(scorerResult.score),
|
|
570
|
+
metadata: scorerResult.metadata,
|
|
571
|
+
duration
|
|
572
|
+
},
|
|
573
|
+
callerCost
|
|
574
|
+
),
|
|
575
|
+
"scored"
|
|
576
|
+
);
|
|
340
577
|
}
|
|
341
578
|
} catch (err) {
|
|
342
|
-
if (err?.name === "AbortError") return;
|
|
343
|
-
const errCost = extractScorerErrorCost(err);
|
|
344
|
-
if (errCost != null) itemScorerCost += errCost;
|
|
345
579
|
scorerErrorsByName[scorer2.name] = `Scorer "${scorer2.name}" threw: ${err instanceof Error ? err.message : String(err)}`;
|
|
346
|
-
|
|
347
|
-
scoreDetails[scorer2.name] = {
|
|
348
|
-
score: null,
|
|
349
|
-
duration: Date.now() - scorerStart,
|
|
350
|
-
cost: errCost
|
|
351
|
-
};
|
|
580
|
+
record(withCost({ score: null, duration }, void 0), "failed");
|
|
352
581
|
}
|
|
582
|
+
if (admission?.closed) context.onClosure?.("scorer");
|
|
353
583
|
});
|
|
354
584
|
const orderedErrors = scorers.map((s) => scorerErrorsByName[s.name]).filter((e) => e != null);
|
|
355
585
|
if (orderedErrors.length > 0) item.scorerErrors = orderedErrors;
|
|
356
|
-
|
|
357
|
-
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// src/diagnostics.ts
|
|
589
|
+
var DEFAULT_COPY_MAX_BYTES = 16 * 1024 * 1024;
|
|
590
|
+
function resolveCaptureLimits(option) {
|
|
591
|
+
if (option === void 0 || option === false) return void 0;
|
|
592
|
+
if (option === true) return {};
|
|
593
|
+
return {
|
|
594
|
+
...option.maxRecordBytes !== void 0 ? { maxRecordBytes: option.maxRecordBytes } : {},
|
|
595
|
+
...option.maxRunBytes !== void 0 ? { maxRunBytes: option.maxRunBytes } : {},
|
|
596
|
+
...option.maxQueueBytes !== void 0 ? { maxQueueBytes: option.maxQueueBytes } : {}
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
function toDiagnosticManifest(manifest) {
|
|
600
|
+
return {
|
|
601
|
+
version: 1,
|
|
602
|
+
artifactId: manifest.artifactId,
|
|
603
|
+
fidelity: "runtime_request",
|
|
604
|
+
status: manifest.status,
|
|
605
|
+
...manifest.reason !== void 0 ? { reason: manifest.reason } : {},
|
|
606
|
+
records: manifest.records,
|
|
607
|
+
bytes: manifest.bytes,
|
|
608
|
+
redaction: manifest.redaction,
|
|
609
|
+
...manifest.expiresAt !== void 0 ? { expiresAt: manifest.expiresAt } : {}
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
function unavailableManifest(artifactId, reason) {
|
|
613
|
+
return {
|
|
614
|
+
version: 1,
|
|
615
|
+
artifactId,
|
|
616
|
+
fidelity: "runtime_request",
|
|
617
|
+
status: "unavailable",
|
|
618
|
+
reason,
|
|
619
|
+
records: 0,
|
|
620
|
+
bytes: 0,
|
|
621
|
+
redaction: "none"
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
var DEFAULT_SIDECAR_MAX_BYTES = 16 * 1024 * 1024;
|
|
625
|
+
var DEFAULT_SIDECAR_MAX_RECORD_BYTES = 4 * 1024 * 1024;
|
|
626
|
+
function validateRequestSidecar(text, options) {
|
|
627
|
+
if (typeof text !== "string") {
|
|
628
|
+
return { ok: false, reason: "requests sidecar must be a JSONL string" };
|
|
629
|
+
}
|
|
630
|
+
const maxBytes = options?.maxBytes ?? DEFAULT_SIDECAR_MAX_BYTES;
|
|
631
|
+
const maxRecordBytes = options?.maxRecordBytes ?? DEFAULT_SIDECAR_MAX_RECORD_BYTES;
|
|
632
|
+
const bytes = Buffer.byteLength(text, "utf-8");
|
|
633
|
+
if (bytes > maxBytes) {
|
|
634
|
+
return { ok: false, reason: `requests sidecar exceeds ${maxBytes} bytes` };
|
|
635
|
+
}
|
|
636
|
+
const lines = text.split("\n").filter((line) => line.trim() !== "");
|
|
637
|
+
for (let i = 0; i < lines.length; i++) {
|
|
638
|
+
const lineBytes = Buffer.byteLength(lines[i], "utf-8");
|
|
639
|
+
if (lineBytes > maxRecordBytes) {
|
|
640
|
+
return {
|
|
641
|
+
ok: false,
|
|
642
|
+
reason: `requests sidecar line ${i + 1} exceeds ${maxRecordBytes} bytes`
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
let parsed;
|
|
646
|
+
try {
|
|
647
|
+
parsed = JSON.parse(lines[i]);
|
|
648
|
+
} catch {
|
|
649
|
+
return { ok: false, reason: `requests sidecar line ${i + 1} is not valid JSON` };
|
|
650
|
+
}
|
|
651
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
652
|
+
return { ok: false, reason: `requests sidecar line ${i + 1} is not an object` };
|
|
653
|
+
}
|
|
654
|
+
const record = parsed;
|
|
655
|
+
if (record.v !== 1) {
|
|
656
|
+
return {
|
|
657
|
+
ok: false,
|
|
658
|
+
reason: `requests sidecar line ${i + 1} declares unsupported codec version ${String(record.v)}`
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
if (typeof record.operationId !== "string" || record.operationId === "") {
|
|
662
|
+
return { ok: false, reason: `requests sidecar line ${i + 1} has no operationId` };
|
|
663
|
+
}
|
|
664
|
+
if (record.phase !== "start" && record.phase !== "attempt" && record.phase !== "end") {
|
|
665
|
+
return { ok: false, reason: `requests sidecar line ${i + 1} has an unknown phase` };
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return { ok: true, lines, bytes };
|
|
669
|
+
}
|
|
670
|
+
function parseRequestRecords(lines) {
|
|
671
|
+
return lines.map((line) => JSON.parse(line));
|
|
672
|
+
}
|
|
673
|
+
function serializeRequestRecords(records) {
|
|
674
|
+
return records.map((record) => JSON.stringify(record)).join("\n") + (records.length ? "\n" : "");
|
|
358
675
|
}
|
|
359
676
|
|
|
360
677
|
// src/runner.ts
|
|
361
678
|
var import_node_crypto = require("crypto");
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
679
|
+
var RESERVED_METADATA_KEYS = [
|
|
680
|
+
"models",
|
|
681
|
+
"modelCallCounts",
|
|
682
|
+
"workflows",
|
|
683
|
+
"workflowCallCounts",
|
|
684
|
+
"tokens",
|
|
685
|
+
"agentCalls"
|
|
686
|
+
];
|
|
687
|
+
var ITEM_OUTCOMES = [
|
|
688
|
+
"completed",
|
|
689
|
+
"failed",
|
|
690
|
+
"cancelled",
|
|
691
|
+
"budget_skipped",
|
|
692
|
+
"budget_interrupted"
|
|
693
|
+
];
|
|
694
|
+
var SCORER_OUTCOMES = [
|
|
695
|
+
"scored",
|
|
696
|
+
"failed",
|
|
697
|
+
"skipped",
|
|
698
|
+
"cancelled",
|
|
699
|
+
"budget_skipped",
|
|
700
|
+
"budget_interrupted"
|
|
701
|
+
];
|
|
367
702
|
function extractUserCost(result, label = "executeWorkflow") {
|
|
368
703
|
if (result === null || typeof result !== "object") return void 0;
|
|
369
704
|
const raw = result.cost;
|
|
370
705
|
if (raw === void 0) return void 0;
|
|
371
706
|
if (typeof raw === "number" && Number.isFinite(raw) && raw >= 0) return raw;
|
|
372
707
|
console.warn(
|
|
373
|
-
`[axl-eval] Ignoring invalid \`cost\` from ${label} return: expected non-negative finite number, got ${typeof raw === "number" ? String(raw) : typeof raw}.
|
|
708
|
+
`[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.`
|
|
374
709
|
);
|
|
375
710
|
return void 0;
|
|
376
711
|
}
|
|
@@ -393,26 +728,146 @@ function extractUserMetadata(result, label = "executeWorkflow") {
|
|
|
393
728
|
}
|
|
394
729
|
return meta;
|
|
395
730
|
}
|
|
396
|
-
function
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
731
|
+
function splitCallerMetadata(user) {
|
|
732
|
+
if (!user) return {};
|
|
733
|
+
let reserved;
|
|
734
|
+
const safe = {};
|
|
735
|
+
for (const [key, value] of Object.entries(user)) {
|
|
736
|
+
if (RESERVED_METADATA_KEYS.includes(key)) {
|
|
737
|
+
(reserved ??= {})[key] = value;
|
|
738
|
+
} else {
|
|
739
|
+
safe[key] = value;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return { reserved, ...Object.keys(safe).length > 0 ? { safe } : {} };
|
|
743
|
+
}
|
|
744
|
+
function isAbortError(err) {
|
|
745
|
+
return err?.name === "AbortError";
|
|
746
|
+
}
|
|
747
|
+
async function stageCapture(runtime, ownerId, option) {
|
|
748
|
+
const limits = resolveCaptureLimits(option);
|
|
749
|
+
if (!limits) return void 0;
|
|
750
|
+
const staged = await runtime.stageDiagnosticArtifact({ kind: "eval", id: ownerId });
|
|
751
|
+
return {
|
|
752
|
+
artifactId: staged.artifactId,
|
|
753
|
+
channel: new import_axl3.RequestCaptureChannel({
|
|
754
|
+
sink: staged.sink,
|
|
755
|
+
...limits,
|
|
756
|
+
// Redaction is the RUNTIME's policy, not the caller's: a run cannot opt
|
|
757
|
+
// out of compliance mode by asking for diagnostics.
|
|
758
|
+
redact: runtime.isRedactEnabled()
|
|
759
|
+
})
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
async function finishCapture(runtime, capture) {
|
|
763
|
+
const status = await capture.channel.close();
|
|
764
|
+
try {
|
|
765
|
+
return await runtime.finalizeDiagnosticArtifact(
|
|
766
|
+
capture.artifactId,
|
|
767
|
+
status.status,
|
|
768
|
+
status.reason,
|
|
769
|
+
// What the channel ACTUALLY applied. The store sees only scrubbed bytes
|
|
770
|
+
// and cannot tell, so a manifest without this reports every compliance-
|
|
771
|
+
// mode artifact as unredacted.
|
|
772
|
+
status.redaction
|
|
773
|
+
);
|
|
774
|
+
} catch {
|
|
775
|
+
return void 0;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
async function abandonCapture(runtime, capture) {
|
|
779
|
+
if (!capture) return;
|
|
780
|
+
await capture.channel.close().catch(() => void 0);
|
|
781
|
+
await runtime.rollbackDiagnosticArtifact(capture.artifactId).catch(() => void 0);
|
|
782
|
+
}
|
|
783
|
+
function attachOperationRefs(items, channel) {
|
|
784
|
+
const byItem = /* @__PURE__ */ new Map();
|
|
785
|
+
const byScorer = /* @__PURE__ */ new Map();
|
|
786
|
+
for (const entry of channel.operations()) {
|
|
787
|
+
if (entry.caseIndex === void 0) continue;
|
|
788
|
+
const ref = {
|
|
789
|
+
operationId: entry.operationId,
|
|
790
|
+
kind: entry.kind,
|
|
791
|
+
...entry.turn !== void 0 ? { turn: entry.turn } : {},
|
|
792
|
+
...entry.attempt !== void 0 ? { attempt: entry.attempt } : {},
|
|
793
|
+
status: entry.status
|
|
794
|
+
};
|
|
795
|
+
if (entry.scorer !== void 0) {
|
|
796
|
+
const forItem = byScorer.get(entry.caseIndex) ?? /* @__PURE__ */ new Map();
|
|
797
|
+
const list = forItem.get(entry.scorer) ?? [];
|
|
798
|
+
list.push(ref);
|
|
799
|
+
forItem.set(entry.scorer, list);
|
|
800
|
+
byScorer.set(entry.caseIndex, forItem);
|
|
801
|
+
} else {
|
|
802
|
+
const list = byItem.get(entry.caseIndex) ?? [];
|
|
803
|
+
list.push(ref);
|
|
804
|
+
byItem.set(entry.caseIndex, list);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
for (const [caseIndex, operations] of byItem) {
|
|
808
|
+
const item = items[caseIndex];
|
|
809
|
+
if (!item) continue;
|
|
810
|
+
item.diagnostics = { operations: [...item.diagnostics?.operations ?? [], ...operations] };
|
|
811
|
+
}
|
|
812
|
+
for (const [caseIndex, forItem] of byScorer) {
|
|
813
|
+
for (const [scorer2, operations] of forItem) {
|
|
814
|
+
const detail = items[caseIndex]?.scoreDetails?.[scorer2];
|
|
815
|
+
if (detail) detail.diagnostics = { operations };
|
|
404
816
|
}
|
|
405
817
|
}
|
|
406
|
-
|
|
818
|
+
}
|
|
819
|
+
var MAX_CAUSE_DEPTH = 8;
|
|
820
|
+
function isProviderErrorLike(value) {
|
|
821
|
+
if (value instanceof import_axl3.ProviderError) return true;
|
|
822
|
+
if (typeof value !== "object" || value === null) return false;
|
|
823
|
+
const candidate = value;
|
|
824
|
+
return candidate.code === "PROVIDER_ERROR" && candidate.name === "ProviderError";
|
|
825
|
+
}
|
|
826
|
+
function describeItemFailure(thrown) {
|
|
827
|
+
let current = thrown;
|
|
828
|
+
for (let depth = 0; depth <= MAX_CAUSE_DEPTH; depth++) {
|
|
829
|
+
if (typeof current !== "object" || current === null) break;
|
|
830
|
+
if (isProviderErrorLike(current)) {
|
|
831
|
+
const { name: name2, provider, status, retryable, requestId } = current;
|
|
832
|
+
return {
|
|
833
|
+
name: typeof name2 === "string" ? name2 : "ProviderError",
|
|
834
|
+
...typeof provider === "string" ? { provider } : {},
|
|
835
|
+
...typeof status === "number" && Number.isFinite(status) ? { status } : {},
|
|
836
|
+
...typeof retryable === "boolean" ? { retryable } : {},
|
|
837
|
+
...typeof requestId === "string" ? { requestId } : {}
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
current = current.cause;
|
|
841
|
+
}
|
|
842
|
+
const name = thrown?.name;
|
|
843
|
+
return typeof thrown === "object" && thrown !== null && typeof name === "string" && name ? { name } : void 0;
|
|
407
844
|
}
|
|
408
845
|
async function runEval(config, executeWorkflow, runtime, options) {
|
|
409
846
|
const startTime = Date.now();
|
|
410
847
|
const id = (0, import_node_crypto.randomUUID)();
|
|
411
|
-
const
|
|
848
|
+
const itemErrorRateLimit = config.failOnItemErrorRate ?? DEFAULT_ITEM_ERROR_RATE_LIMIT;
|
|
849
|
+
if (!isErrorRateLimit(itemErrorRateLimit)) {
|
|
850
|
+
throw new import_axl3.AxlError(
|
|
851
|
+
"INVALID_ITEM_ERROR_RATE",
|
|
852
|
+
`Invalid failOnItemErrorRate (${String(itemErrorRateLimit)}): expected a number in [0, 1]; 1 disables the gate.`
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
const admission = config.budget != null ? new import_axl3.AdmissionController({ limit: parseBudget(config.budget) }) : void 0;
|
|
856
|
+
const capture = await stageCapture(runtime, id, options?.captureRequests);
|
|
857
|
+
let items;
|
|
858
|
+
try {
|
|
859
|
+
items = await config.dataset.getItems();
|
|
860
|
+
} catch (error) {
|
|
861
|
+
await abandonCapture(runtime, capture);
|
|
862
|
+
throw error;
|
|
863
|
+
}
|
|
412
864
|
const droppedAnnotationKeys = [...config.dataset.droppedAnnotationKeys ?? []];
|
|
413
865
|
const concurrency = config.concurrency ?? 5;
|
|
414
866
|
const scorerConcurrency = config.scorerConcurrency ?? 5;
|
|
415
|
-
|
|
867
|
+
let closedBy;
|
|
868
|
+
const noteClosure = (source) => {
|
|
869
|
+
if (closedBy === void 0 && admission?.closed) closedBy = source;
|
|
870
|
+
};
|
|
416
871
|
const scorerContext = {
|
|
417
872
|
resolveProvider: (uri) => {
|
|
418
873
|
if (typeof runtime.resolveProvider !== "function") {
|
|
@@ -425,8 +880,7 @@ async function runEval(config, executeWorkflow, runtime, options) {
|
|
|
425
880
|
signal: options?.signal
|
|
426
881
|
};
|
|
427
882
|
const evalItems = new Array(items.length);
|
|
428
|
-
|
|
429
|
-
let budgetExceeded = false;
|
|
883
|
+
const callerReported = { costItems: 0, costTotal: 0, metadataItems: 0 };
|
|
430
884
|
const runCallSamples = /* @__PURE__ */ new Map();
|
|
431
885
|
function absorbTiming(rollup, evalItem) {
|
|
432
886
|
if (!rollup) return;
|
|
@@ -448,26 +902,28 @@ async function runEval(config, executeWorkflow, runtime, options) {
|
|
|
448
902
|
}
|
|
449
903
|
evalItem.timing = perItem;
|
|
450
904
|
}
|
|
905
|
+
function stub(item, outcome, error) {
|
|
906
|
+
return {
|
|
907
|
+
input: item.input,
|
|
908
|
+
annotations: item.annotations,
|
|
909
|
+
output: null,
|
|
910
|
+
error,
|
|
911
|
+
outcome,
|
|
912
|
+
accounting: emptyAccounting(),
|
|
913
|
+
cost: 0,
|
|
914
|
+
scorerCost: 0,
|
|
915
|
+
scores: {}
|
|
916
|
+
};
|
|
917
|
+
}
|
|
451
918
|
async function processItem(item, itemIndex) {
|
|
452
919
|
if (options?.signal?.aborted) {
|
|
453
|
-
evalItems[itemIndex] =
|
|
454
|
-
input: item.input,
|
|
455
|
-
annotations: item.annotations,
|
|
456
|
-
output: null,
|
|
457
|
-
error: "Cancelled",
|
|
458
|
-
scores: {}
|
|
459
|
-
};
|
|
920
|
+
evalItems[itemIndex] = stub(item, "cancelled", "Cancelled");
|
|
460
921
|
options?.onProgress?.({ type: "item_done", itemIndex, totalItems: items.length });
|
|
461
922
|
return;
|
|
462
923
|
}
|
|
463
|
-
if (
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
annotations: item.annotations,
|
|
467
|
-
output: null,
|
|
468
|
-
error: "Budget exceeded",
|
|
469
|
-
scores: {}
|
|
470
|
-
};
|
|
924
|
+
if (admission?.closed) {
|
|
925
|
+
noteClosure("case");
|
|
926
|
+
evalItems[itemIndex] = stub(item, "budget_skipped", "Budget exceeded");
|
|
471
927
|
options?.onProgress?.({ type: "item_done", itemIndex, totalItems: items.length });
|
|
472
928
|
return;
|
|
473
929
|
}
|
|
@@ -478,77 +934,124 @@ async function runEval(config, executeWorkflow, runtime, options) {
|
|
|
478
934
|
scores: {}
|
|
479
935
|
};
|
|
480
936
|
const itemStart = Date.now();
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
937
|
+
const itemOutcome = await trackScope(
|
|
938
|
+
runtime,
|
|
939
|
+
async () => {
|
|
940
|
+
const generation = await trackScope(
|
|
941
|
+
runtime,
|
|
484
942
|
async () => executeWorkflow(item.input, runtime),
|
|
485
|
-
{
|
|
943
|
+
{
|
|
944
|
+
purpose: "generation",
|
|
945
|
+
captureTraces: options?.captureTraces === true,
|
|
946
|
+
captureTimingSamples: true
|
|
947
|
+
}
|
|
486
948
|
);
|
|
487
949
|
evalItem.duration = Date.now() - itemStart;
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
evalItem.metadata = mergeMetadata(tracked.metadata, extractUserMetadata(tracked.result));
|
|
492
|
-
absorbTiming(tracked.modelTiming, evalItem);
|
|
493
|
-
if (tracked.traces && tracked.traces.length > 0) {
|
|
494
|
-
evalItem.traces = tracked.traces;
|
|
950
|
+
absorbTiming(generation.modelTiming, evalItem);
|
|
951
|
+
if (generation.traces && generation.traces.length > 0) {
|
|
952
|
+
evalItem.traces = generation.traces;
|
|
495
953
|
}
|
|
496
|
-
if (
|
|
497
|
-
|
|
954
|
+
if (generation.status === "rejected") {
|
|
955
|
+
const err = generation.error;
|
|
956
|
+
if (isAdmissionDenied(err)) {
|
|
957
|
+
noteClosure("operation");
|
|
958
|
+
evalItem.outcome = "budget_interrupted";
|
|
959
|
+
evalItem.error = "Budget interrupted";
|
|
960
|
+
} else if (isAbortError(err)) {
|
|
961
|
+
evalItem.outcome = "cancelled";
|
|
962
|
+
evalItem.error = "Cancelled";
|
|
963
|
+
} else {
|
|
964
|
+
evalItem.outcome = "failed";
|
|
965
|
+
const failure = describeItemFailure(err);
|
|
966
|
+
if (failure) evalItem.failure = failure;
|
|
967
|
+
evalItem.error = err instanceof Error ? err.message : String(err);
|
|
968
|
+
if (admission?.closed) noteClosure("case");
|
|
969
|
+
}
|
|
970
|
+
return;
|
|
498
971
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
let trackedMetadata;
|
|
502
|
-
let result;
|
|
503
|
-
if (typeof runtime.trackExecution === "function") {
|
|
504
|
-
const tracked = await runtime.trackExecution(
|
|
505
|
-
async () => executeWorkflow(item.input, runtime),
|
|
506
|
-
{ captureTraces: false, captureTimingSamples: true }
|
|
507
|
-
);
|
|
508
|
-
result = tracked.result;
|
|
509
|
-
trackedTiming = tracked.modelTiming;
|
|
510
|
-
trackedMetadata = tracked.metadata;
|
|
511
|
-
} else {
|
|
512
|
-
result = await executeWorkflow(item.input, runtime);
|
|
513
|
-
}
|
|
514
|
-
evalItem.duration = Date.now() - itemStart;
|
|
972
|
+
const result = generation.value;
|
|
973
|
+
evalItem.outcome = "completed";
|
|
515
974
|
evalItem.output = result.output;
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
totalCost += evalItem.cost;
|
|
975
|
+
const callerCost = extractUserCost(result);
|
|
976
|
+
if (callerCost !== void 0) {
|
|
977
|
+
(evalItem.callerReport ??= {}).cost = callerCost;
|
|
978
|
+
callerReported.costItems += 1;
|
|
979
|
+
callerReported.costTotal += callerCost;
|
|
522
980
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
evalItem.traces = captured;
|
|
981
|
+
const { reserved, safe } = splitCallerMetadata(extractUserMetadata(result));
|
|
982
|
+
if (reserved) {
|
|
983
|
+
(evalItem.callerReport ??= {}).metadata = reserved;
|
|
984
|
+
callerReported.metadataItems += 1;
|
|
985
|
+
}
|
|
986
|
+
if (generation.metadata || safe) {
|
|
987
|
+
evalItem.metadata = { ...generation.metadata, ...safe };
|
|
531
988
|
}
|
|
989
|
+
if (admission?.closed) noteClosure("case");
|
|
990
|
+
await scoreItem(evalItem, config.scorers, {
|
|
991
|
+
runtime,
|
|
992
|
+
scorerContext,
|
|
993
|
+
scorerConcurrency,
|
|
994
|
+
signal: options?.signal,
|
|
995
|
+
admission,
|
|
996
|
+
onClosure: noteClosure
|
|
997
|
+
});
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
purpose: "generation",
|
|
1001
|
+
// Every record produced anywhere under this item — workflow turns,
|
|
1002
|
+
// nested asks, tool continuations, judges — is stamped with the case
|
|
1003
|
+
// index, which is how the result can point an item at its own requests.
|
|
1004
|
+
...capture ? { captureCorrelation: { caseIndex: itemIndex } } : {}
|
|
532
1005
|
}
|
|
533
|
-
evalItems[itemIndex] = evalItem;
|
|
534
|
-
options?.onProgress?.({ type: "item_done", itemIndex, totalItems: items.length });
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
if (budgetLimit != null && totalCost > budgetLimit) {
|
|
538
|
-
budgetExceeded = true;
|
|
539
|
-
}
|
|
540
|
-
const itemScorerCost = await scoreItem(
|
|
541
|
-
evalItem,
|
|
542
|
-
config.scorers,
|
|
543
|
-
scorerConcurrency,
|
|
544
|
-
scorerContext,
|
|
545
|
-
options?.signal
|
|
546
1006
|
);
|
|
547
|
-
|
|
1007
|
+
if (itemOutcome.status === "rejected") throw itemOutcome.error;
|
|
1008
|
+
evalItem.accounting = itemOutcome.accounting;
|
|
1009
|
+
evalItem.cost = itemOutcome.accounting.breakdown.generation;
|
|
1010
|
+
evalItem.scorerCost = itemOutcome.accounting.breakdown.judging;
|
|
1011
|
+
if (itemOutcome.accounting.completeness !== "complete") evalItem.unpriced = true;
|
|
548
1012
|
evalItems[itemIndex] = evalItem;
|
|
549
1013
|
options?.onProgress?.({ type: "item_done", itemIndex, totalItems: items.length });
|
|
550
1014
|
}
|
|
551
|
-
|
|
1015
|
+
const runOutcome = await trackScope(
|
|
1016
|
+
runtime,
|
|
1017
|
+
async () => {
|
|
1018
|
+
await mapWithConcurrency(items, concurrency, (item, i) => processItem(item, i));
|
|
1019
|
+
},
|
|
1020
|
+
// The capture channel is declared ONCE, at the run scope: its byte budget
|
|
1021
|
+
// and pending queue are per RUN, and every nested item/scorer scope
|
|
1022
|
+
// inherits it rather than opening a competing budget of its own.
|
|
1023
|
+
{ admission, ...capture ? { capture: capture.channel } : {} }
|
|
1024
|
+
);
|
|
1025
|
+
if (runOutcome.status === "rejected") {
|
|
1026
|
+
await abandonCapture(runtime, capture);
|
|
1027
|
+
throw runOutcome.error;
|
|
1028
|
+
}
|
|
1029
|
+
let diagnostics;
|
|
1030
|
+
if (capture) {
|
|
1031
|
+
try {
|
|
1032
|
+
attachOperationRefs(evalItems, capture.channel);
|
|
1033
|
+
} catch (error) {
|
|
1034
|
+
await abandonCapture(runtime, capture);
|
|
1035
|
+
throw error;
|
|
1036
|
+
}
|
|
1037
|
+
const manifest = await finishCapture(runtime, capture);
|
|
1038
|
+
diagnostics = manifest ? toDiagnosticManifest(manifest) : (
|
|
1039
|
+
// An artifact that was never sealed is never committed, so publishing
|
|
1040
|
+
// its id would point a reader at bytes the sweeper is about to reclaim.
|
|
1041
|
+
unavailableManifest("", "the diagnostic artifact could not be finalized")
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
const accounting = {
|
|
1045
|
+
...runOutcome.accounting,
|
|
1046
|
+
scope: "run",
|
|
1047
|
+
...admission ? {
|
|
1048
|
+
budget: {
|
|
1049
|
+
...admission.snapshot(),
|
|
1050
|
+
...closedBy ? { closedBy } : {}
|
|
1051
|
+
}
|
|
1052
|
+
} : {},
|
|
1053
|
+
...callerReported.costItems > 0 || callerReported.metadataItems > 0 ? { callerReported } : {}
|
|
1054
|
+
};
|
|
552
1055
|
const failures = evalItems.filter((i) => i.error).length;
|
|
553
1056
|
const scorerNames = config.scorers.map((s) => s.name);
|
|
554
1057
|
const scorerStats = {};
|
|
@@ -557,6 +1060,7 @@ async function runEval(config, executeWorkflow, runtime, options) {
|
|
|
557
1060
|
const { scored, failed, skipped } = scorerCounts(evalItems, name);
|
|
558
1061
|
scorerStats[name] = { ...computeStats(scores), scored, failed, skipped };
|
|
559
1062
|
}
|
|
1063
|
+
const coverage = buildCoverage(evalItems, scorerNames);
|
|
560
1064
|
const scorerTypes = {};
|
|
561
1065
|
for (const s of config.scorers) {
|
|
562
1066
|
scorerTypes[s.name] = s.isLlm ? "llm" : "deterministic";
|
|
@@ -582,21 +1086,10 @@ async function runEval(config, executeWorkflow, runtime, options) {
|
|
|
582
1086
|
}
|
|
583
1087
|
}
|
|
584
1088
|
}
|
|
1089
|
+
const itemErrorRate = evaluateItemErrorRate(coverage.items, items.length, itemErrorRateLimit);
|
|
585
1090
|
const durations = evalItems.filter((i) => !i.error && i.duration != null).map((i) => i.duration);
|
|
586
1091
|
const timing = durations.length > 0 ? computeStats(durations) : void 0;
|
|
587
|
-
const modelTiming = runCallSamples
|
|
588
|
-
[...runCallSamples.entries()].map(([model, samples]) => {
|
|
589
|
-
const firstToken = samples.map((s) => s.firstTokenMs).filter((v) => v != null);
|
|
590
|
-
const stats = {
|
|
591
|
-
calls: samples.length,
|
|
592
|
-
wireMs: computeStats(samples.map((s) => s.wireMs)),
|
|
593
|
-
queuedMs: computeStats(samples.map((s) => s.queuedMs)),
|
|
594
|
-
retryMs: computeStats(samples.map((s) => s.retryMs)),
|
|
595
|
-
...firstToken.length > 0 ? { firstTokenMs: computeStats(firstToken), firstTokenCalls: firstToken.length } : {}
|
|
596
|
-
};
|
|
597
|
-
return [model, stats];
|
|
598
|
-
})
|
|
599
|
-
) : void 0;
|
|
1092
|
+
const modelTiming = buildModelTiming(runCallSamples);
|
|
600
1093
|
const totalModelCalls = /* @__PURE__ */ new Map();
|
|
601
1094
|
for (const item of evalItems) {
|
|
602
1095
|
const itemCounts = item.metadata?.modelCallCounts;
|
|
@@ -659,20 +1152,64 @@ async function runEval(config, executeWorkflow, runtime, options) {
|
|
|
659
1152
|
...droppedAnnotationKeys.length > 0 ? { droppedAnnotationKeys } : {}
|
|
660
1153
|
},
|
|
661
1154
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
662
|
-
totalCost,
|
|
663
|
-
...
|
|
1155
|
+
totalCost: accounting.knownCost,
|
|
1156
|
+
...accounting.completeness !== "complete" ? { unpriced: true } : {},
|
|
1157
|
+
accounting,
|
|
1158
|
+
...diagnostics ? { diagnostics } : {},
|
|
664
1159
|
duration: Date.now() - startTime,
|
|
665
1160
|
items: evalItems,
|
|
666
1161
|
summary: {
|
|
667
1162
|
count: items.length,
|
|
668
1163
|
failures,
|
|
1164
|
+
coverage,
|
|
669
1165
|
scorers: scorerStats,
|
|
670
1166
|
timing,
|
|
671
1167
|
...modelTiming ? { modelTiming } : {},
|
|
672
|
-
...degraded ? { degraded } : {}
|
|
1168
|
+
...degraded ? { degraded } : {},
|
|
1169
|
+
...itemErrorRate.failed > 0 ? { itemErrorRate } : {}
|
|
673
1170
|
}
|
|
674
1171
|
};
|
|
675
1172
|
}
|
|
1173
|
+
function buildCoverage(items, scorerNames) {
|
|
1174
|
+
const coverage = {
|
|
1175
|
+
items: Object.fromEntries(ITEM_OUTCOMES.map((o) => [o, 0])),
|
|
1176
|
+
scorers: {}
|
|
1177
|
+
};
|
|
1178
|
+
for (const name of scorerNames) {
|
|
1179
|
+
coverage.scorers[name] = Object.fromEntries(SCORER_OUTCOMES.map((o) => [o, 0]));
|
|
1180
|
+
}
|
|
1181
|
+
for (const item of items) {
|
|
1182
|
+
const outcome = item.outcome ?? (item.error ? "failed" : "completed");
|
|
1183
|
+
if (coverage.items[outcome] !== void 0) coverage.items[outcome] += 1;
|
|
1184
|
+
for (const name of scorerNames) {
|
|
1185
|
+
const detail = item.scoreDetails?.[name];
|
|
1186
|
+
if (!detail?.outcome) continue;
|
|
1187
|
+
const bucket = coverage.scorers[name];
|
|
1188
|
+
if (bucket[detail.outcome] !== void 0) bucket[detail.outcome] += 1;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return coverage;
|
|
1192
|
+
}
|
|
1193
|
+
function buildModelTiming(runCallSamples) {
|
|
1194
|
+
if (runCallSamples.size === 0) return void 0;
|
|
1195
|
+
return Object.fromEntries(
|
|
1196
|
+
[...runCallSamples.entries()].map(([model, samples]) => {
|
|
1197
|
+
const firstToken = samples.map((s) => s.firstTokenMs).filter((v) => v != null);
|
|
1198
|
+
return [
|
|
1199
|
+
model,
|
|
1200
|
+
{
|
|
1201
|
+
calls: samples.length,
|
|
1202
|
+
wireMs: computeStats(samples.map((s) => s.wireMs)),
|
|
1203
|
+
queuedMs: computeStats(samples.map((s) => s.queuedMs)),
|
|
1204
|
+
retryMs: computeStats(samples.map((s) => s.retryMs)),
|
|
1205
|
+
// Custom providers may omit the optional field; that call adds 0.
|
|
1206
|
+
rateLimitRetries: samples.reduce((sum, s) => sum + (s.rateLimitRetries ?? 0), 0),
|
|
1207
|
+
...firstToken.length > 0 ? { firstTokenMs: computeStats(firstToken), firstTokenCalls: firstToken.length } : {}
|
|
1208
|
+
}
|
|
1209
|
+
];
|
|
1210
|
+
})
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
676
1213
|
|
|
677
1214
|
// src/bootstrap.ts
|
|
678
1215
|
function pairedBootstrapCI(differences, options) {
|
|
@@ -741,6 +1278,69 @@ function xorshift32(seed) {
|
|
|
741
1278
|
}
|
|
742
1279
|
|
|
743
1280
|
// src/compare.ts
|
|
1281
|
+
function certifyCost(baselineRuns, candidateRuns, scorerNames) {
|
|
1282
|
+
for (const [side, runs] of [
|
|
1283
|
+
["baseline", baselineRuns],
|
|
1284
|
+
["candidate", candidateRuns]
|
|
1285
|
+
]) {
|
|
1286
|
+
for (const run of runs) {
|
|
1287
|
+
const accounting = readAccounting(run);
|
|
1288
|
+
if (accounting.completeness === "unverified") {
|
|
1289
|
+
return `${side} run ${run.id} carries no accounting (unverified) \u2014 its total is not a measured figure.`;
|
|
1290
|
+
}
|
|
1291
|
+
if (accounting.completeness === "incomplete") {
|
|
1292
|
+
const reasons = Object.keys(accounting.reasons).join(", ") || "unknown spend";
|
|
1293
|
+
return `${side} run ${run.id} has incomplete accounting (${reasons}) \u2014 its total is a lower bound.`;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
const scopes = /* @__PURE__ */ new Map();
|
|
1298
|
+
for (const [side, runs] of [
|
|
1299
|
+
["baseline", baselineRuns],
|
|
1300
|
+
["candidate", candidateRuns]
|
|
1301
|
+
]) {
|
|
1302
|
+
for (const run of runs) {
|
|
1303
|
+
const scope = readAccounting(run).scope;
|
|
1304
|
+
const seen = scopes.get(side);
|
|
1305
|
+
if (seen !== void 0 && seen !== scope) {
|
|
1306
|
+
return `${side} mixes accounting scopes ("${seen}" and "${scope}") \u2014 a rescore total covers judging only and cannot be averaged with a full run.`;
|
|
1307
|
+
}
|
|
1308
|
+
scopes.set(side, scope);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
const baselineScope = scopes.get("baseline");
|
|
1312
|
+
const candidateScope = scopes.get("candidate");
|
|
1313
|
+
if (baselineScope !== candidateScope) {
|
|
1314
|
+
return `accounting scope differs (baseline "${baselineScope}" vs candidate "${candidateScope}") \u2014 a rescore total covers judging only.`;
|
|
1315
|
+
}
|
|
1316
|
+
const runCount = Math.min(baselineRuns.length, candidateRuns.length);
|
|
1317
|
+
for (let r = 0; r < runCount; r++) {
|
|
1318
|
+
const bRun = baselineRuns[r];
|
|
1319
|
+
const cRun = candidateRuns[r];
|
|
1320
|
+
if (bRun.items.length !== cRun.items.length) {
|
|
1321
|
+
return `item counts differ (baseline ${bRun.items.length} vs candidate ${cRun.items.length}) \u2014 the runs did different amounts of work.`;
|
|
1322
|
+
}
|
|
1323
|
+
for (let i = 0; i < bRun.items.length; i++) {
|
|
1324
|
+
const bItem = bRun.items[i];
|
|
1325
|
+
const cItem = cRun.items[i];
|
|
1326
|
+
const bRan = !bItem.error;
|
|
1327
|
+
const cRan = !cItem.error;
|
|
1328
|
+
if (bRan !== cRan) {
|
|
1329
|
+
const stopped = bRan ? cItem : bItem;
|
|
1330
|
+
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.`;
|
|
1331
|
+
}
|
|
1332
|
+
if (!bRan) continue;
|
|
1333
|
+
for (const name of scorerNames) {
|
|
1334
|
+
const bScored = bItem.scores[name] != null;
|
|
1335
|
+
const cScored = cItem.scores[name] != null;
|
|
1336
|
+
if (bScored !== cScored) {
|
|
1337
|
+
return `scorer coverage differs for "${name}" at item ${i}: only ${bScored ? "baseline" : "candidate"} produced a score.`;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
return null;
|
|
1343
|
+
}
|
|
744
1344
|
var DEFAULT_LLM_THRESHOLD = 0.05;
|
|
745
1345
|
var DEFAULT_DETERMINISTIC_THRESHOLD = 0;
|
|
746
1346
|
var LEGACY_THRESHOLD = 0.1;
|
|
@@ -905,17 +1505,21 @@ function evalCompare(baseline, candidate, options) {
|
|
|
905
1505
|
timing = { baselineMean: round(bMean), candidateMean: round(cMean), delta, deltaPercent };
|
|
906
1506
|
}
|
|
907
1507
|
let cost;
|
|
908
|
-
const baselineAvgCost = baselineRuns.reduce((sum, r) => sum + r.
|
|
909
|
-
const candidateAvgCost = candidateRuns.reduce((sum, r) => sum + r.
|
|
910
|
-
|
|
1508
|
+
const baselineAvgCost = baselineRuns.reduce((sum, r) => sum + readAccounting(r).knownCost, 0) / baselineRuns.length;
|
|
1509
|
+
const candidateAvgCost = candidateRuns.reduce((sum, r) => sum + readAccounting(r).knownCost, 0) / candidateRuns.length;
|
|
1510
|
+
const hasAccounting = [...baselineRuns, ...candidateRuns].some((r) => r.accounting != null);
|
|
1511
|
+
if (baselineAvgCost > 0 || candidateAvgCost > 0 || hasAccounting) {
|
|
911
1512
|
const deltaRaw = candidateAvgCost - baselineAvgCost;
|
|
912
1513
|
const delta = round(deltaRaw);
|
|
913
|
-
const deltaPercent = baselineAvgCost > 0 ? round(deltaRaw / baselineAvgCost * 100) :
|
|
1514
|
+
const deltaPercent = baselineAvgCost > 0 ? round(deltaRaw / baselineAvgCost * 100) : null;
|
|
1515
|
+
const verdict = certifyCost(baselineRuns, candidateRuns, baselineScorerNames);
|
|
914
1516
|
cost = {
|
|
915
1517
|
baselineTotal: round(baselineAvgCost),
|
|
916
1518
|
candidateTotal: round(candidateAvgCost),
|
|
917
1519
|
delta,
|
|
918
|
-
deltaPercent
|
|
1520
|
+
deltaPercent,
|
|
1521
|
+
certified: verdict === null,
|
|
1522
|
+
...verdict !== null ? { reason: verdict } : {}
|
|
919
1523
|
};
|
|
920
1524
|
}
|
|
921
1525
|
const parts = [];
|
|
@@ -936,7 +1540,7 @@ function evalCompare(baseline, candidate, options) {
|
|
|
936
1540
|
const dir = timing.delta > 0 ? "slower" : "faster";
|
|
937
1541
|
parts.push(`${Math.abs(timing.deltaPercent).toFixed(0)}% ${dir}`);
|
|
938
1542
|
}
|
|
939
|
-
if (cost && Math.abs(cost.deltaPercent) > 1) {
|
|
1543
|
+
if (cost?.certified && cost.deltaPercent !== null && Math.abs(cost.deltaPercent) > 1) {
|
|
940
1544
|
const dir = cost.delta > 0 ? "more expensive" : "cheaper";
|
|
941
1545
|
parts.push(`${Math.abs(cost.deltaPercent).toFixed(0)}% ${dir}`);
|
|
942
1546
|
}
|
|
@@ -1001,13 +1605,150 @@ function evaluateScorerErrorRateGate(comparison, maxScorerErrorRate) {
|
|
|
1001
1605
|
}
|
|
1002
1606
|
return null;
|
|
1003
1607
|
}
|
|
1608
|
+
function compareItemErrorRates(baseline, candidate, maxItemErrorRate = DEFAULT_ITEM_ERROR_RATE_LIMIT) {
|
|
1609
|
+
if (!isErrorRateLimit(maxItemErrorRate)) {
|
|
1610
|
+
throw new Error(
|
|
1611
|
+
`Invalid maxItemErrorRate (${String(maxItemErrorRate)}): expected a number in [0, 1].`
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
const baselineRuns = Array.isArray(baseline) ? baseline : [baseline];
|
|
1615
|
+
const candidateRuns = Array.isArray(candidate) ? candidate : [candidate];
|
|
1616
|
+
const runCount = Math.min(baselineRuns.length, candidateRuns.length);
|
|
1617
|
+
const rates = [];
|
|
1618
|
+
for (const [side, runs] of [
|
|
1619
|
+
["baseline", baselineRuns],
|
|
1620
|
+
["candidate", candidateRuns]
|
|
1621
|
+
]) {
|
|
1622
|
+
for (let r = 0; r < runCount; r++) {
|
|
1623
|
+
const run = runs[r];
|
|
1624
|
+
const coverage = run.summary.coverage?.items;
|
|
1625
|
+
const legacy = coverage === void 0;
|
|
1626
|
+
const verdict = legacy ? evaluateItemErrorRate(
|
|
1627
|
+
buildCoverage(run.items, []).items,
|
|
1628
|
+
run.items.length,
|
|
1629
|
+
maxItemErrorRate
|
|
1630
|
+
) : evaluateItemErrorRate(coverage, run.summary.count, maxItemErrorRate);
|
|
1631
|
+
rates.push({ side, runIndex: r, runId: run.id, legacy, ...verdict });
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
return rates;
|
|
1635
|
+
}
|
|
1636
|
+
function describeItemErrorRate(rate, multiRun) {
|
|
1637
|
+
const which = multiRun ? ` run ${rate.runIndex + 1} (${rate.runId})` : ` (${rate.runId})`;
|
|
1638
|
+
const legacy = rate.legacy ? ", derived from the items of a pre-0.24 artifact" : "";
|
|
1639
|
+
return `${rate.side}${which} item error rate ${formatPercent(rate.rate)} (${rate.failed}/${rate.attempted} attempted items failed${legacy})`;
|
|
1640
|
+
}
|
|
1641
|
+
function evaluateItemErrorRateGate(baseline, candidate, maxItemErrorRate = DEFAULT_ITEM_ERROR_RATE_LIMIT) {
|
|
1642
|
+
const rates = compareItemErrorRates(baseline, candidate, maxItemErrorRate);
|
|
1643
|
+
const multiRun = rates.some((r) => r.runIndex > 0);
|
|
1644
|
+
const over = rates.find((r) => r.exceeded);
|
|
1645
|
+
if (!over) return null;
|
|
1646
|
+
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).`;
|
|
1647
|
+
}
|
|
1004
1648
|
|
|
1005
1649
|
// src/rescore.ts
|
|
1650
|
+
var import_axl4 = require("@axlsdk/axl");
|
|
1006
1651
|
var import_node_crypto2 = require("crypto");
|
|
1652
|
+
var COPY_SHARE_OF_RUN_BOUND = 0.75;
|
|
1653
|
+
function describeFailure(error) {
|
|
1654
|
+
return error instanceof Error ? error.message : String(error);
|
|
1655
|
+
}
|
|
1656
|
+
function isDiagnosticsUnavailable(error) {
|
|
1657
|
+
return error?.code === "DIAGNOSTICS_UNAVAILABLE";
|
|
1658
|
+
}
|
|
1659
|
+
async function beginCapture(source, ownerId, runtime, options) {
|
|
1660
|
+
const limits = resolveCaptureLimits(options?.captureRequests);
|
|
1661
|
+
if (!limits) return {};
|
|
1662
|
+
const open = (artifactId, sink, carriedBytes) => ({
|
|
1663
|
+
artifactId,
|
|
1664
|
+
channel: new import_axl4.RequestCaptureChannel({
|
|
1665
|
+
sink,
|
|
1666
|
+
...limits,
|
|
1667
|
+
carriedBytes,
|
|
1668
|
+
// Redaction is the RUNTIME's policy, not the caller's: a rescore cannot
|
|
1669
|
+
// opt out of compliance mode by asking for diagnostics.
|
|
1670
|
+
redact: runtime.isRedactEnabled()
|
|
1671
|
+
})
|
|
1672
|
+
});
|
|
1673
|
+
const sourceId = source.diagnostics?.artifactId;
|
|
1674
|
+
let stagedId;
|
|
1675
|
+
try {
|
|
1676
|
+
if (!sourceId) {
|
|
1677
|
+
const staged = await runtime.stageDiagnosticArtifact({ kind: "eval", id: ownerId });
|
|
1678
|
+
stagedId = staged.artifactId;
|
|
1679
|
+
return { capture: { ...open(staged.artifactId, staged.sink, 0), copied: false } };
|
|
1680
|
+
}
|
|
1681
|
+
const runBound = limits.maxRunBytes ?? DEFAULT_COPY_MAX_BYTES;
|
|
1682
|
+
const maxBytes = Math.max(1, Math.floor(runBound * COPY_SHARE_OF_RUN_BOUND));
|
|
1683
|
+
const copied = await runtime.copyDiagnosticArtifact(
|
|
1684
|
+
sourceId,
|
|
1685
|
+
{ kind: "eval", id: ownerId },
|
|
1686
|
+
{ maxBytes }
|
|
1687
|
+
);
|
|
1688
|
+
if (!copied) {
|
|
1689
|
+
return {
|
|
1690
|
+
degraded: unavailableManifest(
|
|
1691
|
+
"",
|
|
1692
|
+
"the source run's captured requests are no longer available"
|
|
1693
|
+
)
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
stagedId = copied.artifactId;
|
|
1697
|
+
const capture = {
|
|
1698
|
+
...open(copied.artifactId, copied.sink, copied.bytes),
|
|
1699
|
+
copied: true,
|
|
1700
|
+
carriedRedaction: copied.redaction
|
|
1701
|
+
};
|
|
1702
|
+
if (copied.truncated) {
|
|
1703
|
+
capture.copyTruncated = `the copied source records stopped at ${maxBytes} bytes, the share of the ${runBound} byte run limit reserved for them`;
|
|
1704
|
+
}
|
|
1705
|
+
return { capture };
|
|
1706
|
+
} catch (error) {
|
|
1707
|
+
if (stagedId !== void 0) {
|
|
1708
|
+
await runtime.rollbackDiagnosticArtifact(stagedId).catch(() => void 0);
|
|
1709
|
+
}
|
|
1710
|
+
if (isDiagnosticsUnavailable(error)) throw error;
|
|
1711
|
+
return {
|
|
1712
|
+
degraded: unavailableManifest(
|
|
1713
|
+
"",
|
|
1714
|
+
`the captured requests could not be carried forward: ${describeFailure(error)}`
|
|
1715
|
+
)
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
async function abandonCapture2(runtime, capture) {
|
|
1720
|
+
if (!capture) return;
|
|
1721
|
+
await capture.channel.close().catch(() => void 0);
|
|
1722
|
+
await runtime.rollbackDiagnosticArtifact(capture.artifactId).catch(() => void 0);
|
|
1723
|
+
}
|
|
1724
|
+
async function finishCapture2(runtime, capture) {
|
|
1725
|
+
const status = await capture.channel.close();
|
|
1726
|
+
const reason = [capture.copyTruncated, status.reason].filter(Boolean).join("; ") || void 0;
|
|
1727
|
+
const truncated = capture.copyTruncated !== void 0 || status.status === "truncated";
|
|
1728
|
+
const finalStatus = status.status === "unavailable" ? "unavailable" : truncated ? "truncated" : status.status;
|
|
1729
|
+
const redaction = status.redaction === "applied" && (capture.carriedRedaction ?? "applied") === "applied" ? "applied" : "none";
|
|
1730
|
+
try {
|
|
1731
|
+
const manifest = await runtime.finalizeDiagnosticArtifact(
|
|
1732
|
+
capture.artifactId,
|
|
1733
|
+
finalStatus,
|
|
1734
|
+
reason,
|
|
1735
|
+
redaction
|
|
1736
|
+
);
|
|
1737
|
+
return toDiagnosticManifest(manifest);
|
|
1738
|
+
} catch {
|
|
1739
|
+
return unavailableManifest("", "the diagnostic artifact could not be finalized");
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1007
1742
|
async function rescore(result, scorers, runtime, options) {
|
|
1008
1743
|
const startTime = Date.now();
|
|
1744
|
+
const rescoredId = (0, import_node_crypto2.randomUUID)();
|
|
1009
1745
|
const concurrency = options?.concurrency ?? 5;
|
|
1010
1746
|
const scorerConcurrency = options?.scorerConcurrency ?? 5;
|
|
1747
|
+
const admission = options?.budget != null ? new import_axl4.AdmissionController({ limit: parseBudget(options.budget) }) : void 0;
|
|
1748
|
+
let closedBy;
|
|
1749
|
+
const noteClosure = (source) => {
|
|
1750
|
+
if (closedBy === void 0 && admission?.closed) closedBy = source;
|
|
1751
|
+
};
|
|
1011
1752
|
const scorerContext = {
|
|
1012
1753
|
resolveProvider: (uri) => {
|
|
1013
1754
|
if (typeof runtime.resolveProvider !== "function") {
|
|
@@ -1020,7 +1761,8 @@ async function rescore(result, scorers, runtime, options) {
|
|
|
1020
1761
|
signal: options?.signal
|
|
1021
1762
|
};
|
|
1022
1763
|
const rescored = new Array(result.items.length);
|
|
1023
|
-
|
|
1764
|
+
const { capture, degraded } = await beginCapture(result, rescoredId, runtime, options);
|
|
1765
|
+
const carryDiagnostics = capture?.copied === true;
|
|
1024
1766
|
async function rescoreItem(original, itemIndex) {
|
|
1025
1767
|
if (options?.signal?.aborted) {
|
|
1026
1768
|
rescored[itemIndex] = {
|
|
@@ -1028,16 +1770,31 @@ async function rescore(result, scorers, runtime, options) {
|
|
|
1028
1770
|
annotations: original.annotations,
|
|
1029
1771
|
output: original.output,
|
|
1030
1772
|
error: "cancelled",
|
|
1773
|
+
outcome: "cancelled",
|
|
1774
|
+
accounting: emptyAccounting(),
|
|
1031
1775
|
scores: {}
|
|
1032
1776
|
};
|
|
1033
1777
|
return;
|
|
1034
1778
|
}
|
|
1035
1779
|
if (original.error) {
|
|
1780
|
+
const accounting2 = emptyAccounting();
|
|
1036
1781
|
rescored[itemIndex] = {
|
|
1037
1782
|
input: original.input,
|
|
1038
1783
|
annotations: original.annotations,
|
|
1039
1784
|
output: original.output,
|
|
1040
1785
|
error: original.error,
|
|
1786
|
+
...original.outcome ? { outcome: original.outcome } : {},
|
|
1787
|
+
// The failure cause is a fact about the source run too.
|
|
1788
|
+
...original.failure ? { failure: original.failure } : {},
|
|
1789
|
+
accounting: accounting2,
|
|
1790
|
+
// Same rule as a scored item: an item carrying `accounting` always
|
|
1791
|
+
// carries both compat views, so no reader falls through to the legacy
|
|
1792
|
+
// "no accounting → trust the caller's number" branch and reports the
|
|
1793
|
+
// SOURCE run's spend as this rescore's. A passthrough did no work, so
|
|
1794
|
+
// both are a measured $0; the source's spend lives in the run-level
|
|
1795
|
+
// `accounting.source` provenance and nowhere else.
|
|
1796
|
+
cost: accounting2.breakdown.generation,
|
|
1797
|
+
scorerCost: accounting2.breakdown.judging,
|
|
1041
1798
|
scores: {}
|
|
1042
1799
|
};
|
|
1043
1800
|
return;
|
|
@@ -1048,20 +1805,77 @@ async function rescore(result, scorers, runtime, options) {
|
|
|
1048
1805
|
output: original.output,
|
|
1049
1806
|
metadata: original.metadata,
|
|
1050
1807
|
traces: original.traces,
|
|
1808
|
+
outcome: "completed",
|
|
1809
|
+
// Carried forward VERBATIM, original operation ids included. The rescore
|
|
1810
|
+
// did not re-run the workflow, so these still describe the calls that
|
|
1811
|
+
// produced this output; reminting the ids would break the only link back
|
|
1812
|
+
// to the run that made them. Only carried when the source records were
|
|
1813
|
+
// actually copied into this rescore's artifact — a dangling reference is
|
|
1814
|
+
// worse than none.
|
|
1815
|
+
...carryDiagnostics && original.diagnostics ? { diagnostics: original.diagnostics } : {},
|
|
1051
1816
|
scores: {},
|
|
1052
1817
|
scoreDetails: {}
|
|
1053
1818
|
};
|
|
1054
|
-
const
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1819
|
+
const itemOutcome = await trackScope(
|
|
1820
|
+
runtime,
|
|
1821
|
+
async () => {
|
|
1822
|
+
await scoreItem(item, scorers, {
|
|
1823
|
+
runtime,
|
|
1824
|
+
scorerContext,
|
|
1825
|
+
scorerConcurrency,
|
|
1826
|
+
signal: options?.signal,
|
|
1827
|
+
admission,
|
|
1828
|
+
onClosure: noteClosure
|
|
1829
|
+
});
|
|
1830
|
+
},
|
|
1831
|
+
{
|
|
1832
|
+
purpose: "judging",
|
|
1833
|
+
// Every record produced under this item is stamped with the case index,
|
|
1834
|
+
// which is how a `ScorerDetail` can point at its own judge calls.
|
|
1835
|
+
...capture ? { captureCorrelation: { caseIndex: itemIndex } } : {}
|
|
1836
|
+
}
|
|
1060
1837
|
);
|
|
1061
|
-
|
|
1838
|
+
if (itemOutcome.status === "rejected") throw itemOutcome.error;
|
|
1839
|
+
item.accounting = itemOutcome.accounting;
|
|
1840
|
+
item.cost = itemOutcome.accounting.breakdown.generation;
|
|
1841
|
+
item.scorerCost = itemOutcome.accounting.breakdown.judging;
|
|
1842
|
+
if (itemOutcome.accounting.completeness !== "complete") item.unpriced = true;
|
|
1062
1843
|
rescored[itemIndex] = item;
|
|
1063
1844
|
}
|
|
1064
|
-
|
|
1845
|
+
const runOutcome = await trackScope(
|
|
1846
|
+
runtime,
|
|
1847
|
+
async () => {
|
|
1848
|
+
await mapWithConcurrency(result.items, concurrency, (item, i) => rescoreItem(item, i));
|
|
1849
|
+
},
|
|
1850
|
+
// The channel is declared ONCE, at the run scope: its byte budget and
|
|
1851
|
+
// pending queue are per rescore, and every nested item/scorer scope inherits
|
|
1852
|
+
// it rather than opening a competing budget of its own.
|
|
1853
|
+
{ purpose: "judging", admission, ...capture ? { capture: capture.channel } : {} }
|
|
1854
|
+
);
|
|
1855
|
+
if (runOutcome.status === "rejected") {
|
|
1856
|
+
await abandonCapture2(runtime, capture);
|
|
1857
|
+
throw runOutcome.error;
|
|
1858
|
+
}
|
|
1859
|
+
const sourceAccounting = result.accounting;
|
|
1860
|
+
const generation = sourceAccounting?.scope === "rescore" ? sourceAccounting.source?.generation ?? null : sourceAccounting ?? null;
|
|
1861
|
+
let diagnostics = degraded;
|
|
1862
|
+
if (capture) {
|
|
1863
|
+
try {
|
|
1864
|
+
attachOperationRefs(rescored, capture.channel);
|
|
1865
|
+
} catch (error) {
|
|
1866
|
+
await abandonCapture2(runtime, capture);
|
|
1867
|
+
throw error;
|
|
1868
|
+
}
|
|
1869
|
+
diagnostics = await finishCapture2(runtime, capture);
|
|
1870
|
+
}
|
|
1871
|
+
const accounting = {
|
|
1872
|
+
...runOutcome.accounting,
|
|
1873
|
+
scope: "rescore",
|
|
1874
|
+
// `?? null` is load-bearing: a legacy source has no accounting and must read
|
|
1875
|
+
// as "unknown generation", never as a synthesized complete record.
|
|
1876
|
+
source: { runId: result.id, generation },
|
|
1877
|
+
...admission ? { budget: { ...admission.snapshot(), ...closedBy ? { closedBy } : {} } } : {}
|
|
1878
|
+
};
|
|
1065
1879
|
const failures = rescored.filter((i) => i.error).length;
|
|
1066
1880
|
const scorerNames = scorers.map((s) => s.name);
|
|
1067
1881
|
const scorerStats = {};
|
|
@@ -1075,7 +1889,7 @@ async function rescore(result, scorers, runtime, options) {
|
|
|
1075
1889
|
scorerTypes[s.name] = s.isLlm ? "llm" : "deterministic";
|
|
1076
1890
|
}
|
|
1077
1891
|
return {
|
|
1078
|
-
id:
|
|
1892
|
+
id: rescoredId,
|
|
1079
1893
|
dataset: result.dataset,
|
|
1080
1894
|
metadata: (() => {
|
|
1081
1895
|
const rest = { ...result.metadata };
|
|
@@ -1099,10 +1913,24 @@ async function rescore(result, scorers, runtime, options) {
|
|
|
1099
1913
|
return merged;
|
|
1100
1914
|
})(),
|
|
1101
1915
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1102
|
-
totalCost,
|
|
1916
|
+
totalCost: accounting.knownCost,
|
|
1917
|
+
...accounting.completeness !== "complete" ? { unpriced: true } : {},
|
|
1918
|
+
accounting,
|
|
1919
|
+
...diagnostics ? { diagnostics } : {},
|
|
1103
1920
|
duration: Date.now() - startTime,
|
|
1104
1921
|
items: rescored,
|
|
1105
|
-
summary: {
|
|
1922
|
+
summary: {
|
|
1923
|
+
count: result.items.length,
|
|
1924
|
+
failures,
|
|
1925
|
+
coverage: buildCoverage(rescored, scorerNames),
|
|
1926
|
+
scorers: scorerStats,
|
|
1927
|
+
// A rescore makes no generation calls, so the source run's wall-clock
|
|
1928
|
+
// and provider latency are still the only measurements of these items.
|
|
1929
|
+
// `modelTiming` cannot be rebuilt from `item.timing` (sums, not per-call
|
|
1930
|
+
// samples): carry both forward.
|
|
1931
|
+
...result.summary.timing ? { timing: { ...result.summary.timing } } : {},
|
|
1932
|
+
...result.summary.modelTiming ? { modelTiming: structuredClone(result.summary.modelTiming) } : {}
|
|
1933
|
+
}
|
|
1106
1934
|
};
|
|
1107
1935
|
}
|
|
1108
1936
|
|
|
@@ -1172,6 +2000,7 @@ function aggregateRuns(runs) {
|
|
|
1172
2000
|
workflows,
|
|
1173
2001
|
dataset: dataset2,
|
|
1174
2002
|
totalCost: round(totalCost),
|
|
2003
|
+
accounting: aggregateAccounting(runs.map((r) => readAccounting(r))),
|
|
1175
2004
|
totalDuration: round(totalDuration),
|
|
1176
2005
|
scorers,
|
|
1177
2006
|
timing
|
|
@@ -1179,17 +2008,30 @@ function aggregateRuns(runs) {
|
|
|
1179
2008
|
}
|
|
1180
2009
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1181
2010
|
0 && (module.exports = {
|
|
2011
|
+
DEFAULT_COPY_MAX_BYTES,
|
|
2012
|
+
DEFAULT_SIDECAR_MAX_BYTES,
|
|
2013
|
+
aggregateAccounting,
|
|
1182
2014
|
aggregateRuns,
|
|
1183
2015
|
dataset,
|
|
1184
2016
|
defineEval,
|
|
1185
2017
|
evalCompare,
|
|
2018
|
+
evaluateItemErrorRateGate,
|
|
1186
2019
|
evaluateScorerErrorRateGate,
|
|
1187
2020
|
evaluateScorerTolerance,
|
|
2021
|
+
isBudgetStopped,
|
|
1188
2022
|
llmScorer,
|
|
1189
2023
|
normalizeScorerResult,
|
|
1190
2024
|
pairedBootstrapCI,
|
|
2025
|
+
parseRequestRecords,
|
|
2026
|
+
readAccounting,
|
|
2027
|
+
refusedWork,
|
|
1191
2028
|
rescore,
|
|
2029
|
+
resolveCaptureLimits,
|
|
1192
2030
|
runEval,
|
|
1193
|
-
scorer
|
|
2031
|
+
scorer,
|
|
2032
|
+
serializeRequestRecords,
|
|
2033
|
+
toDiagnosticManifest,
|
|
2034
|
+
unavailableManifest,
|
|
2035
|
+
validateRequestSidecar
|
|
1194
2036
|
});
|
|
1195
2037
|
//# sourceMappingURL=index.cjs.map
|