@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 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, // run 10 items in parallel
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 `runIndex` are stripped from inherited metadata (rescored results are independent evaluations). Only scorer cost is tracked (workflow cost is zero).
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`) |