Eval Depth (v0.65.0)

Failure-mode coverage goes from 7 to 11. The v0.56 diagnose report card stays the daily driver; v0.65 adds four optional deeper probes you opt into when you actually care, plus a fifth piece that is not a probe at all: a calibration gate for the judge the other evals lean on.

soup eval behavior — pre/post safety diff

bash
soup eval behavior <run-id> --battery xstest \
  --evidence ./responses.json --output ./behavior.json

Bundled safety / refusal / jailbreak / sycophancy probe sets, scored pre-FT vs. post-FT with the v0.26 / v0.56 OK ≥0.85 / MINOR ≥0.60 / MAJOR thresholds.

Five batteries at launch:

BatteryWhat it catches
xstestOver-refusal on benign prompts
harmbenchJailbreak resistance
jailbreakbenchJailbreak prompt-pair contrasts
elephantSycophancy / opinion-shifting
sycevalSycophantic alignment to user

Evidence is operator-supplied JSON {pre_responses, post_responses, oracle} — no auto-rollout. 16 MiB file cap, O_NOFOLLOW open against symlink swap.

soup eval capability — lm-eval-harness task surface

bash
soup eval capability <run-id> --suite full --output ./capability.json

Validated lm-eval-harness task IDs for 7 bundled benchmarks — MMLU-Pro, GPQA, BBEH, AIME, MATH-500, HumanEval+, SWE-bench-Verified. Four profiles: full, fast, math, code.

Operator runs the harness; soup eval capability validates the task IDs and emits the runbook.

soup eval checklist — MFT / INV / DIR DSL

yaml
# spec.yaml
tests:
  - name: capital_facts
    kind: mft                     # minimum functionality
    prompts: ["What is the capital of France?"]
    expected: ["Paris"]
  - name: paraphrase_stable
    kind: inv                     # invariance under paraphrase
    prompts: ["Capital of FR?", "Tell me FR capital"]
    expected: ["Paris"]
  - name: more_polite
    kind: dir                     # directional perturbation
    prompts: ["You're rude.", "You're being unhelpful."]
    expected: ["apolog"]

CheckList-style behavioral DSL. Up to 1,000 tests per spec; 1 MiB YAML cap; enforce_under_cwd_and_no_symlink on file open.

soup eval irt-subset — Rasch IRT cost-cut

bash
soup eval irt-subset ./responses.jsonl --size small --output ./plan.json

Fits a 1-parameter Rasch IRT model to per-item correctness signals and selects a minimum-cost subset that preserves ranking power:

  • full = 100% of items
  • small = ~30% (Rasch information-weighted)
  • tiny = ~10%

The math: P(correct | θ, β) = σ(θ - β), item information I(β) = σ(-β) · σ(β). Information peaks at β≈0 (50/50 items most discriminate). Pure-Python kernel — no numpy/scipy.

256 MiB JSONL cap; item-ID validated (≤256 chars, no null bytes).

Judge calibration — measure the judge before you trust its scores

Every judge-based eval on this page, and soup ship --task-mode judge_score and pairwise too, rests on an LLM judge. A judge is an instrument, and an uncalibrated one produces numbers that look exactly like calibrated ones. v0.65.0 shipped the piece that measures it.

Library only, no CLI surface. It is soup_cli.eval.calibrate and you call it from Python. There is no soup eval calibrate subcommand. Note also that the gate's own error message suggests running soup eval design --calibrate, and that flag does not exist: soup eval design takes only --goal, --num-dimensions and --output. Ignore the suggestion, not the error.

Two things get measured, both from judging the same pairs in both orders:

  • Position bias (fit_position_bias) returns a coefficient in [-1, 1]. 0.0 means the judge agrees with itself when you swap the slots; +1.0 means it always picks whichever answer was shown first, regardless of content; -1.0 means it always picks the second. It is computed as the rate at which the two orderings disagree, so a perfectly consistent judge scores 0.0.
  • Agreement rate against an oracle set you supply.

conformal_threshold(scores, alpha=0.1) then turns the calibration scores into an abstention threshold: at production time, judgements whose confidence falls below it should be abstained from rather than scored, which is what preserves the coverage you asked for. alpha=0.0 abstains on nothing, alpha=1.0 abstains on everything below the maximum.

The part worth having is the gate:

python
from soup_cli.eval.calibrate import ensure_judge_calibrated, load_judge_calibration

report = load_judge_calibration("judge_calibration.json")
ensure_judge_calibrated(report, min_agreement=0.7, max_bias=0.3)

ensure_judge_calibrated raises rather than returning a verdict, and it raises on all four of: no calibration ran at all, the report says not calibrated, agreement below min_agreement, or position bias outside ±max_bias. That is the same design choice as soup reward stress refusing a gameable verifier: an instrument that has not been checked should stop the run, not quietly score it.

Reports persist with write_judge_calibration / load_judge_calibration and are stored in the registry under the artifact kind judge_calibration, so a judge's calibration travels with the run that used it.

See also

  • Diagnose — v0.56 6-probe report card, the lighter daily driver.
  • Post-train x-rays — v0.66 mechanistic interpretability probes that stack on top of v0.65 behaviour evals.

Soup is free and Apache-2.0. If it saved you a training run, starring the repo costs nothing and helps most. You can also fund the GPU time behind the work a 4 GB laptop cannot reach.