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} by default. soup eval behavior --base-model <m> --adapter <a> [--device <d>] generates both response sets itself instead. With neither, it prints "No --evidence supplied; emitting neutral OK report." and exits 0, so an OK with no evidence measured nothing. 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.

By default soup eval capability validates the task IDs and emits a runbook for you to execute. Add --live with --model <hf-id> and it runs the harness itself for the suite's tasks. --tasks is an optional comma-separated lm-eval task override that bypasses --suite, and --limit and --device also apply.

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: ["sorry"]
bash
soup eval checklist spec.yaml --evidence ./responses.json --output ./checklist.json

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 does not generate the responses, and it loads no model. responses.json maps each test name to the model's responses, one per prompt, for example {"capital_facts": ["The capital is Paris."], "paraphrase_stable": ["Paris.", "It is Paris"], "more_polite": ["Sorry about that", "I am sorry"]}. An expected keyword matches as a whole word, case-insensitively, so a stem such as apolog does not match apologize. For mft and dir tests, pass rates of 0.85 or more are OK, 0.60 or more MINOR and anything lower MAJOR; an inv test is OK or MAJOR. The command exits 2 when the overall verdict is MAJOR.

A test with no entry in the evidence, or no --evidence at all, scores a neutral OK with 0 of 0 checked, and the command exits 0. An all-OK report without evidence means nothing ran.

soup eval irt-subset — IRT cost-cut

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

Fits a Rasch (1PL) IRT model by default to per-item correctness signals and selects a minimum-cost subset that preserves ranking power. --model 2pl adds a per-item discrimination, fitted by coordinate-ascent maximum likelihood instead of the closed-form Rasch fit (v0.71.6), and --model 3pl also a guessing floor, which is not fitted: it is set from the correct-rate floor among the lowest-ability third of respondents (capped), because a joint 3PL fit is unstable without priors. Input is JSONL with one row per response, {"item_id": "q1", "correct": true} for the default 1PL, and {"respondent_id": "model-a", "item_id": "q1", "correct": true} for 2pl and 3pl, which fit a response matrix and exit 2 on a row without respondent_id. --size picks the profile:

  • 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 — the 7-probe report card (6 at v0.56, citation recall added in v0.71.10), 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.