@axlsdk/eval 0.23.3 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -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/cli.js
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
aggregateRuns,
|
|
4
|
+
compareItemErrorRates,
|
|
5
|
+
describeItemErrorRate,
|
|
4
6
|
evalCompare,
|
|
7
|
+
evaluateItemErrorRateGate,
|
|
5
8
|
evaluateScorerErrorRateGate,
|
|
9
|
+
formatPercent,
|
|
10
|
+
isBudgetStopped,
|
|
11
|
+
isErrorRateLimit,
|
|
12
|
+
readAccounting,
|
|
6
13
|
rescore,
|
|
7
14
|
runEval,
|
|
8
|
-
scorerCounts
|
|
9
|
-
|
|
15
|
+
scorerCounts,
|
|
16
|
+
serializeRequestRecords
|
|
17
|
+
} from "./chunk-HKEAPM4B.js";
|
|
10
18
|
|
|
11
19
|
// src/cli.ts
|
|
12
20
|
import { readdirSync, statSync } from "fs";
|
|
@@ -71,6 +79,12 @@ function validateEvalConfig(cfg) {
|
|
|
71
79
|
return `exports an invalid failOnScorerErrorRate (${typeof v === "number" ? v : typeof v}) \u2014 must be a number between 0 and 1.`;
|
|
72
80
|
}
|
|
73
81
|
}
|
|
82
|
+
{
|
|
83
|
+
const v = c.failOnItemErrorRate;
|
|
84
|
+
if (v != null && !isErrorRateLimit(v)) {
|
|
85
|
+
return `exports an invalid failOnItemErrorRate (${typeof v === "number" ? v : typeof v}) \u2014 must be a number between 0 and 1 (1 disables the gate).`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
74
88
|
return void 0;
|
|
75
89
|
}
|
|
76
90
|
|
|
@@ -83,8 +97,11 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
83
97
|
"--threshold",
|
|
84
98
|
"--runs",
|
|
85
99
|
"--capture-traces",
|
|
100
|
+
"--capture-requests",
|
|
86
101
|
"--concurrency",
|
|
87
|
-
"--scorers"
|
|
102
|
+
"--scorers",
|
|
103
|
+
"--budget",
|
|
104
|
+
"--max-item-error-rate"
|
|
88
105
|
]);
|
|
89
106
|
var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
90
107
|
"--output",
|
|
@@ -92,7 +109,9 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
92
109
|
"--conditions",
|
|
93
110
|
"--runs",
|
|
94
111
|
"--concurrency",
|
|
95
|
-
"--scorers"
|
|
112
|
+
"--scorers",
|
|
113
|
+
"--budget",
|
|
114
|
+
"--max-item-error-rate"
|
|
96
115
|
]);
|
|
97
116
|
function envInt(name) {
|
|
98
117
|
const raw = process.env[name];
|
|
@@ -101,14 +120,25 @@ function envInt(name) {
|
|
|
101
120
|
if (!Number.isFinite(n) || n <= 0) return void 0;
|
|
102
121
|
return n;
|
|
103
122
|
}
|
|
123
|
+
function parseErrorRateFlag(flag, raw) {
|
|
124
|
+
const n = /^\d*\.?\d+$/.test(raw.trim()) ? Number(raw) : NaN;
|
|
125
|
+
if (!Number.isFinite(n) || n < 0 || n > 1) {
|
|
126
|
+
console.error(`Error: ${flag} must be a number in [0, 1], got "${raw}"`);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
return n;
|
|
130
|
+
}
|
|
104
131
|
function parseEvalArgs(args) {
|
|
105
132
|
let outputPath;
|
|
106
133
|
let configArg;
|
|
107
134
|
let conditions = [];
|
|
108
135
|
let runs = 1;
|
|
109
136
|
let captureTraces = false;
|
|
137
|
+
let captureRequests = false;
|
|
110
138
|
let concurrency;
|
|
111
139
|
let scorerNames;
|
|
140
|
+
let budget;
|
|
141
|
+
let maxItemErrorRate;
|
|
112
142
|
const paths = [];
|
|
113
143
|
for (let i = 0; i < args.length; i++) {
|
|
114
144
|
const arg = args[i];
|
|
@@ -129,6 +159,10 @@ function parseEvalArgs(args) {
|
|
|
129
159
|
} else {
|
|
130
160
|
concurrency = n;
|
|
131
161
|
}
|
|
162
|
+
} else if (arg === "--budget") {
|
|
163
|
+
budget = value;
|
|
164
|
+
} else if (arg === "--max-item-error-rate") {
|
|
165
|
+
maxItemErrorRate = parseErrorRateFlag(arg, value);
|
|
132
166
|
} else if (arg === "--scorers") {
|
|
133
167
|
scorerNames = [
|
|
134
168
|
...new Set(
|
|
@@ -140,6 +174,8 @@ function parseEvalArgs(args) {
|
|
|
140
174
|
}
|
|
141
175
|
} else if (arg === "--capture-traces") {
|
|
142
176
|
captureTraces = true;
|
|
177
|
+
} else if (arg === "--capture-requests") {
|
|
178
|
+
captureRequests = true;
|
|
143
179
|
} else if (arg.startsWith("--")) {
|
|
144
180
|
if (!KNOWN_FLAGS.has(arg)) {
|
|
145
181
|
console.error(`Unknown flag: ${arg}`);
|
|
@@ -155,13 +191,90 @@ function parseEvalArgs(args) {
|
|
|
155
191
|
conditions,
|
|
156
192
|
runs,
|
|
157
193
|
captureTraces,
|
|
194
|
+
captureRequests,
|
|
158
195
|
concurrency,
|
|
159
196
|
scorerNames,
|
|
197
|
+
budget,
|
|
198
|
+
maxItemErrorRate,
|
|
160
199
|
paths
|
|
161
200
|
};
|
|
162
201
|
}
|
|
163
202
|
|
|
164
203
|
// src/cli-format.ts
|
|
204
|
+
function formatKnownSpend(accounting) {
|
|
205
|
+
const cost = `$${accounting.knownCost.toFixed(2)}`;
|
|
206
|
+
if (accounting.completeness === "complete") return cost;
|
|
207
|
+
if (accounting.completeness === "unverified") return `${cost} (unverified)`;
|
|
208
|
+
const reasons = Object.entries(accounting.reasons).map(([reason, count]) => `${count} ${reason}`).join(", ");
|
|
209
|
+
return `${cost} (incomplete: ${reasons || "unknown spend"})`;
|
|
210
|
+
}
|
|
211
|
+
function formatBudgetLine(accounting) {
|
|
212
|
+
const budget = accounting.budget;
|
|
213
|
+
if (!budget) return void 0;
|
|
214
|
+
const limit = `$${budget.limit.toFixed(2)}`;
|
|
215
|
+
const spent = `$${budget.knownSpend.toFixed(2)}`;
|
|
216
|
+
if (budget.status === "open") {
|
|
217
|
+
return ` Budget: ${spent} of ${limit} (open)`;
|
|
218
|
+
}
|
|
219
|
+
const by = budget.closedBy ? `, first observed by ${budget.closedBy}` : "";
|
|
220
|
+
return ` Budget: STOPPED \u2014 ${spent} known spend against a ${limit} limit, $${budget.knownOvershoot.toFixed(2)} over${by}`;
|
|
221
|
+
}
|
|
222
|
+
function formatCoverageLine(result) {
|
|
223
|
+
const items = result.summary.coverage?.items;
|
|
224
|
+
if (!items) return void 0;
|
|
225
|
+
const parts = [
|
|
226
|
+
["failed", items.failed],
|
|
227
|
+
["cancelled", items.cancelled],
|
|
228
|
+
["budget-skipped", items.budget_skipped],
|
|
229
|
+
["budget-interrupted", items.budget_interrupted]
|
|
230
|
+
].filter(([, n]) => n > 0).map(([label, n]) => `${n} ${label}`);
|
|
231
|
+
if (parts.length === 0) return void 0;
|
|
232
|
+
const rate = result.summary.itemErrorRate;
|
|
233
|
+
const rateSuffix = rate ? ` \u2014 item error rate ${formatPercent(rate.rate)} (limit ${formatPercent(rate.limit)})` : "";
|
|
234
|
+
const line = ` Items: ${items.completed} completed, ${parts.join(", ")}${rateSuffix}`;
|
|
235
|
+
const causes = formatFailureCauses(result);
|
|
236
|
+
return causes ? `${line}
|
|
237
|
+
${causes}` : line;
|
|
238
|
+
}
|
|
239
|
+
function formatFailureCauses(result) {
|
|
240
|
+
const counts = /* @__PURE__ */ new Map();
|
|
241
|
+
for (const item of result.items) {
|
|
242
|
+
if (item.outcome !== "failed") continue;
|
|
243
|
+
const f = item.failure;
|
|
244
|
+
let label = "other";
|
|
245
|
+
if (f?.status !== void 0) {
|
|
246
|
+
const status = f.status === 0 ? "network" : String(f.status);
|
|
247
|
+
label = f.provider ? `${status} (${f.provider})` : status;
|
|
248
|
+
}
|
|
249
|
+
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
250
|
+
}
|
|
251
|
+
if (counts.size === 0) return void 0;
|
|
252
|
+
const groups = [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([label, n]) => `${n} \xD7 ${label}`);
|
|
253
|
+
return ` Failure causes: ${groups.join(", ")}`;
|
|
254
|
+
}
|
|
255
|
+
function itemErrorRateMessage(result, label) {
|
|
256
|
+
const rate = result.summary.itemErrorRate;
|
|
257
|
+
if (!rate?.exceeded) return null;
|
|
258
|
+
return `[axl-eval] ITEM ERROR RATE EXCEEDED: ${label} \u2014 ${rate.failed} of ${rate.attempted} attempted item(s) failed in the workflow (item error rate ${formatPercent(rate.rate)}), over the ${formatPercent(rate.limit)} limit; the scores cover only the surviving items. Raise failOnItemErrorRate or pass --max-item-error-rate <0..1> to accept it (1 disables the gate).`;
|
|
259
|
+
}
|
|
260
|
+
function budgetStopMessage(result, label) {
|
|
261
|
+
const budget = readAccounting(result).budget;
|
|
262
|
+
if (!budget || !isBudgetStopped({ budget, coverage: result.summary.coverage })) return null;
|
|
263
|
+
const items = result.summary.coverage?.items;
|
|
264
|
+
const stopped = items ? ` ${items.budget_skipped} case(s) never started, ${items.budget_interrupted} stopped mid-flight, ${items.completed} completed.` : "";
|
|
265
|
+
return `[axl-eval] BUDGET STOPPED: ${label} \u2014 known spend $${budget.knownSpend.toFixed(2)} reached the $${budget.limit.toFixed(2)} limit (over by $${budget.knownOvershoot.toFixed(2)}).${stopped} The run is incomplete by design; this is NOT a model or scorer failure.`;
|
|
266
|
+
}
|
|
267
|
+
function isTotalWipeout(result) {
|
|
268
|
+
const { count, failures } = result.summary;
|
|
269
|
+
if (count === 0) return false;
|
|
270
|
+
const coverage = result.summary.coverage?.items;
|
|
271
|
+
if (coverage) {
|
|
272
|
+
const budgetStopped = coverage.budget_skipped + coverage.budget_interrupted;
|
|
273
|
+
if (coverage.completed > 0 || budgetStopped > 0) return false;
|
|
274
|
+
return coverage.failed === count;
|
|
275
|
+
}
|
|
276
|
+
return failures >= count;
|
|
277
|
+
}
|
|
165
278
|
function formatModelTimingLines(modelTiming, nameWidth) {
|
|
166
279
|
if (!modelTiming) return [];
|
|
167
280
|
return Object.entries(modelTiming).map(([model, t]) => {
|
|
@@ -171,6 +284,7 @@ function formatModelTimingLines(modelTiming, nameWidth) {
|
|
|
171
284
|
}
|
|
172
285
|
parts.push(`queued ${formatMs(t.queuedMs.mean)}`);
|
|
173
286
|
parts.push(`retries ${formatMs(t.retryMs.mean)}`);
|
|
287
|
+
if (t.rateLimitRetries) parts.push(`rate-limited ${t.rateLimitRetries}\xD7`);
|
|
174
288
|
const calls = `${t.calls} call${t.calls === 1 ? "" : "s"}`;
|
|
175
289
|
return ` ${model.padEnd(Math.max(nameWidth - 2, 0))} ${parts.join(" \xB7 ")} (${calls}, mean/p95 per call)`;
|
|
176
290
|
});
|
|
@@ -231,7 +345,16 @@ Usage:
|
|
|
231
345
|
(success + failure). Adds memory
|
|
232
346
|
overhead proportional to dataset size
|
|
233
347
|
x turns x agents; off by default.
|
|
348
|
+
axl-eval <path> --budget <amount> Stop admitting spend once known cost
|
|
349
|
+
reaches the limit (e.g. "$1", "0.50").
|
|
350
|
+
Overrides the eval file's budget and
|
|
351
|
+
applies PER RUN under --runs. A
|
|
352
|
+
budget-stopped run exits 1 with a
|
|
353
|
+
distinct BUDGET STOPPED line.
|
|
234
354
|
axl-eval rescore <results> <eval-file> Re-run scorers on saved outputs
|
|
355
|
+
axl-eval rescore <results> <eval-file> --budget <amount> Limit the NEW judging
|
|
356
|
+
spend only; the source run's cost is
|
|
357
|
+
not counted against it.
|
|
235
358
|
axl-eval compare <a> <b> Compare two eval result files
|
|
236
359
|
axl-eval compare <a> <b> --threshold <v> Set regression threshold (global or per-scorer)
|
|
237
360
|
axl-eval compare <a> <b> --fail-on-regression Exit 1 if regressions
|
|
@@ -246,7 +369,8 @@ Config auto-detection (when --config is not specified):
|
|
|
246
369
|
${CONFIG_CANDIDATES.join(" -> ")}
|
|
247
370
|
|
|
248
371
|
When a config is found, the exported AxlRuntime is passed to executeWorkflow
|
|
249
|
-
and
|
|
372
|
+
and spend is measured by the runner's accounting scope (a "cost" your callback
|
|
373
|
+
returns is recorded under item.callerReport, never used as the measurement).
|
|
250
374
|
When no config is found, a bare AxlRuntime is created (providers from env vars).
|
|
251
375
|
`);
|
|
252
376
|
process.exit(0);
|
|
@@ -296,17 +420,27 @@ function scorerFilteredScorers(r) {
|
|
|
296
420
|
if (!meta || meta.scorerFiltered !== true) return void 0;
|
|
297
421
|
return Array.isArray(meta.scorersRun) ? meta.scorersRun : [];
|
|
298
422
|
}
|
|
423
|
+
function parseMaxItemErrorRate(args) {
|
|
424
|
+
const idx = args.indexOf("--max-item-error-rate");
|
|
425
|
+
if (idx === -1) return void 0;
|
|
426
|
+
if (idx + 1 >= args.length) {
|
|
427
|
+
console.error("Error: --max-item-error-rate requires a value in [0, 1]");
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
430
|
+
return parseErrorRateFlag("--max-item-error-rate", args[idx + 1]);
|
|
431
|
+
}
|
|
299
432
|
async function runCompare(args) {
|
|
300
433
|
const failOnRegression = args.includes("--fail-on-regression");
|
|
301
434
|
const thresholds = parseThresholdArg(args);
|
|
302
435
|
const maxScorerErrorRate = parseMaxScorerErrorRate(args);
|
|
303
|
-
const
|
|
436
|
+
const maxItemErrorRate = parseMaxItemErrorRate(args);
|
|
437
|
+
const valueFlags = /* @__PURE__ */ new Set(["--threshold", "--max-scorer-error-rate", "--max-item-error-rate"]);
|
|
304
438
|
const files = args.filter(
|
|
305
439
|
(a, i) => !a.startsWith("--") && !(i > 0 && valueFlags.has(args[i - 1]))
|
|
306
440
|
);
|
|
307
441
|
if (files.length !== 2) {
|
|
308
442
|
console.error(
|
|
309
|
-
"Usage: axl-eval compare <baseline.json> <candidate.json> [--threshold <value>] [--fail-on-regression] [--max-scorer-error-rate <0..1>]"
|
|
443
|
+
"Usage: axl-eval compare <baseline.json> <candidate.json> [--threshold <value>] [--fail-on-regression] [--max-scorer-error-rate <0..1>] [--max-item-error-rate <0..1>]"
|
|
310
444
|
);
|
|
311
445
|
process.exit(1);
|
|
312
446
|
}
|
|
@@ -371,9 +505,13 @@ Compare: baseline (${comparison.baseline.id.slice(0, 8)}) -> candidate (${compar
|
|
|
371
505
|
if (comparison.cost) {
|
|
372
506
|
const c = comparison.cost;
|
|
373
507
|
const sign = c.delta > 0 ? "+" : "";
|
|
508
|
+
const change = c.deltaPercent === null ? `${sign}$${Math.abs(c.delta).toFixed(2)}, no % from a $0 baseline` : `${sign}${c.deltaPercent.toFixed(1)}%`;
|
|
374
509
|
console.log(
|
|
375
|
-
` Cost: baseline $${c.baselineTotal.toFixed(2)} -> candidate $${c.candidateTotal.toFixed(2)} (${
|
|
510
|
+
` Cost: baseline $${c.baselineTotal.toFixed(2)} -> candidate $${c.candidateTotal.toFixed(2)} (${change})`
|
|
376
511
|
);
|
|
512
|
+
if (!c.certified) {
|
|
513
|
+
console.log(` not certified: ${c.reason ?? "costs are not comparable"}`);
|
|
514
|
+
}
|
|
377
515
|
}
|
|
378
516
|
const baselineRef = Array.isArray(baseline) ? baseline[0] : baseline;
|
|
379
517
|
const stable = Math.max(
|
|
@@ -418,6 +556,17 @@ Compare: baseline (${comparison.baseline.id.slice(0, 8)}) -> candidate (${compar
|
|
|
418
556
|
const reason = evaluateScorerErrorRateGate(comparison, maxScorerErrorRate);
|
|
419
557
|
if (reason) refuseToGate(reason);
|
|
420
558
|
}
|
|
559
|
+
const itemRates = compareItemErrorRates(baseline, candidate, maxItemErrorRate);
|
|
560
|
+
const multiRunCompare = itemRates.some((r) => r.runIndex > 0);
|
|
561
|
+
for (const rate of itemRates) {
|
|
562
|
+
if (rate.failed > 0 && !rate.exceeded) {
|
|
563
|
+
console.error(
|
|
564
|
+
`[axl-eval] WARNING: ${describeItemErrorRate(rate, multiRunCompare)}, within the ${formatPercent(rate.limit)} limit; its scores cover only the surviving items.`
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const coverageReason = evaluateItemErrorRateGate(baseline, candidate, maxItemErrorRate);
|
|
569
|
+
if (coverageReason) refuseToGate(coverageReason);
|
|
421
570
|
if (failOnRegression && comparison.regressions.length > 0) {
|
|
422
571
|
const hasSignificance = scorerNames.some((n) => comparison.scorers[n].significant != null);
|
|
423
572
|
if (hasSignificance) {
|
|
@@ -431,7 +580,16 @@ Compare: baseline (${comparison.baseline.id.slice(0, 8)}) -> candidate (${compar
|
|
|
431
580
|
}
|
|
432
581
|
}
|
|
433
582
|
async function runRescore(args, signal) {
|
|
434
|
-
const {
|
|
583
|
+
const {
|
|
584
|
+
outputPath,
|
|
585
|
+
configArg,
|
|
586
|
+
conditions,
|
|
587
|
+
concurrency,
|
|
588
|
+
scorerNames,
|
|
589
|
+
budget,
|
|
590
|
+
maxItemErrorRate,
|
|
591
|
+
paths
|
|
592
|
+
} = parseEvalArgs(args);
|
|
435
593
|
if (paths.length < 2) {
|
|
436
594
|
console.error("Usage: axl-eval rescore <results.json> <eval-file> [--output <file>]");
|
|
437
595
|
process.exit(1);
|
|
@@ -442,11 +600,18 @@ async function runRescore(args, signal) {
|
|
|
442
600
|
);
|
|
443
601
|
process.exit(1);
|
|
444
602
|
}
|
|
603
|
+
if (maxItemErrorRate != null) {
|
|
604
|
+
console.error(
|
|
605
|
+
"Error: --max-item-error-rate is not supported with rescore; item failures belong to the source run \u2014 gate them with `axl-eval compare`."
|
|
606
|
+
);
|
|
607
|
+
process.exit(1);
|
|
608
|
+
}
|
|
445
609
|
const itemConcurrency = concurrency ?? envInt("AXL_EVAL_CONCURRENCY");
|
|
446
610
|
const [resultsPath, evalFilePath] = paths;
|
|
447
611
|
const raw = JSON.parse(await readFileAsync(resultsPath, "utf-8"));
|
|
448
612
|
const results = Array.isArray(raw) ? raw : [raw];
|
|
449
613
|
const runtime = await getRuntime(configArg, conditions);
|
|
614
|
+
let exitCode = 0;
|
|
450
615
|
try {
|
|
451
616
|
const mod = await importModule(path.resolve(evalFilePath), import.meta.url);
|
|
452
617
|
const evalConfig = pickDefault(mod);
|
|
@@ -461,29 +626,49 @@ async function runRescore(args, signal) {
|
|
|
461
626
|
process.exit(1);
|
|
462
627
|
}
|
|
463
628
|
const rescored = [];
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
629
|
+
let budgetUsable = true;
|
|
630
|
+
try {
|
|
631
|
+
for (const resultData of results) {
|
|
632
|
+
if (signal.aborted) break;
|
|
633
|
+
rescored.push(
|
|
634
|
+
await rescore(resultData, evalConfig.scorers, runtime, {
|
|
635
|
+
signal,
|
|
636
|
+
...itemConcurrency != null ? { concurrency: itemConcurrency } : {},
|
|
637
|
+
// Gates the NEW judging spend only — the source run's cost is history
|
|
638
|
+
// and is not counted against this limit.
|
|
639
|
+
...budget != null ? { budget } : {}
|
|
640
|
+
})
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
} catch (err) {
|
|
644
|
+
if (err?.code === "INVALID_BUDGET") {
|
|
645
|
+
console.error(`[axl-eval] ${err instanceof Error ? err.message : String(err)}`);
|
|
646
|
+
exitCode = 1;
|
|
647
|
+
budgetUsable = false;
|
|
648
|
+
} else {
|
|
649
|
+
throw err;
|
|
650
|
+
}
|
|
475
651
|
}
|
|
476
|
-
if (
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
652
|
+
if (budgetUsable) {
|
|
653
|
+
for (const r of rescored) {
|
|
654
|
+
console.log("\n" + formatTable(r) + "\n");
|
|
655
|
+
}
|
|
656
|
+
if (outputPath) {
|
|
657
|
+
const output = Array.isArray(raw) ? rescored : rescored[0];
|
|
658
|
+
const outputDir = path.dirname(path.resolve(outputPath));
|
|
659
|
+
await mkdir(outputDir, { recursive: true });
|
|
660
|
+
await writeFileAsync(outputPath, JSON.stringify(output, null, 2), "utf-8");
|
|
661
|
+
console.log(`Rescored results saved to ${outputPath}`);
|
|
662
|
+
}
|
|
663
|
+
for (const r of rescored) {
|
|
664
|
+
if (reportBudgetStop(r, `rescore ${resultsPath}`)) exitCode = 1;
|
|
665
|
+
}
|
|
482
666
|
}
|
|
483
667
|
} finally {
|
|
484
668
|
await runtime.shutdown().catch(() => {
|
|
485
669
|
});
|
|
486
670
|
}
|
|
671
|
+
if (exitCode !== 0) process.exit(exitCode);
|
|
487
672
|
}
|
|
488
673
|
function collectEvalFiles(p) {
|
|
489
674
|
if (p.includes("*")) return expandGlob(p, process.cwd());
|
|
@@ -541,11 +726,15 @@ function formatTable(result) {
|
|
|
541
726
|
}
|
|
542
727
|
lines.push(...formatModelTimingLines(result.summary.modelTiming, maxNameLen));
|
|
543
728
|
const durationSec = (result.duration / 1e3).toFixed(1);
|
|
544
|
-
const
|
|
729
|
+
const accounting = readAccounting(result);
|
|
545
730
|
lines.push("");
|
|
546
731
|
lines.push(
|
|
547
|
-
` Failures: ${result.summary.failures}/${result.summary.count} | Cost: ${
|
|
732
|
+
` Failures: ${result.summary.failures}/${result.summary.count} | Cost: ${formatKnownSpend(accounting)} | Duration: ${durationSec}s`
|
|
548
733
|
);
|
|
734
|
+
const budgetLine = formatBudgetLine(accounting);
|
|
735
|
+
if (budgetLine) lines.push(budgetLine);
|
|
736
|
+
const coverageLine = formatCoverageLine(result);
|
|
737
|
+
if (coverageLine) lines.push(coverageLine);
|
|
549
738
|
const itemsWithErrors = result.items.filter((i) => i.scorerErrors?.length);
|
|
550
739
|
if (itemsWithErrors.length > 0) {
|
|
551
740
|
const uniqueErrors = [...new Set(itemsWithErrors.flatMap((i) => i.scorerErrors))];
|
|
@@ -589,10 +778,11 @@ function formatMultiRunTable(summary) {
|
|
|
589
778
|
` ${"Timing".padEnd(maxNameLen)} ${((summary.timing.mean / 1e3).toFixed(2) + " \xB1 " + (summary.timing.std / 1e3).toFixed(2) + "s").padStart(colWidth)}`
|
|
590
779
|
);
|
|
591
780
|
}
|
|
592
|
-
const costStr = summary.totalCost > 0 ? `$${summary.totalCost.toFixed(2)}` : "$0.00";
|
|
593
781
|
const durationStr = (summary.totalDuration / 1e3).toFixed(1);
|
|
594
782
|
lines.push("");
|
|
595
|
-
lines.push(
|
|
783
|
+
lines.push(
|
|
784
|
+
` Total Cost: ${formatKnownSpend(summary.accounting)} | Total Duration: ${durationStr}s`
|
|
785
|
+
);
|
|
596
786
|
return lines.join("\n");
|
|
597
787
|
}
|
|
598
788
|
function reportDegraded(result, label) {
|
|
@@ -617,11 +807,22 @@ function reportFullySkippedScorers(result, label) {
|
|
|
617
807
|
}
|
|
618
808
|
}
|
|
619
809
|
}
|
|
810
|
+
function reportBudgetStop(result, label) {
|
|
811
|
+
const message = budgetStopMessage(result, label);
|
|
812
|
+
if (!message) return false;
|
|
813
|
+
console.error(message);
|
|
814
|
+
return true;
|
|
815
|
+
}
|
|
816
|
+
function reportItemErrorRate(result, label, wipeout) {
|
|
817
|
+
const message = itemErrorRateMessage(result, label);
|
|
818
|
+
if (!message) return false;
|
|
819
|
+
if (!wipeout) console.error(message);
|
|
820
|
+
return true;
|
|
821
|
+
}
|
|
620
822
|
function reportTotalWipeout(result, label) {
|
|
621
|
-
|
|
622
|
-
if (count === 0 || failures < count) return false;
|
|
823
|
+
if (!isTotalWipeout(result)) return false;
|
|
623
824
|
console.error(
|
|
624
|
-
`[axl-eval] FAILED: ${label} \u2014 all ${count} item(s) errored in the workflow (0 succeeded); the eval produced no scorable output.`
|
|
825
|
+
`[axl-eval] FAILED: ${label} \u2014 all ${result.summary.count} item(s) errored in the workflow (0 succeeded); the eval produced no scorable output.`
|
|
625
826
|
);
|
|
626
827
|
return true;
|
|
627
828
|
}
|
|
@@ -685,6 +886,27 @@ async function getRuntime(configArg, conditions) {
|
|
|
685
886
|
const { AxlRuntime } = await import("@axlsdk/axl");
|
|
686
887
|
return new AxlRuntime();
|
|
687
888
|
}
|
|
889
|
+
async function writeRequestSidecar(runtime, outputPath, results) {
|
|
890
|
+
const artifactIds = results.map((result) => result.diagnostics?.artifactId).filter((id) => typeof id === "string" && id !== "");
|
|
891
|
+
if (artifactIds.length === 0) return;
|
|
892
|
+
const store = runtime.getDiagnosticArtifactStore();
|
|
893
|
+
if (!store) return;
|
|
894
|
+
const records = [];
|
|
895
|
+
for (const artifactId of artifactIds) {
|
|
896
|
+
const opened = await store.open(artifactId).catch(() => void 0);
|
|
897
|
+
if (!opened) continue;
|
|
898
|
+
for await (const line of opened.lines) {
|
|
899
|
+
try {
|
|
900
|
+
records.push(JSON.parse(line));
|
|
901
|
+
} catch {
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
if (records.length === 0) return;
|
|
906
|
+
const sidecarPath = outputPath.replace(/\.json$/i, "") + ".requests.jsonl";
|
|
907
|
+
await writeFileAsync(sidecarPath, serializeRequestRecords(records), "utf-8");
|
|
908
|
+
console.log(`Captured requests saved to ${sidecarPath}`);
|
|
909
|
+
}
|
|
688
910
|
async function runEvalCommand(args, signal) {
|
|
689
911
|
const {
|
|
690
912
|
outputPath,
|
|
@@ -692,8 +914,11 @@ async function runEvalCommand(args, signal) {
|
|
|
692
914
|
conditions,
|
|
693
915
|
runs,
|
|
694
916
|
captureTraces,
|
|
917
|
+
captureRequests,
|
|
695
918
|
concurrency,
|
|
696
919
|
scorerNames,
|
|
920
|
+
budget,
|
|
921
|
+
maxItemErrorRate,
|
|
697
922
|
paths
|
|
698
923
|
} = parseEvalArgs(args);
|
|
699
924
|
if (paths.length === 0) {
|
|
@@ -728,6 +953,8 @@ async function runEvalCommand(args, signal) {
|
|
|
728
953
|
continue;
|
|
729
954
|
}
|
|
730
955
|
evalConfig.concurrency = concurrency ?? envInt("AXL_EVAL_CONCURRENCY") ?? evalConfig.concurrency ?? 5;
|
|
956
|
+
if (budget != null) evalConfig.budget = budget;
|
|
957
|
+
if (maxItemErrorRate != null) evalConfig.failOnItemErrorRate = maxItemErrorRate;
|
|
731
958
|
if (scorerNames?.length) {
|
|
732
959
|
const available = evalConfig.scorers.map((s) => s.name);
|
|
733
960
|
const unknown = scorerNames.filter((n) => !available.includes(n));
|
|
@@ -756,27 +983,11 @@ async function runEvalCommand(args, signal) {
|
|
|
756
983
|
}
|
|
757
984
|
let executeWorkflow;
|
|
758
985
|
if (customExecute) {
|
|
759
|
-
executeWorkflow = async (input, rt) =>
|
|
760
|
-
const {
|
|
761
|
-
result,
|
|
762
|
-
cost: trackedCost,
|
|
763
|
-
metadata
|
|
764
|
-
} = await runtime.trackExecution(async () => {
|
|
765
|
-
return customExecute(input, rt);
|
|
766
|
-
});
|
|
767
|
-
return {
|
|
768
|
-
output: result.output,
|
|
769
|
-
cost: result.cost ?? trackedCost,
|
|
770
|
-
metadata: result.metadata ?? metadata
|
|
771
|
-
};
|
|
772
|
-
};
|
|
986
|
+
executeWorkflow = async (input, rt) => customExecute(input, rt);
|
|
773
987
|
} else if (runtime.getWorkflow(evalConfig.workflow)) {
|
|
774
|
-
executeWorkflow = async (input) => {
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
});
|
|
778
|
-
return { output: result, cost, metadata };
|
|
779
|
-
};
|
|
988
|
+
executeWorkflow = async (input) => ({
|
|
989
|
+
output: await runtime.execute(evalConfig.workflow, input)
|
|
990
|
+
});
|
|
780
991
|
} else {
|
|
781
992
|
const exportShape = (() => {
|
|
782
993
|
const top = Object.keys(mod).filter((k) => k !== "default");
|
|
@@ -798,7 +1009,11 @@ async function runEvalCommand(args, signal) {
|
|
|
798
1009
|
failedFiles++;
|
|
799
1010
|
continue;
|
|
800
1011
|
}
|
|
801
|
-
const runOptions = {
|
|
1012
|
+
const runOptions = {
|
|
1013
|
+
signal,
|
|
1014
|
+
...captureTraces ? { captureTraces: true } : {},
|
|
1015
|
+
...captureRequests ? { captureRequests: true } : {}
|
|
1016
|
+
};
|
|
802
1017
|
const scorersRun = scorerNames?.length ? evalConfig.scorers.map((s) => s.name) : void 0;
|
|
803
1018
|
const stampFiltered = (result) => {
|
|
804
1019
|
if (scorersRun) {
|
|
@@ -851,11 +1066,13 @@ async function runEvalCommand(args, signal) {
|
|
|
851
1066
|
console.log("\n" + formatMultiRunTable(summary) + "\n");
|
|
852
1067
|
for (const r of runResults) results.push(r);
|
|
853
1068
|
let anyFailing = false;
|
|
854
|
-
for (const r of runResults) {
|
|
1069
|
+
for (const [i, r] of runResults.entries()) {
|
|
1070
|
+
const budgetStopped = reportBudgetStop(r, filePath);
|
|
855
1071
|
const wipeout = reportTotalWipeout(r, filePath);
|
|
1072
|
+
const thinned = reportItemErrorRate(r, `${filePath} run ${i + 1}/${runs}`, wipeout);
|
|
856
1073
|
const degraded = reportDegraded(r, filePath);
|
|
857
1074
|
reportFullySkippedScorers(r, filePath);
|
|
858
|
-
if (wipeout || degraded) anyFailing = true;
|
|
1075
|
+
if (budgetStopped || wipeout || thinned || degraded) anyFailing = true;
|
|
859
1076
|
}
|
|
860
1077
|
if (anyFailing && !partial) failedFiles++;
|
|
861
1078
|
} else {
|
|
@@ -863,10 +1080,12 @@ async function runEvalCommand(args, signal) {
|
|
|
863
1080
|
stampFiltered(result);
|
|
864
1081
|
results.push(result);
|
|
865
1082
|
console.log("\n" + formatTable(result) + "\n");
|
|
1083
|
+
const budgetStopped = reportBudgetStop(result, filePath);
|
|
866
1084
|
const wipeout = reportTotalWipeout(result, filePath);
|
|
1085
|
+
const thinned = reportItemErrorRate(result, filePath, wipeout);
|
|
867
1086
|
const degraded = reportDegraded(result, filePath);
|
|
868
1087
|
reportFullySkippedScorers(result, filePath);
|
|
869
|
-
if (wipeout || degraded) failedFiles++;
|
|
1088
|
+
if (budgetStopped || wipeout || thinned || degraded) failedFiles++;
|
|
870
1089
|
}
|
|
871
1090
|
} catch (err) {
|
|
872
1091
|
console.error(
|
|
@@ -881,6 +1100,7 @@ async function runEvalCommand(args, signal) {
|
|
|
881
1100
|
await mkdir(outputDir, { recursive: true });
|
|
882
1101
|
await writeFileAsync(outputPath, JSON.stringify(output, null, 2), "utf-8");
|
|
883
1102
|
console.log(`Results saved to ${outputPath}`);
|
|
1103
|
+
await writeRequestSidecar(runtime, outputPath, results);
|
|
884
1104
|
}
|
|
885
1105
|
} finally {
|
|
886
1106
|
await runtime.shutdown().catch(() => {
|