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
soup eval behavior <run-id> --battery xstest \
--evidence ./responses.json --output ./behavior.jsonBundled 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:
| Battery | What it catches |
|---|---|
xstest | Over-refusal on benign prompts |
harmbench | Jailbreak resistance |
jailbreakbench | Jailbreak prompt-pair contrasts |
elephant | Sycophancy / opinion-shifting |
syceval | Sycophantic 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
soup eval capability <run-id> --suite full --output ./capability.jsonValidated 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
# 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"]soup eval checklist spec.yaml --evidence ./responses.json --output ./checklist.jsonCheckList-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
soup eval irt-subset ./responses.jsonl --size small --output ./plan.jsonFits 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 itemssmall= ~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.calibrateand you call it from Python. There is nosoup eval calibratesubcommand. Note also that the gate's own error message suggests runningsoup eval design --calibrate, and that flag does not exist:soup eval designtakes only--goal,--num-dimensionsand--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:
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.