@axlsdk/eval 0.17.8 → 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 +74 -7
- package/dist/{chunk-NPKIP4SB.js → chunk-BWBX5YVQ.js} +245 -164
- package/dist/chunk-BWBX5YVQ.js.map +1 -0
- package/dist/cli.cjs +537 -214
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +305 -52
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +327 -177
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +199 -18
- package/dist/index.d.ts +199 -18
- package/dist/index.js +91 -17
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/dist/chunk-NPKIP4SB.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.
|
|
@@ -185,6 +187,32 @@ const qualityJudge = llmScorer({ name: 'quality', model: 'openai:gpt-4o', ... })
|
|
|
185
187
|
const safetyJudge = llmScorer({ name: 'safety', model: 'anthropic:claude-sonnet-4-5-20250514', ... });
|
|
186
188
|
```
|
|
187
189
|
|
|
190
|
+
#### Tuning the judge
|
|
191
|
+
|
|
192
|
+
For reasoning-capable judge models (gpt-5.x, Anthropic Opus/Sonnet 4.5+, Gemini 3.x), `effort` is the highest-leverage knob — judges with reasoning enabled are materially more consistent. The full `ChatOptions` surface is forwarded:
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
const carefulJudge = llmScorer({
|
|
196
|
+
name: 'careful',
|
|
197
|
+
description: 'Reasoning-grade quality judge',
|
|
198
|
+
model: 'openai-responses:gpt-5',
|
|
199
|
+
system: 'Rate output quality and explain your reasoning.',
|
|
200
|
+
effort: 'high', // huge calibration improvement on reasoning models
|
|
201
|
+
maxTokens: 1024, // cap chatty judges to control spend
|
|
202
|
+
includeThoughts: true, // surface reasoning summaries — Gemini + openai-responses only
|
|
203
|
+
// thinkingBudget: 4096, // precise token budget (advanced)
|
|
204
|
+
// stop: ['\n###'], // stop sequences
|
|
205
|
+
// providerOptions: { ... }, // provider-specific escape hatch
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
A note on a few of the knobs:
|
|
210
|
+
|
|
211
|
+
- **`includeThoughts`** is a no-op on Anthropic and the `openai:` (Chat Completions) provider. Use `openai-responses:` or any `google:` model to actually receive reasoning summaries.
|
|
212
|
+
- **`providerOptions`** is merged last into the raw API request, so it can override anything Axl computes — including `responseFormat`. To switch the judge to strict JSON Schema mode on providers that support it (OpenAI Chat Completions, OpenAI Responses, Gemini), pass `providerOptions: { response_format: { type: 'json_schema', json_schema: { name: 'judgment', strict: true, schema: <your-schema-as-json> } } }`. The downstream `extractJson` pipeline handles either shape.
|
|
213
|
+
|
|
214
|
+
The eval runner also propagates its `AbortSignal` into the judge call, so cancelling a run aborts in-flight LLM calls mid-flight (instead of letting the connection finish and silently completing a doomed item).
|
|
215
|
+
|
|
188
216
|
## Running Evals
|
|
189
217
|
|
|
190
218
|
### CLI
|
|
@@ -197,10 +225,42 @@ npx axl-eval ./evals/ # run all *.eval.* files in
|
|
|
197
225
|
npx axl-eval ./evals/ --output ./results/v1.json # save results to JSON
|
|
198
226
|
npx axl-eval ./evals/ --config ./axl.config.ts # use a specific runtime config
|
|
199
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)
|
|
200
230
|
```
|
|
201
231
|
|
|
202
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).
|
|
203
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
|
+
|
|
204
264
|
### Programmatic
|
|
205
265
|
|
|
206
266
|
**`runtime.eval()`** — when you have a workflow registered on the runtime. The `workflow` field must match the registered name:
|
|
@@ -404,6 +464,7 @@ Compare two runs to detect regressions and improvements. Runs must use the same
|
|
|
404
464
|
```bash
|
|
405
465
|
npx axl-eval compare ./results/v1.json ./results/v2.json
|
|
406
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)
|
|
407
468
|
```
|
|
408
469
|
|
|
409
470
|
```
|
|
@@ -543,18 +604,21 @@ return { output, cost: ctx.totalCost };
|
|
|
543
604
|
|
|
544
605
|
### Common patterns
|
|
545
606
|
|
|
546
|
-
**Concurrency** — process items in parallel (default: 5):
|
|
607
|
+
**Concurrency** — process items in parallel (default: 5), and fan out each item's scorers (default: 5):
|
|
547
608
|
|
|
548
609
|
```typescript
|
|
549
610
|
export default defineEval({
|
|
550
611
|
workflow: 'qa-eval',
|
|
551
612
|
dataset: ds,
|
|
552
|
-
scorers: [qualityJudge],
|
|
553
|
-
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
|
|
554
616
|
budget: '$5.00', // stop if total cost exceeds $5
|
|
555
617
|
});
|
|
556
618
|
```
|
|
557
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
|
+
|
|
558
622
|
**Per-item budget** — cap cost for a single workflow execution:
|
|
559
623
|
|
|
560
624
|
```typescript
|
|
@@ -594,7 +658,9 @@ npx axl-eval rescore ./results/v1.json ./evals/qa.eval.ts
|
|
|
594
658
|
npx axl-eval rescore ./results/v1.json ./evals/qa.eval.ts --output ./results/v1-rescored.json
|
|
595
659
|
```
|
|
596
660
|
|
|
597
|
-
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).
|
|
598
664
|
|
|
599
665
|
Programmatically:
|
|
600
666
|
|
|
@@ -602,7 +668,8 @@ Programmatically:
|
|
|
602
668
|
import { rescore } from '@axlsdk/eval';
|
|
603
669
|
|
|
604
670
|
const rescored = await rescore(originalResult, [updatedScorer, newScorer], runtime, {
|
|
605
|
-
concurrency: 10,
|
|
671
|
+
concurrency: 10, // items rescored in parallel
|
|
672
|
+
scorerConcurrency: 3, // scorers per item run in parallel (default 5)
|
|
606
673
|
});
|
|
607
674
|
|
|
608
675
|
console.log(rescored.metadata.rescored); // true
|
|
@@ -688,7 +755,7 @@ const comparison = evalCompare(baselineRuns, candidateRuns);
|
|
|
688
755
|
|
|
689
756
|
| Type | Description |
|
|
690
757
|
|------|-------------|
|
|
691
|
-
| `EvalConfig` | Eval definition (workflow, dataset, scorers, concurrency, budget) |
|
|
758
|
+
| `EvalConfig` | Eval definition (workflow, dataset, scorers, concurrency, scorerConcurrency, budget) |
|
|
692
759
|
| `EvalResult` | Full eval output (items, summary, cost, duration) |
|
|
693
760
|
| `EvalItem` | Per-item result (input, output, scores, scoreDetails, metadata, traces?) |
|
|
694
761
|
| `EvalSummary` | Aggregate statistics (count, failures, scorer stats, timing) |
|
|
@@ -698,7 +765,7 @@ const comparison = evalCompare(baselineRuns, candidateRuns);
|
|
|
698
765
|
| `EvalRegression` / `EvalImprovement` | Per-item change record (itemIndex, scorer, scores, delta) |
|
|
699
766
|
| `ScorerDetail` | Per-scorer detail (score, metadata, duration, cost) |
|
|
700
767
|
| `ScorerResult` | Scorer return type (`{ score, metadata?, cost? }`) |
|
|
701
|
-
| `RescoreOptions` | Options for `rescore()` (`concurrency`) |
|
|
768
|
+
| `RescoreOptions` | Options for `rescore()` (`concurrency`, `scorerConcurrency`, `signal`) |
|
|
702
769
|
| `MultiRunSummary` | Aggregated multi-run output (per-scorer mean/std/min/max) |
|
|
703
770
|
| `BootstrapCIResult` | CI result (`{ lower, upper, mean, pRegression, pImprovement }`) |
|
|
704
771
|
| `RunEvalOptions` | Options for `runEval()` (`onProgress`, `signal`, `captureTraces`) |
|