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} — no auto-rollout. 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.
Operator runs the harness; soup eval capability validates the task IDs and emits the runbook.
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: ["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
soup eval irt-subset ./responses.jsonl --size small --output ./plan.jsonFits a 1-parameter Rasch IRT model to per-item correctness signals and selects a minimum-cost subset that preserves ranking power:
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 — 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.