@axlsdk/eval 0.17.9 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -7
- package/dist/{chunk-NETD4AUB.js → chunk-BWBX5YVQ.js} +231 -162
- package/dist/chunk-BWBX5YVQ.js.map +1 -0
- package/dist/cli.cjs +497 -207
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +279 -47
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +301 -174
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +176 -1
- package/dist/index.d.ts +176 -1
- package/dist/index.js +79 -16
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/dist/chunk-NETD4AUB.js.map +0 -1
package/README.md
CHANGED
|
@@ -101,6 +101,8 @@ const ds = dataset({
|
|
|
101
101
|
|
|
102
102
|
You can also load from a file: `dataset({ name: 'large', schema, file: './data.json' })`.
|
|
103
103
|
|
|
104
|
+
> **Annotation keys must be in the schema.** Zod strips keys it doesn't declare, so an annotation field missing from the `annotations` schema is dropped before it reaches your scorers — and a scorer reading `annotations?.thatField` then silently sees `undefined` and becomes a no-op. By default `dataset()` warns once per dataset listing any dropped keys (`annotation key(s) dropped by schema: ...`). Set `onExtraAnnotationKeys: 'error'` to fail instead (recommended in CI), or `'ignore'` to silence. This is the only guard for `file:` datasets, which aren't type-checked.
|
|
105
|
+
|
|
104
106
|
## Scorers
|
|
105
107
|
|
|
106
108
|
Scorers rate each output on a 0-1 scale. You can mix deterministic and LLM scorers in the same eval.
|
|
@@ -223,10 +225,42 @@ npx axl-eval ./evals/ # run all *.eval.* files in
|
|
|
223
225
|
npx axl-eval ./evals/ --output ./results/v1.json # save results to JSON
|
|
224
226
|
npx axl-eval ./evals/ --config ./axl.config.ts # use a specific runtime config
|
|
225
227
|
npx axl-eval ./evals/ --conditions development # add Node.js import conditions (monorepo source exports)
|
|
228
|
+
npx axl-eval ./evals/qa.eval.ts --concurrency 10 # override item concurrency for this run
|
|
229
|
+
npx axl-eval ./evals/qa.eval.ts --scorers accuracy # run only named scorer(s) (single file)
|
|
226
230
|
```
|
|
227
231
|
|
|
228
232
|
The CLI resolves a runtime automatically: `--config <path>` > auto-detect `axl.config.*` > bare `new AxlRuntime()` (providers from env vars). Use `--conditions` when your eval file imports from monorepo packages that use conditional exports (e.g., `"development"` condition for source TypeScript instead of compiled dist).
|
|
229
233
|
|
|
234
|
+
**`--concurrency <n>` / `AXL_EVAL_CONCURRENCY`** override item concurrency per-invocation (precedence: flag > env > `defineEval` value > default 5). Concurrency is pure scheduling — it never changes results. Note the per-eval `scorerConcurrency` (default 5) is independent, so the worst-case number of simultaneous scorer calls is `concurrency × scorerConcurrency`; lower `--concurrency` if a rate-limited judge model needs a tighter ceiling.
|
|
235
|
+
|
|
236
|
+
**`--scorers <a,b>`** runs only the named scorers (single eval file only) for a fast iteration loop. The result is stamped `metadata.scorerFiltered: true` so `axl-eval compare` warns — and, under `--fail-on-regression`, refuses — to gate on a subset run mistaken for a full baseline.
|
|
237
|
+
|
|
238
|
+
#### Scorer failure rate — two complementary gates
|
|
239
|
+
|
|
240
|
+
Concurrent scorers (`concurrency × scorerConcurrency`, up to 25 in-flight by default) can overrun a rate-limited judge model. When a judge call exhausts the provider's retry backoff it throws, the runner records that item's score as `null`, and the mean is computed over the **survivors** — so a thinned sample can still look green. The runner now always reports per-scorer `scored`/`failed` counts (visible in the table as `(18/20 scored, 2 failed)` and in Studio as an amber badge), and two opt-in gates turn that signal into a non-zero exit:
|
|
241
|
+
|
|
242
|
+
| Gate | Where | When it fires |
|
|
243
|
+
|------|-------|---------------|
|
|
244
|
+
| `failOnScorerErrorRate` (config field) | **Source-side** — at run/produce time | `runEval` flags `summary.degraded` (and the CLI exits non-zero) when a scorer exceeds tolerance, *before* the thinned result is saved. `runEval` never throws — it returns the result with the flag set, preserving multi-run partials and auto-save. |
|
|
245
|
+
| `--max-scorer-error-rate <0..1>` (compare flag) | **Gate-side** — at consume/gate time | `axl-eval compare` refuses (exit 1) to certify a baseline/candidate whose failure rate is over the limit. Always *warns* when either side rests on a thinned sample, even without the flag. |
|
|
246
|
+
|
|
247
|
+
Both are **type-aware**: deterministic scorers tolerate **zero** failures (a deterministic scorer that throws is a bug, not noise); LLM judges use the configured rate. They're complementary — set `failOnScorerErrorRate` in the eval file to fail fast at run time, and pass `--max-scorer-error-rate` in CI to refuse to gate on an artifact that was already thinned (e.g. an imported result).
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
// eval file — fail this run if the judge errored on >10% of items
|
|
251
|
+
export default { workflow: 'qa', dataset: ds, scorers: [judge], failOnScorerErrorRate: 0.1 };
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
npx axl-eval compare base.json cand.json --fail-on-regression --max-scorer-error-rate 0.05
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Building a custom CI gate? Both decisions are exported as pure functions — `evaluateScorerErrorRateGate` and `evaluateScorerTolerance` (see the [API reference](../../docs/api-reference.md)).
|
|
259
|
+
|
|
260
|
+
#### Total-workflow-wipeout guard
|
|
261
|
+
|
|
262
|
+
Separate from the scorer signal above: if **every** item errored in the *workflow* (0 succeeded), the eval produced no scorable output, so `axl-eval` always exits non-zero with `FAILED: … all N item(s) errored in the workflow` — a fully-broken eval (bad provider URI, an exception in every run) can never go green in CI. This is non-configurable (a 0%-success eval is unambiguously broken) and distinct from `failOnScorerErrorRate` (which is about a flaky *scorer* and deliberately ignores a run with no scored items). Partial workflow-failure rates stay visible (`Failures: N/M` in the table) but non-gating.
|
|
263
|
+
|
|
230
264
|
### Programmatic
|
|
231
265
|
|
|
232
266
|
**`runtime.eval()`** — when you have a workflow registered on the runtime. The `workflow` field must match the registered name:
|
|
@@ -430,6 +464,7 @@ Compare two runs to detect regressions and improvements. Runs must use the same
|
|
|
430
464
|
```bash
|
|
431
465
|
npx axl-eval compare ./results/v1.json ./results/v2.json
|
|
432
466
|
npx axl-eval compare v1.json v2.json --fail-on-regression # exit 1 if significant regressions
|
|
467
|
+
npx axl-eval compare v1.json v2.json --max-scorer-error-rate 0.05 # exit 1 if a scorer failed on >5% of the items it ran against (deterministic scorers: zero tolerance)
|
|
433
468
|
```
|
|
434
469
|
|
|
435
470
|
```
|
|
@@ -569,18 +604,21 @@ return { output, cost: ctx.totalCost };
|
|
|
569
604
|
|
|
570
605
|
### Common patterns
|
|
571
606
|
|
|
572
|
-
**Concurrency** — process items in parallel (default: 5):
|
|
607
|
+
**Concurrency** — process items in parallel (default: 5), and fan out each item's scorers (default: 5):
|
|
573
608
|
|
|
574
609
|
```typescript
|
|
575
610
|
export default defineEval({
|
|
576
611
|
workflow: 'qa-eval',
|
|
577
612
|
dataset: ds,
|
|
578
|
-
scorers: [qualityJudge],
|
|
579
|
-
concurrency: 10,
|
|
613
|
+
scorers: [qualityJudge, toneJudge, safetyJudge],
|
|
614
|
+
concurrency: 10, // run 10 items in parallel
|
|
615
|
+
scorerConcurrency: 3, // run up to 3 of an item's scorers at once
|
|
580
616
|
budget: '$5.00', // stop if total cost exceeds $5
|
|
581
617
|
});
|
|
582
618
|
```
|
|
583
619
|
|
|
620
|
+
`scorerConcurrency` parallelizes the per-item judge phase — the dominant cost for evals with several `llmScorer`s. The worst-case number of simultaneous scorer calls is `concurrency × scorerConcurrency`, so a rate-limited judge model may need a lower value (set `scorerConcurrency: 1` for the old serial behavior). Because the per-item `budget` check runs once before an item's scorers, scorer-cost overshoot is bounded by `concurrency × scorerConcurrency × max-scorer-cost`.
|
|
621
|
+
|
|
584
622
|
**Per-item budget** — cap cost for a single workflow execution:
|
|
585
623
|
|
|
586
624
|
```typescript
|
|
@@ -620,7 +658,9 @@ npx axl-eval rescore ./results/v1.json ./evals/qa.eval.ts
|
|
|
620
658
|
npx axl-eval rescore ./results/v1.json ./evals/qa.eval.ts --output ./results/v1-rescored.json
|
|
621
659
|
```
|
|
622
660
|
|
|
623
|
-
The rescored result gets a new `id` and has `metadata.rescored: true` and `metadata.originalId` pointing to the source run. `runGroupId` and `
|
|
661
|
+
The rescored result gets a new `id` and has `metadata.rescored: true` and `metadata.originalId` pointing to the source run. `runGroupId`, `runIndex`, and any `scorerFiltered`/`scorersRun` stamp are stripped from inherited metadata (rescored results are independent evaluations, and a rescore always runs the eval file's full scorer set). Only scorer cost is tracked (workflow cost is zero).
|
|
662
|
+
|
|
663
|
+
`--concurrency`/`AXL_EVAL_CONCURRENCY` are honored by `rescore` too (item-level; `scorerConcurrency` keeps its default 5). `--scorers` is **not** supported with `rescore` — it's a run-command-only filter; rescore re-runs whatever scorers the eval file exports (pass a subset there instead).
|
|
624
664
|
|
|
625
665
|
Programmatically:
|
|
626
666
|
|
|
@@ -628,7 +668,8 @@ Programmatically:
|
|
|
628
668
|
import { rescore } from '@axlsdk/eval';
|
|
629
669
|
|
|
630
670
|
const rescored = await rescore(originalResult, [updatedScorer, newScorer], runtime, {
|
|
631
|
-
concurrency: 10,
|
|
671
|
+
concurrency: 10, // items rescored in parallel
|
|
672
|
+
scorerConcurrency: 3, // scorers per item run in parallel (default 5)
|
|
632
673
|
});
|
|
633
674
|
|
|
634
675
|
console.log(rescored.metadata.rescored); // true
|
|
@@ -714,7 +755,7 @@ const comparison = evalCompare(baselineRuns, candidateRuns);
|
|
|
714
755
|
|
|
715
756
|
| Type | Description |
|
|
716
757
|
|------|-------------|
|
|
717
|
-
| `EvalConfig` | Eval definition (workflow, dataset, scorers, concurrency, budget) |
|
|
758
|
+
| `EvalConfig` | Eval definition (workflow, dataset, scorers, concurrency, scorerConcurrency, budget) |
|
|
718
759
|
| `EvalResult` | Full eval output (items, summary, cost, duration) |
|
|
719
760
|
| `EvalItem` | Per-item result (input, output, scores, scoreDetails, metadata, traces?) |
|
|
720
761
|
| `EvalSummary` | Aggregate statistics (count, failures, scorer stats, timing) |
|
|
@@ -724,7 +765,7 @@ const comparison = evalCompare(baselineRuns, candidateRuns);
|
|
|
724
765
|
| `EvalRegression` / `EvalImprovement` | Per-item change record (itemIndex, scorer, scores, delta) |
|
|
725
766
|
| `ScorerDetail` | Per-scorer detail (score, metadata, duration, cost) |
|
|
726
767
|
| `ScorerResult` | Scorer return type (`{ score, metadata?, cost? }`) |
|
|
727
|
-
| `RescoreOptions` | Options for `rescore()` (`concurrency`) |
|
|
768
|
+
| `RescoreOptions` | Options for `rescore()` (`concurrency`, `scorerConcurrency`, `signal`) |
|
|
728
769
|
| `MultiRunSummary` | Aggregated multi-run output (per-scorer mean/std/min/max) |
|
|
729
770
|
| `BootstrapCIResult` | CI result (`{ lower, upper, mean, pRegression, pImprovement }`) |
|
|
730
771
|
| `RunEvalOptions` | Options for `runEval()` (`onProgress`, `signal`, `captureTraces`) |
|
|
@@ -64,10 +64,52 @@ function xorshift32(seed) {
|
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
// src/
|
|
67
|
+
// src/utils.ts
|
|
68
|
+
function computeStats(scores) {
|
|
69
|
+
if (scores.length === 0) return { mean: 0, min: 0, max: 0, p50: 0, p95: 0 };
|
|
70
|
+
const sorted = [...scores].sort((a, b) => a - b);
|
|
71
|
+
const mean = sorted.reduce((a, b) => a + b, 0) / sorted.length;
|
|
72
|
+
const min = sorted[0];
|
|
73
|
+
const max = sorted[sorted.length - 1];
|
|
74
|
+
const p50 = sorted[Math.floor(sorted.length * 0.5)];
|
|
75
|
+
const p95 = sorted[Math.floor(sorted.length * 0.95)];
|
|
76
|
+
return { mean: round2(mean), min: round2(min), max: round2(max), p50: round2(p50), p95: round2(p95) };
|
|
77
|
+
}
|
|
68
78
|
function round2(n) {
|
|
69
79
|
return Math.round(n * 1e3) / 1e3;
|
|
70
80
|
}
|
|
81
|
+
function scorerCounts(items, name) {
|
|
82
|
+
let scored = 0;
|
|
83
|
+
let failed = 0;
|
|
84
|
+
for (const i of items) {
|
|
85
|
+
if (i.error) continue;
|
|
86
|
+
if (i.scores[name] != null) scored++;
|
|
87
|
+
else if (i.scoreDetails?.[name]?.duration != null) failed++;
|
|
88
|
+
}
|
|
89
|
+
return { scored, failed };
|
|
90
|
+
}
|
|
91
|
+
function evaluateScorerTolerance(scored, failed, type, limit) {
|
|
92
|
+
const attempted = scored + failed;
|
|
93
|
+
const rate = attempted === 0 ? 0 : failed / attempted;
|
|
94
|
+
const exceeds = attempted > 0 && (type === "deterministic" ? failed > 0 : rate > limit);
|
|
95
|
+
return { attempted, rate, exceeds, zeroSample: attempted === 0 };
|
|
96
|
+
}
|
|
97
|
+
async function mapWithConcurrency(items, concurrency, task) {
|
|
98
|
+
const results = new Array(items.length);
|
|
99
|
+
const requested = Number.isFinite(concurrency) ? Math.floor(concurrency) : 1;
|
|
100
|
+
const workerCount = items.length === 0 ? 0 : Math.min(Math.max(1, requested), items.length);
|
|
101
|
+
let next = 0;
|
|
102
|
+
async function runNext() {
|
|
103
|
+
while (next < items.length) {
|
|
104
|
+
const i = next++;
|
|
105
|
+
results[i] = await task(items[i], i);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
await Promise.all(Array.from({ length: workerCount }, () => runNext()));
|
|
109
|
+
return results;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/compare.ts
|
|
71
113
|
var DEFAULT_LLM_THRESHOLD = 0.05;
|
|
72
114
|
var DEFAULT_DETERMINISTIC_THRESHOLD = 0;
|
|
73
115
|
var LEGACY_THRESHOLD = 0.1;
|
|
@@ -141,11 +183,29 @@ function evalCompare(baseline, candidate, options) {
|
|
|
141
183
|
const delta = round2(deltaRaw);
|
|
142
184
|
const deltaPercent = bMeanRaw > 0 ? round2(deltaRaw / bMeanRaw * 100) : 0;
|
|
143
185
|
const threshold = resolveThreshold(name, options, baselineRef.metadata);
|
|
186
|
+
let baselineScored = 0;
|
|
187
|
+
let baselineFailed = 0;
|
|
188
|
+
for (const run of baselineRuns) {
|
|
189
|
+
const c = scorerCounts(run.items, name);
|
|
190
|
+
baselineScored += c.scored;
|
|
191
|
+
baselineFailed += c.failed;
|
|
192
|
+
}
|
|
193
|
+
let candidateScored = 0;
|
|
194
|
+
let candidateFailed = 0;
|
|
195
|
+
for (const run of candidateRuns) {
|
|
196
|
+
const c = scorerCounts(run.items, name);
|
|
197
|
+
candidateScored += c.scored;
|
|
198
|
+
candidateFailed += c.failed;
|
|
199
|
+
}
|
|
144
200
|
const entry = {
|
|
145
201
|
baselineMean: bMean,
|
|
146
202
|
candidateMean: cMean,
|
|
147
203
|
delta,
|
|
148
|
-
deltaPercent
|
|
204
|
+
deltaPercent,
|
|
205
|
+
baselineScored,
|
|
206
|
+
baselineFailed,
|
|
207
|
+
candidateScored,
|
|
208
|
+
candidateFailed
|
|
149
209
|
};
|
|
150
210
|
const diffs = pairedDiffs[name];
|
|
151
211
|
if (diffs.length >= 2) {
|
|
@@ -281,11 +341,41 @@ function detectPartial(runs) {
|
|
|
281
341
|
if (runs.length >= attempted) return void 0;
|
|
282
342
|
return { completed: runs.length, attempted };
|
|
283
343
|
}
|
|
344
|
+
function evaluateScorerErrorRateGate(comparison, maxScorerErrorRate) {
|
|
345
|
+
const scorerTypes = comparison.baseline.metadata?.scorerTypes ?? {};
|
|
346
|
+
for (const name of Object.keys(comparison.scorers)) {
|
|
347
|
+
const s = comparison.scorers[name];
|
|
348
|
+
const type = scorerTypes[name] === "deterministic" ? "deterministic" : "llm";
|
|
349
|
+
const limit = type === "deterministic" ? 0 : maxScorerErrorRate;
|
|
350
|
+
for (const side of ["baseline", "candidate"]) {
|
|
351
|
+
const scored = (side === "baseline" ? s.baselineScored : s.candidateScored) ?? 0;
|
|
352
|
+
const failed = (side === "baseline" ? s.baselineFailed : s.candidateFailed) ?? 0;
|
|
353
|
+
const verdict = evaluateScorerTolerance(scored, failed, type, limit);
|
|
354
|
+
if (verdict.zeroSample) {
|
|
355
|
+
return `scorer "${name}" has no scored items on ${side} (its items errored or were skipped) \u2014 cannot certify a mean over an empty sample.`;
|
|
356
|
+
}
|
|
357
|
+
if (verdict.exceeds) {
|
|
358
|
+
const limitStr = type === "deterministic" ? "deterministic zero-tolerance" : `${(limit * 100).toFixed(1)}%`;
|
|
359
|
+
return `scorer "${name}" failed ${(verdict.rate * 100).toFixed(1)}% on ${side} (${failed}/${verdict.attempted}), over the ${limitStr} limit.`;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
284
365
|
|
|
285
366
|
// src/scorer.ts
|
|
286
367
|
function normalizeScorerResult(result) {
|
|
287
368
|
return typeof result === "number" ? { score: result } : result;
|
|
288
369
|
}
|
|
370
|
+
function attachScorerErrorCost(err, cost) {
|
|
371
|
+
if (err && typeof err === "object" && typeof cost === "number" && Number.isFinite(cost) && cost >= 0) {
|
|
372
|
+
err.cost = cost;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function extractScorerErrorCost(err) {
|
|
376
|
+
const v = err && typeof err === "object" ? err.cost : void 0;
|
|
377
|
+
return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : void 0;
|
|
378
|
+
}
|
|
289
379
|
function scorer(config) {
|
|
290
380
|
return {
|
|
291
381
|
name: config.name,
|
|
@@ -295,19 +385,60 @@ function scorer(config) {
|
|
|
295
385
|
};
|
|
296
386
|
}
|
|
297
387
|
|
|
298
|
-
// src/
|
|
299
|
-
function
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
388
|
+
// src/score-item.ts
|
|
389
|
+
async function scoreItem(item, scorers, scorerConcurrency, scorerContext, signal) {
|
|
390
|
+
const scoreDetails = item.scoreDetails ??= {};
|
|
391
|
+
for (const s of scorers) {
|
|
392
|
+
item.scores[s.name] = null;
|
|
393
|
+
scoreDetails[s.name] = { score: null };
|
|
394
|
+
}
|
|
395
|
+
let itemScorerCost = 0;
|
|
396
|
+
const scorerErrorsByName = {};
|
|
397
|
+
await mapWithConcurrency(scorers, scorerConcurrency, async (scorer2) => {
|
|
398
|
+
if (signal?.aborted) return;
|
|
399
|
+
const scorerStart = Date.now();
|
|
400
|
+
try {
|
|
401
|
+
const raw = await scorer2.score(item.output, item.input, item.annotations, scorerContext);
|
|
402
|
+
const scorerResult = normalizeScorerResult(raw);
|
|
403
|
+
if (scorerResult.cost != null) {
|
|
404
|
+
itemScorerCost += scorerResult.cost;
|
|
405
|
+
}
|
|
406
|
+
const scorerDuration = Date.now() - scorerStart;
|
|
407
|
+
if (!Number.isFinite(scorerResult.score) || scorerResult.score < 0 || scorerResult.score > 1) {
|
|
408
|
+
scorerErrorsByName[scorer2.name] = `Scorer "${scorer2.name}" returned out-of-range score ${scorerResult.score} for input ${JSON.stringify(item.input)}`;
|
|
409
|
+
item.scores[scorer2.name] = null;
|
|
410
|
+
scoreDetails[scorer2.name] = {
|
|
411
|
+
score: null,
|
|
412
|
+
metadata: scorerResult.metadata,
|
|
413
|
+
duration: scorerDuration,
|
|
414
|
+
cost: scorerResult.cost
|
|
415
|
+
};
|
|
416
|
+
} else {
|
|
417
|
+
item.scores[scorer2.name] = round2(scorerResult.score);
|
|
418
|
+
scoreDetails[scorer2.name] = {
|
|
419
|
+
score: round2(scorerResult.score),
|
|
420
|
+
metadata: scorerResult.metadata,
|
|
421
|
+
duration: scorerDuration,
|
|
422
|
+
cost: scorerResult.cost
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
} catch (err) {
|
|
426
|
+
if (err?.name === "AbortError") return;
|
|
427
|
+
const errCost = extractScorerErrorCost(err);
|
|
428
|
+
if (errCost != null) itemScorerCost += errCost;
|
|
429
|
+
scorerErrorsByName[scorer2.name] = `Scorer "${scorer2.name}" threw: ${err instanceof Error ? err.message : String(err)}`;
|
|
430
|
+
item.scores[scorer2.name] = null;
|
|
431
|
+
scoreDetails[scorer2.name] = {
|
|
432
|
+
score: null,
|
|
433
|
+
duration: Date.now() - scorerStart,
|
|
434
|
+
cost: errCost
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
const orderedErrors = scorers.map((s) => scorerErrorsByName[s.name]).filter((e) => e != null);
|
|
439
|
+
if (orderedErrors.length > 0) item.scorerErrors = orderedErrors;
|
|
440
|
+
item.scorerCost = itemScorerCost > 0 ? itemScorerCost : void 0;
|
|
441
|
+
return itemScorerCost;
|
|
311
442
|
}
|
|
312
443
|
|
|
313
444
|
// src/runner.ts
|
|
@@ -350,7 +481,9 @@ async function runEval(config, executeWorkflow, runtime, options) {
|
|
|
350
481
|
const startTime = Date.now();
|
|
351
482
|
const id = randomUUID();
|
|
352
483
|
const items = await config.dataset.getItems();
|
|
484
|
+
const droppedAnnotationKeys = [...config.dataset.droppedAnnotationKeys ?? []];
|
|
353
485
|
const concurrency = config.concurrency ?? 5;
|
|
486
|
+
const scorerConcurrency = config.scorerConcurrency ?? 5;
|
|
354
487
|
const budgetLimit = config.budget ? parseCost(config.budget) : void 0;
|
|
355
488
|
const scorerContext = {
|
|
356
489
|
resolveProvider: (uri) => {
|
|
@@ -439,89 +572,53 @@ async function runEval(config, executeWorkflow, runtime, options) {
|
|
|
439
572
|
if (budgetLimit != null && totalCost > budgetLimit) {
|
|
440
573
|
budgetExceeded = true;
|
|
441
574
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
item.input,
|
|
451
|
-
item.annotations,
|
|
452
|
-
scorerContext
|
|
453
|
-
);
|
|
454
|
-
const scorerResult = normalizeScorerResult(raw);
|
|
455
|
-
if (scorerResult.cost != null) {
|
|
456
|
-
itemScorerCost += scorerResult.cost;
|
|
457
|
-
totalCost += scorerResult.cost;
|
|
458
|
-
}
|
|
459
|
-
const scorerDuration = Date.now() - scorerStart;
|
|
460
|
-
if (!Number.isFinite(scorerResult.score) || scorerResult.score < 0 || scorerResult.score > 1) {
|
|
461
|
-
if (!evalItem.scorerErrors) evalItem.scorerErrors = [];
|
|
462
|
-
evalItem.scorerErrors.push(
|
|
463
|
-
`Scorer "${scorer2.name}" returned out-of-range score ${scorerResult.score} for input ${JSON.stringify(item.input)}`
|
|
464
|
-
);
|
|
465
|
-
evalItem.scores[scorer2.name] = null;
|
|
466
|
-
evalItem.scoreDetails[scorer2.name] = {
|
|
467
|
-
score: null,
|
|
468
|
-
metadata: scorerResult.metadata,
|
|
469
|
-
duration: scorerDuration,
|
|
470
|
-
cost: scorerResult.cost
|
|
471
|
-
};
|
|
472
|
-
} else {
|
|
473
|
-
evalItem.scores[scorer2.name] = round3(scorerResult.score);
|
|
474
|
-
evalItem.scoreDetails[scorer2.name] = {
|
|
475
|
-
score: round3(scorerResult.score),
|
|
476
|
-
metadata: scorerResult.metadata,
|
|
477
|
-
duration: scorerDuration,
|
|
478
|
-
cost: scorerResult.cost
|
|
479
|
-
};
|
|
480
|
-
}
|
|
481
|
-
} catch (err) {
|
|
482
|
-
const errCost = typeof err?.cost === "number" ? err.cost : void 0;
|
|
483
|
-
if (errCost != null) {
|
|
484
|
-
itemScorerCost += errCost;
|
|
485
|
-
totalCost += errCost;
|
|
486
|
-
}
|
|
487
|
-
if (!evalItem.scorerErrors) evalItem.scorerErrors = [];
|
|
488
|
-
evalItem.scorerErrors.push(
|
|
489
|
-
`Scorer "${scorer2.name}" threw: ${err instanceof Error ? err.message : String(err)}`
|
|
490
|
-
);
|
|
491
|
-
evalItem.scores[scorer2.name] = null;
|
|
492
|
-
evalItem.scoreDetails[scorer2.name] = {
|
|
493
|
-
score: null,
|
|
494
|
-
duration: Date.now() - scorerStart,
|
|
495
|
-
cost: errCost
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
evalItem.scorerCost = itemScorerCost > 0 ? itemScorerCost : void 0;
|
|
575
|
+
const itemScorerCost = await scoreItem(
|
|
576
|
+
evalItem,
|
|
577
|
+
config.scorers,
|
|
578
|
+
scorerConcurrency,
|
|
579
|
+
scorerContext,
|
|
580
|
+
options?.signal
|
|
581
|
+
);
|
|
582
|
+
totalCost += itemScorerCost;
|
|
500
583
|
evalItems[itemIndex] = evalItem;
|
|
501
584
|
options?.onProgress?.({ type: "item_done", itemIndex, totalItems: items.length });
|
|
502
585
|
}
|
|
503
|
-
|
|
504
|
-
async function runNext() {
|
|
505
|
-
while (index < items.length) {
|
|
506
|
-
const currentIndex = index++;
|
|
507
|
-
await processItem(items[currentIndex], currentIndex);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => runNext());
|
|
511
|
-
await Promise.all(workers);
|
|
586
|
+
await mapWithConcurrency(items, concurrency, (item, i) => processItem(item, i));
|
|
512
587
|
const failures = evalItems.filter((i) => i.error).length;
|
|
513
588
|
const scorerNames = config.scorers.map((s) => s.name);
|
|
514
589
|
const scorerStats = {};
|
|
515
590
|
for (const name of scorerNames) {
|
|
516
591
|
const scores = evalItems.filter((i) => !i.error && i.scores[name] != null).map((i) => i.scores[name]);
|
|
517
|
-
|
|
592
|
+
const { scored, failed } = scorerCounts(evalItems, name);
|
|
593
|
+
scorerStats[name] = { ...computeStats(scores), scored, failed };
|
|
518
594
|
}
|
|
519
|
-
const durations = evalItems.filter((i) => !i.error && i.duration != null).map((i) => i.duration);
|
|
520
|
-
const timing = durations.length > 0 ? computeStats(durations) : void 0;
|
|
521
595
|
const scorerTypes = {};
|
|
522
596
|
for (const s of config.scorers) {
|
|
523
597
|
scorerTypes[s.name] = s.isLlm ? "llm" : "deterministic";
|
|
524
598
|
}
|
|
599
|
+
let degraded;
|
|
600
|
+
const rawLimit = config.failOnScorerErrorRate;
|
|
601
|
+
if (rawLimit != null) {
|
|
602
|
+
if (!Number.isFinite(rawLimit) || rawLimit < 0 || rawLimit > 1) {
|
|
603
|
+
console.warn(
|
|
604
|
+
`[axl-eval] Ignoring invalid failOnScorerErrorRate (${rawLimit}); expected a number in [0, 1].`
|
|
605
|
+
);
|
|
606
|
+
} else {
|
|
607
|
+
for (const name of scorerNames) {
|
|
608
|
+
const stats = scorerStats[name];
|
|
609
|
+
const scored = stats.scored ?? 0;
|
|
610
|
+
const failed = stats.failed ?? 0;
|
|
611
|
+
const type = scorerTypes[name] === "llm" ? "llm" : "deterministic";
|
|
612
|
+
const limit = type === "deterministic" ? 0 : rawLimit;
|
|
613
|
+
const verdict = evaluateScorerTolerance(scored, failed, type, limit);
|
|
614
|
+
if (verdict.exceeds) {
|
|
615
|
+
(degraded ??= []).push({ scorer: name, rate: verdict.rate, limit, type, scored, failed });
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const durations = evalItems.filter((i) => !i.error && i.duration != null).map((i) => i.duration);
|
|
621
|
+
const timing = durations.length > 0 ? computeStats(durations) : void 0;
|
|
525
622
|
const totalModelCalls = /* @__PURE__ */ new Map();
|
|
526
623
|
for (const item of evalItems) {
|
|
527
624
|
const itemCounts = item.metadata?.modelCallCounts;
|
|
@@ -580,13 +677,20 @@ async function runEval(config, executeWorkflow, runtime, options) {
|
|
|
580
677
|
...config.metadata,
|
|
581
678
|
scorerTypes,
|
|
582
679
|
...modelsMeta,
|
|
583
|
-
...workflowsMeta
|
|
680
|
+
...workflowsMeta,
|
|
681
|
+
...droppedAnnotationKeys.length > 0 ? { droppedAnnotationKeys } : {}
|
|
584
682
|
},
|
|
585
683
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
586
684
|
totalCost,
|
|
587
685
|
duration: Date.now() - startTime,
|
|
588
686
|
items: evalItems,
|
|
589
|
-
summary: {
|
|
687
|
+
summary: {
|
|
688
|
+
count: items.length,
|
|
689
|
+
failures,
|
|
690
|
+
scorers: scorerStats,
|
|
691
|
+
timing,
|
|
692
|
+
...degraded ? { degraded } : {}
|
|
693
|
+
}
|
|
590
694
|
};
|
|
591
695
|
}
|
|
592
696
|
|
|
@@ -595,6 +699,7 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
595
699
|
async function rescore(result, scorers, runtime, options) {
|
|
596
700
|
const startTime = Date.now();
|
|
597
701
|
const concurrency = options?.concurrency ?? 5;
|
|
702
|
+
const scorerConcurrency = options?.scorerConcurrency ?? 5;
|
|
598
703
|
const scorerContext = {
|
|
599
704
|
resolveProvider: (uri) => {
|
|
600
705
|
if (typeof runtime.resolveProvider !== "function") {
|
|
@@ -638,77 +743,24 @@ async function rescore(result, scorers, runtime, options) {
|
|
|
638
743
|
scores: {},
|
|
639
744
|
scoreDetails: {}
|
|
640
745
|
};
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
totalCost += scorerResult.cost;
|
|
650
|
-
}
|
|
651
|
-
const scorerDuration = Date.now() - scorerStart;
|
|
652
|
-
if (!Number.isFinite(scorerResult.score) || scorerResult.score < 0 || scorerResult.score > 1) {
|
|
653
|
-
if (!item.scorerErrors) item.scorerErrors = [];
|
|
654
|
-
item.scorerErrors.push(
|
|
655
|
-
`Scorer "${scorer2.name}" returned out-of-range score ${scorerResult.score} for input ${JSON.stringify(item.input)}`
|
|
656
|
-
);
|
|
657
|
-
item.scores[scorer2.name] = null;
|
|
658
|
-
item.scoreDetails[scorer2.name] = {
|
|
659
|
-
score: null,
|
|
660
|
-
metadata: scorerResult.metadata,
|
|
661
|
-
duration: scorerDuration,
|
|
662
|
-
cost: scorerResult.cost
|
|
663
|
-
};
|
|
664
|
-
} else {
|
|
665
|
-
item.scores[scorer2.name] = round3(scorerResult.score);
|
|
666
|
-
item.scoreDetails[scorer2.name] = {
|
|
667
|
-
score: round3(scorerResult.score),
|
|
668
|
-
metadata: scorerResult.metadata,
|
|
669
|
-
duration: scorerDuration,
|
|
670
|
-
cost: scorerResult.cost
|
|
671
|
-
};
|
|
672
|
-
}
|
|
673
|
-
} catch (err) {
|
|
674
|
-
const errCost = typeof err?.cost === "number" ? err.cost : void 0;
|
|
675
|
-
if (errCost != null) {
|
|
676
|
-
itemScorerCost += errCost;
|
|
677
|
-
totalCost += errCost;
|
|
678
|
-
}
|
|
679
|
-
if (!item.scorerErrors) item.scorerErrors = [];
|
|
680
|
-
item.scorerErrors.push(
|
|
681
|
-
`Scorer "${scorer2.name}" threw: ${err instanceof Error ? err.message : String(err)}`
|
|
682
|
-
);
|
|
683
|
-
item.scores[scorer2.name] = null;
|
|
684
|
-
item.scoreDetails[scorer2.name] = {
|
|
685
|
-
score: null,
|
|
686
|
-
duration: Date.now() - scorerStart,
|
|
687
|
-
cost: errCost
|
|
688
|
-
};
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
item.scorerCost = itemScorerCost > 0 ? itemScorerCost : void 0;
|
|
746
|
+
const itemScorerCost = await scoreItem(
|
|
747
|
+
item,
|
|
748
|
+
scorers,
|
|
749
|
+
scorerConcurrency,
|
|
750
|
+
scorerContext,
|
|
751
|
+
options?.signal
|
|
752
|
+
);
|
|
753
|
+
totalCost += itemScorerCost;
|
|
692
754
|
rescored[itemIndex] = item;
|
|
693
755
|
}
|
|
694
|
-
|
|
695
|
-
async function runNext() {
|
|
696
|
-
while (index < result.items.length) {
|
|
697
|
-
const currentIndex = index++;
|
|
698
|
-
await rescoreItem(result.items[currentIndex], currentIndex);
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
const workers = Array.from(
|
|
702
|
-
{ length: Math.min(concurrency, result.items.length) },
|
|
703
|
-
() => runNext()
|
|
704
|
-
);
|
|
705
|
-
await Promise.all(workers);
|
|
756
|
+
await mapWithConcurrency(result.items, concurrency, (item, i) => rescoreItem(item, i));
|
|
706
757
|
const failures = rescored.filter((i) => i.error).length;
|
|
707
758
|
const scorerNames = scorers.map((s) => s.name);
|
|
708
759
|
const scorerStats = {};
|
|
709
760
|
for (const name of scorerNames) {
|
|
710
761
|
const scores = rescored.filter((i) => !i.error && i.scores[name] != null).map((i) => i.scores[name]);
|
|
711
|
-
|
|
762
|
+
const { scored, failed } = scorerCounts(rescored, name);
|
|
763
|
+
scorerStats[name] = { ...computeStats(scores), scored, failed };
|
|
712
764
|
}
|
|
713
765
|
const scorerTypes = {};
|
|
714
766
|
for (const s of scorers) {
|
|
@@ -718,7 +770,11 @@ async function rescore(result, scorers, runtime, options) {
|
|
|
718
770
|
id: randomUUID2(),
|
|
719
771
|
dataset: result.dataset,
|
|
720
772
|
metadata: (() => {
|
|
721
|
-
const
|
|
773
|
+
const rest = { ...result.metadata };
|
|
774
|
+
delete rest.runGroupId;
|
|
775
|
+
delete rest.runIndex;
|
|
776
|
+
delete rest.scorerFiltered;
|
|
777
|
+
delete rest.scorersRun;
|
|
722
778
|
const merged = {
|
|
723
779
|
...rest,
|
|
724
780
|
rescored: true,
|
|
@@ -771,15 +827,24 @@ function aggregateRuns(runs) {
|
|
|
771
827
|
const scorerNames = Object.keys(runs[0].summary.scorers);
|
|
772
828
|
const scorers = {};
|
|
773
829
|
for (const name of scorerNames) {
|
|
774
|
-
const
|
|
830
|
+
const contributing = runs.filter((r) => {
|
|
831
|
+
const sc = r.summary.scorers[name]?.scored;
|
|
832
|
+
return sc == null || sc > 0;
|
|
833
|
+
});
|
|
834
|
+
const meanSource = contributing.length > 0 ? contributing : runs;
|
|
835
|
+
const means = meanSource.map((r) => r.summary.scorers[name]?.mean ?? 0);
|
|
775
836
|
const meanOfMeans = means.reduce((a, b) => a + b, 0) / means.length;
|
|
776
837
|
const minMean = Math.min(...means);
|
|
777
838
|
const maxMean = Math.max(...means);
|
|
839
|
+
const scored = runs.reduce((sum, r) => sum + (r.summary.scorers[name]?.scored ?? 0), 0);
|
|
840
|
+
const failed = runs.reduce((sum, r) => sum + (r.summary.scorers[name]?.failed ?? 0), 0);
|
|
778
841
|
scorers[name] = {
|
|
779
|
-
mean:
|
|
780
|
-
std:
|
|
781
|
-
min:
|
|
782
|
-
max:
|
|
842
|
+
mean: round2(meanOfMeans),
|
|
843
|
+
std: round2(std(means, meanOfMeans)),
|
|
844
|
+
min: round2(minMean),
|
|
845
|
+
max: round2(maxMean),
|
|
846
|
+
scored,
|
|
847
|
+
failed
|
|
783
848
|
};
|
|
784
849
|
}
|
|
785
850
|
let timing;
|
|
@@ -787,8 +852,8 @@ function aggregateRuns(runs) {
|
|
|
787
852
|
if (timingMeans.length === runs.length && timingMeans.length > 0) {
|
|
788
853
|
const meanTiming = timingMeans.reduce((a, b) => a + b, 0) / timingMeans.length;
|
|
789
854
|
timing = {
|
|
790
|
-
mean:
|
|
791
|
-
std:
|
|
855
|
+
mean: round2(meanTiming),
|
|
856
|
+
std: round2(std(timingMeans, meanTiming))
|
|
792
857
|
};
|
|
793
858
|
}
|
|
794
859
|
return {
|
|
@@ -796,8 +861,8 @@ function aggregateRuns(runs) {
|
|
|
796
861
|
runCount: runs.length,
|
|
797
862
|
workflows,
|
|
798
863
|
dataset,
|
|
799
|
-
totalCost:
|
|
800
|
-
totalDuration:
|
|
864
|
+
totalCost: round2(totalCost),
|
|
865
|
+
totalDuration: round2(totalDuration),
|
|
801
866
|
scorers,
|
|
802
867
|
timing
|
|
803
868
|
};
|
|
@@ -805,11 +870,15 @@ function aggregateRuns(runs) {
|
|
|
805
870
|
|
|
806
871
|
export {
|
|
807
872
|
pairedBootstrapCI,
|
|
873
|
+
scorerCounts,
|
|
874
|
+
evaluateScorerTolerance,
|
|
808
875
|
evalCompare,
|
|
876
|
+
evaluateScorerErrorRateGate,
|
|
809
877
|
normalizeScorerResult,
|
|
878
|
+
attachScorerErrorCost,
|
|
810
879
|
scorer,
|
|
811
880
|
runEval,
|
|
812
881
|
rescore,
|
|
813
882
|
aggregateRuns
|
|
814
883
|
};
|
|
815
|
-
//# sourceMappingURL=chunk-
|
|
884
|
+
//# sourceMappingURL=chunk-BWBX5YVQ.js.map
|