Evaluation Runs
@capture records one function call. An evaluation is a different shape: many
cases, one run, and a verdict over the whole thing. briefcase.integrations.evals
records that shape as decision records, so an eval lands in the same store as
everything else you capture.
pip install briefcase-ai[evals] New in 4.0.0
Install
pip install briefcase-ai[evals]The extra installs zstandard on Python below 3.14, which zipfile needs to read
inspect-ai .eval archives. Nothing else; the parsers are stdlib only.
Log a run as it happens
EvalRun is a context manager. Each log_case emits one eval.case record;
leaving the block emits a single eval.run summary.
from dataclasses import dataclass
@dataclassclass Case: id: str question: str answer: str prompt_tokens: int = 0 completion_tokens: int = 0
dataset = [ Case("q1", "What is 2+2?", "4", 8, 1), Case("q2", "Capital of France?", "Paris", 9, 1),]model = lambda question: "4" if "2+2" in question else "Paris"import briefcasefrom briefcase.integrations.evals import EvalRun
briefcase.observe("runs.jsonl")
with EvalRun("gsm8k", model="claude-opus-4-5") as run: for case in dataset: answer = model(case.question) run.log_case( case.id, inputs=case.question, outputs=answer, target=case.answer, passed=answer == case.answer, scores={"exact_match": 1.0 if answer == case.answer else 0.0}, input_tokens=case.prompt_tokens, output_tokens=case.completion_tokens, )
print(run.summary()["pass_rate"])summary() aggregates pass rate, per-score mean/min/max/count, and token
totals. With a model and token counts it also estimates cost through
briefcase.cost; pass include_drift=True to add output drift across cases.
Both degrade to None rather than raising when the calculator is unavailable.
Replay a log you already have
The parsers are stdlib only and never import the evaluation framework, so you can read a log on a machine where neither tool is installed.
from briefcase.integrations.evals import from_inspect_log, from_lm_eval_results, replay
# inspect-ai: a .json log or a .eval archivereplay(from_inspect_log("logs/2026-08-12_gsm8k.eval"))
# lm-eval-harness: results plus its per-sample jsonlreplay(from_lm_eval_results("results.json", "samples_gsm8k.jsonl"))replay() emits the same eval.case and eval.run records as a live run, so
a replayed log and a fresh one are queried identically.
To inspect before emitting, parse without replaying:
parsed = from_inspect_log("logs/run.eval")parsed.name # task nameparsed.model # model under testparsed.metrics # aggregate scores, e.g. {"match/accuracy": 0.5}parsed.cases # normalized case dicts, ready for EvalRun.ingestA file that does not match the expected shape raises ValueError naming the
format, rather than returning a half-parsed run.
Feed parsed cases into a run you control
from briefcase.integrations.evals import EvalRun, from_lm_eval_results
parsed = from_lm_eval_results("results.json", "samples.jsonl")
run = EvalRun("nightly", exporter=my_exporter, model=parsed.model)run.ingest(parsed.cases)print(run.finish()["outputs"]["pass_rate"])Record types
| Type | Emitted | Carries |
|---|---|---|
eval.case | once per case | case id, inputs, outputs, target, scores, pass flag, tokens |
eval.run | once per run | pass rate, per-score statistics, token totals, cost, optional drift |
Both flow through whatever exporter briefcase.observe(...) configured, exactly
like a @capture record.
What the parsers understand
-
inspect-ai —
.jsonlogs and.evalarchives. Letter grades mapC/I/P/Nto1.0/0.0/0.5/0.0; chat-message inputs are flattened torole: contentlines; output is read from the first choice’s message. -
lm-eval-harness —
results.jsonfor aggregate metrics (filter suffixes stripped, stderr and alias dropped) and the samples jsonl for per-case rows. Multiple-choice tasks produce one[logprob, is_greedy]pair per choice rather than generated text, sooutputsholds that list as JSON.
Parsing is best-effort over the documented shapes: missing optional keys are skipped. Both parsers are verified against artifacts inspect-ai 0.3.257 and lm-eval-harness 0.4.12 actually wrote.
Limits
pass_rate counts only cases that carried a verdict. log_case without
passed= records the case and leaves it out of the rate. Two cases where one has no
verdict and the other passed give total_cases 2 and pass_rate 1.0. Read
passed + failed against total_cases before you trust the number.
Cost and drift degrade to None, never to an error. Cost needs a model the
pricing table knows and token counts on the
cases; an unknown model id yields None with nothing logged. Drift needs
include_drift=True and at least two string outputs. A None here means not
computed, not zero.
finish() is one-way. log_case after it raises RuntimeError. Calling
finish() again returns the same summary without emitting a second eval.run.
The parsers read the shapes those two tools write today. A log from a much older
or newer release, or a fork with a different schema, raises ValueError naming the
format rather than half-parsing. That is deliberate: a partial eval is worse than no
eval.
.eval archives need a zstd backend below Python 3.14. See the caution above.
Nothing re-runs the eval. replay() re-emits records from a log you already
have. Producing new results is your harness’s job.
API reference
briefcase.integrations.evals has the full EvalRun and
parser signatures.
Where this fits
Evaluation runs close the Operate & Evaluate act: aggregate many decisions into one comparable verdict instead of reading them one at a time.