@axlsdk/eval 0.23.2 → 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/README.md
CHANGED
|
@@ -271,6 +271,9 @@ npx axl-eval ./evals/ --config ./axl.config.ts # use a specific runtime con
|
|
|
271
271
|
npx axl-eval ./evals/ --conditions development # add Node.js import conditions (monorepo source exports)
|
|
272
272
|
npx axl-eval ./evals/qa.eval.ts --concurrency 10 # override item concurrency for this run
|
|
273
273
|
npx axl-eval ./evals/qa.eval.ts --scorers accuracy # run only named scorer(s) (single file)
|
|
274
|
+
npx axl-eval ./evals/qa.eval.ts --capture-requests # also record the requests Axl submitted
|
|
275
|
+
npx axl-eval ./evals/qa.eval.ts --budget 2.50 # stop admitting paid work once known spend reaches $2.50
|
|
276
|
+
npx axl-eval ./evals/qa.eval.ts --max-item-error-rate 0.1 # allow up to 10% of items to fail (default 5%; 1 disables)
|
|
274
277
|
```
|
|
275
278
|
|
|
276
279
|
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).
|
|
@@ -308,9 +311,31 @@ npx axl-eval compare base.json cand.json --fail-on-regression --max-scorer-error
|
|
|
308
311
|
|
|
309
312
|
Building a custom CI gate? Both decisions are exported as pure functions — `evaluateScorerErrorRateGate` and `evaluateScorerTolerance` (see the [API reference](../../docs/api-reference.md)).
|
|
310
313
|
|
|
314
|
+
#### Item error rate — on by default
|
|
315
|
+
|
|
316
|
+
A run that loses items in the *workflow* (a rate-limit storm, an incident, a tool bug) is scored over the survivors, so its means can look healthy while most of the dataset never ran. `axl-eval` therefore fails a run by default when **more than 5%** of its attempted items failed:
|
|
317
|
+
|
|
318
|
+
```
|
|
319
|
+
Items: 89 completed, 250 failed — item error rate 73.7% (limit 5%)
|
|
320
|
+
Failure causes: 250 × 429 (openai)
|
|
321
|
+
[axl-eval] ITEM ERROR RATE EXCEEDED: qa.eval.ts — 250 of 339 attempted item(s) failed in the workflow (item error rate 73.7%), over the 5% limit; …
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
The rate is `failed / (count − cancelled − budget_skipped − budget_interrupted)`: a cancelled or budget-stopped item is reported by its own gate and never counts here, and the gate fires on strictly `>`. Under `--runs N` every run is gated on its own, and the failing run is named. The artifact is still written.
|
|
325
|
+
|
|
326
|
+
| Knob | Where | Effect |
|
|
327
|
+
|------|-------|--------|
|
|
328
|
+
| `failOnItemErrorRate` (config field, default `0.05`) | **Source-side** — `runEval` | Sets the limit; `runEval` records `summary.itemErrorRate` whenever an item failed. An invalid value throws before the dataset loads. |
|
|
329
|
+
| `--max-item-error-rate <0..1>` (run flag) | CLI | Overrides the config for this invocation. `1` disables the gate (the rate is still printed). |
|
|
330
|
+
| `--max-item-error-rate <0..1>` (compare flag, default `0.05`) | **Gate-side** — `axl-eval compare` | Refuses to certify a side (any compared run) whose item error rate is over the limit, including legacy artifacts (rate derived from their items). Warns about a side that lost items within the limit. |
|
|
331
|
+
|
|
332
|
+
Each failed item records **why** on `item.failure` — `{ name, provider, status, retryable, requestId }` taken from the first `ProviderError` on the thrown value or its `cause` chain, or just the thrown `name` otherwise — and the table groups them on the `Failure causes:` line (`network` is status `0`, `other` is a non-provider failure), so a rate-limit storm reads differently from a bug. `failure` never records `ProviderError.body`; `item.error` keeps the error message as before, which for some providers can include error-response text.
|
|
333
|
+
|
|
334
|
+
`rescore` does not apply the gate — its failed items belong to the source run — and rejects the flag; `compare` re-applies the floor to a rescored artifact. Building your own gate? `evaluateItemErrorRateGate(baseline, candidate, limit?)` is exported.
|
|
335
|
+
|
|
311
336
|
#### Total-workflow-wipeout guard
|
|
312
337
|
|
|
313
|
-
Separate from the
|
|
338
|
+
Separate from the gates 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 still applies with the item gate disabled (`--max-item-error-rate 1`). It is distinct from `failOnScorerErrorRate` (which is about a flaky *scorer* and deliberately ignores a run with no scored items).
|
|
314
339
|
|
|
315
340
|
### Programmatic
|
|
316
341
|
|
|
@@ -381,7 +406,12 @@ const results = await runEval(
|
|
|
381
406
|
);
|
|
382
407
|
```
|
|
383
408
|
|
|
384
|
-
**
|
|
409
|
+
**A returned `cost` is a claim, not a total.** Spend is measured by the runtime, so a
|
|
410
|
+
`cost` your `executeWorkflow` callback returns never sets `item.cost` and never feeds
|
|
411
|
+
`totalCost`. It is preserved verbatim on `item.callerReport.cost` (and summarized on
|
|
412
|
+
`accounting.callerReported`) so you can still inspect it or compare it against what was
|
|
413
|
+
actually measured. The value is still validated — a non-negative finite number, or it is
|
|
414
|
+
dropped with a `console.warn` — so `{ cost: 'free' }` can poison nothing.
|
|
385
415
|
|
|
386
416
|
**Additive item metadata.** Returning `{ output, metadata: { category: 'billing' } }`
|
|
387
417
|
preserves tracked models, tokens, agent calls, and workflow attribution, with or without
|
|
@@ -435,8 +465,12 @@ console.log(results.summary.scorers['quality'].skipped); // 0 (items the applie
|
|
|
435
465
|
console.log(results.summary.count); // 50 items
|
|
436
466
|
console.log(results.summary.failures); // 2 workflow errors
|
|
437
467
|
console.log(results.summary.timing); // { mean, min, max, p50, p95 } in ms — WORKFLOW wall clock
|
|
438
|
-
console.log(results.summary.modelTiming); // per model: { calls, wireMs, queuedMs, retryMs, firstTokenMs? } — PROVIDER latency, per-CALL distributions
|
|
439
|
-
console.log(results.totalCost); // 0.42 (workflow +
|
|
468
|
+
console.log(results.summary.modelTiming); // per model: { calls, wireMs, queuedMs, retryMs, firstTokenMs?, rateLimitRetries? } — PROVIDER latency, per-CALL distributions
|
|
469
|
+
console.log(results.totalCost); // 0.42 — MEASURED spend (workflow + judges)
|
|
470
|
+
console.log(results.unpriced); // true when that total is only a lower bound
|
|
471
|
+
console.log(results.accounting.completeness); // 'complete' | 'incomplete' | 'unverified'
|
|
472
|
+
console.log(results.accounting.breakdown); // { generation, judging, external }
|
|
473
|
+
console.log(results.summary.coverage.items); // { completed, failed, cancelled, budget_skipped, budget_interrupted }
|
|
440
474
|
console.log(results.metadata.models); // ["openai:gpt-4o"] (sorted by usage)
|
|
441
475
|
console.log(results.metadata.modelCounts); // { "openai:gpt-4o": 48, "openai:gpt-4o-mini": 2 } (total LLM calls per model)
|
|
442
476
|
|
|
@@ -447,7 +481,7 @@ for (const item of results.items) {
|
|
|
447
481
|
// Timing and cost
|
|
448
482
|
console.log(item.duration); // workflow execution ms (tools, gates, queue and all)
|
|
449
483
|
console.log(item.timing); // per model: { calls, queuedMs, retryMs, wireMs, firstTokenMs?, firstTokenCalls? } sums — absent if nothing was timed
|
|
450
|
-
console.log(item.cost); //
|
|
484
|
+
console.log(item.cost); // measured generation spend (kept even if the case later threw)
|
|
451
485
|
console.log(item.scorerCost); // total scorer cost for this item
|
|
452
486
|
|
|
453
487
|
// Execution metadata (models, tokens, agent calls — captured by AxlRuntime)
|
|
@@ -546,6 +580,7 @@ Compare two runs to detect regressions and improvements. Runs must use the same
|
|
|
546
580
|
npx axl-eval compare ./results/v1.json ./results/v2.json
|
|
547
581
|
npx axl-eval compare v1.json v2.json --fail-on-regression # exit 1 if significant regressions
|
|
548
582
|
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)
|
|
583
|
+
npx axl-eval compare v1.json v2.json --max-item-error-rate 0.2 # accept a side that lost up to 20% of its items (default 5%, always on; 1 disables)
|
|
549
584
|
```
|
|
550
585
|
|
|
551
586
|
```
|
|
@@ -675,14 +710,57 @@ export async function executeWorkflow(input: { raw: string }) {
|
|
|
675
710
|
|
|
676
711
|
### Cost tracking
|
|
677
712
|
|
|
678
|
-
Cost is
|
|
713
|
+
Cost is measured automatically: the runner opens an accounting scope per run, per item,
|
|
714
|
+
and per scorer, so `totalCost` is what the runtime observed rather than a sum over trace
|
|
715
|
+
events. Judge costs are included and count toward the `budget` limit. Spend the runtime
|
|
716
|
+
could not price is reported as unknown (`unpriced`, with `accounting.reasons`) rather
|
|
717
|
+
than as zero — and because unknown spend cannot be enforced against, it does not consume
|
|
718
|
+
a budget either.
|
|
719
|
+
|
|
720
|
+
### Captured requests (opt-in)
|
|
721
|
+
|
|
722
|
+
When a score looks wrong, the next question is what the model was actually
|
|
723
|
+
asked. `captureRequests` records the request Axl submitted for every model call
|
|
724
|
+
in the run — case turns, tool continuations, nested asks and LLM judges — beside
|
|
725
|
+
the result rather than inside it:
|
|
726
|
+
|
|
727
|
+
```typescript
|
|
728
|
+
const runtime = new AxlRuntime({
|
|
729
|
+
diagnostics: { artifacts: { root: '.axl/artifacts' } },
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
const results = await runEval(config, executeWorkflow, runtime, {
|
|
733
|
+
captureRequests: true,
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
results.diagnostics; // { status, records, bytes, redaction, … }
|
|
737
|
+
results.items[0].diagnostics?.operations; // the generation calls behind this item
|
|
738
|
+
results.items[0].scoreDetails?.quality.diagnostics; // the judge's own calls
|
|
739
|
+
```
|
|
679
740
|
|
|
680
|
-
|
|
741
|
+
```bash
|
|
742
|
+
npx axl-eval ./evals/qa.eval.ts --capture-requests --output ./results/v1.json
|
|
743
|
+
# also writes ./results/v1.requests.jsonl
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
Capture is **off** by default, requires a configured artifact store (a run that
|
|
747
|
+
asks for it without one fails before the dataset loads), is bounded per record /
|
|
748
|
+
per run / per pending queue, and never delays a provider call. It is on a
|
|
749
|
+
different rail from accounting: a failing, truncated or redacted capture leaves
|
|
750
|
+
the numbers byte-identical and reports the loss on `diagnostics.status`. See
|
|
751
|
+
[observability.md](../../docs/observability.md#captured-requests-opt-in).
|
|
752
|
+
|
|
753
|
+
A returned cost can no longer override that measurement — it would let a workflow
|
|
754
|
+
understate what it spent. Report one anyway if it is useful to compare against:
|
|
681
755
|
|
|
682
756
|
```typescript
|
|
757
|
+
// Recorded on item.callerReport.cost; item.cost stays the measured figure.
|
|
683
758
|
return { output, cost: ctx.totalCost };
|
|
684
759
|
```
|
|
685
760
|
|
|
761
|
+
To genuinely exclude work from a run's spend, do it outside the eval — for example warm
|
|
762
|
+
up a cache before calling `runEval` — rather than by subtracting after the fact.
|
|
763
|
+
|
|
686
764
|
### Common patterns
|
|
687
765
|
|
|
688
766
|
**Concurrency** — process items in parallel (default: 5), and fan out each item's scorers (default: 5):
|
|
@@ -698,7 +776,17 @@ export default defineEval({
|
|
|
698
776
|
});
|
|
699
777
|
```
|
|
700
778
|
|
|
701
|
-
`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).
|
|
779
|
+
`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).
|
|
780
|
+
|
|
781
|
+
**`budget` is a threshold, not a reservation.** Admission closes once known spend reaches
|
|
782
|
+
the limit: later cases become `budget_skipped`, later LLM scorers are skipped,
|
|
783
|
+
deterministic scorers still run, and a case whose next call is refused becomes
|
|
784
|
+
`budget_interrupted` while keeping the charge it already incurred. Calls already in
|
|
785
|
+
flight are allowed to settle, so the run can end slightly over —
|
|
786
|
+
`accounting.budget.knownOvershoot` says by how much, and the higher your `concurrency ×
|
|
787
|
+
scorerConcurrency`, the larger that can be. An invalid limit throws before the dataset is
|
|
788
|
+
even loaded. The CLI prints a distinct `[axl-eval] BUDGET STOPPED …` line and exits
|
|
789
|
+
non-zero without counting the stop as a model failure.
|
|
702
790
|
|
|
703
791
|
**Per-item budget** — cap cost for a single workflow execution:
|
|
704
792
|
|
|
@@ -755,7 +843,9 @@ const rescored = await rescore(originalResult, [updatedScorer, newScorer], runti
|
|
|
755
843
|
|
|
756
844
|
console.log(rescored.metadata.rescored); // true
|
|
757
845
|
console.log(rescored.metadata.originalId); // original result ID
|
|
758
|
-
console.log(rescored.totalCost); //
|
|
846
|
+
console.log(rescored.totalCost); // judging only — the original generation is not re-counted
|
|
847
|
+
console.log(rescored.accounting.scope); // 'rescore'
|
|
848
|
+
console.log(rescored.accounting.source); // { runId, generation } — the run this was scored from
|
|
759
849
|
```
|
|
760
850
|
|
|
761
851
|
## Multi-Run
|
|
@@ -837,6 +927,10 @@ const comparison = evalCompare(baselineRuns, candidateRuns);
|
|
|
837
927
|
| Type | Description |
|
|
838
928
|
|------|-------------|
|
|
839
929
|
| `EvalConfig` | Eval definition (workflow, dataset, scorers, concurrency, scorerConcurrency, budget) |
|
|
930
|
+
| `readAccounting(result)` | A result's accounting, or an `unverified` record synthesized from a pre-0.24 artifact's `totalCost` |
|
|
931
|
+
| `aggregateAccounting(inputs)` | Fold several accounting records conservatively — sums known spend, unions reasons, worst completeness wins |
|
|
932
|
+
| `EvalAccounting` / `EvalCoverage` | Run accounting (scope, budget, source, caller claims) and the item / scorer outcome counts |
|
|
933
|
+
| `EvalItemOutcome` / `ScorerOutcome` | `completed \| failed \| cancelled \| budget_skipped \| budget_interrupted`, and the scorer equivalent with `scored` / `skipped` |
|
|
840
934
|
| `EvalResult` | Full eval output (items, summary, cost, duration) |
|
|
841
935
|
| `EvalItem` | Per-item result (input, output, scores, scoreDetails, metadata, traces?, `timing?`) |
|
|
842
936
|
| `EvalSummary` | Aggregate statistics (count, failures, per-scorer stats incl. `scored`/`failed`/`skipped`, wall-clock `timing`, per-model `modelTiming?`) |
|