A framework for running evals against small (and large) models
Project description
smevals
A framework for running evals against small (and large) models
Installation
uv tool install smevals
Or pip install smevals, or just uvx smevals --help.
Vocabulary used by this project
The top-level concept is an Eval: a collection of Tasks used to determine how good a particular model or model-and-harness configuration is at a specific high-level capability, such as text-to-SQL, drawing a pelican riding a bicycle, or evaluating whether an implementation satisfies a provided specification.
Evals can optionally be grouped into Suites of related Evals, primarily as a mechanism for organizing them on disk.
An Eval is a collection of Tasks. These are the individual exercises that a model must complete for its abilities to be evaluated.
A Config describes the setup used to attempt Tasks. It specifies a model and may include model parameters, system prompts, tools and other settings.
To gather evidence, we create a Run. A Run is the immutable record of executing one Task against one Config using a Runner. A Runner is a reusable CLI program that may send prompts directly to a model or build on an agent harness such as Codex or Pi.
The same Task and Config can be executed multiple times, producing multiple Runs to help account for non-deterministic results. Each Run includes a timestamp to help track these.
Once we have gathered Runs, we apply a Grader to each Run to produce a Grade. A Grader is a configured sequence of Checks, plus rules for combining their results into that Grade.
Checks are individual assertions or measurements. Some may be simple, such as “does the output contain this text?” Others may be more complex, such as “render this SVG to an image and have an LLM judge assess it”. Each Check names the Checker that performs it, along with configuration for that Checker such as patterns, rubrics or expected values. A Check can be marked as required, in which case its failure halts the Grader and skips the remaining Checks.
A Checker is a named operation or a reusable CLI program that implements one kind of Check. contains and xml-valid are named operations, ../checkers/render-svg might be a custom program. The same Checker can be used by many Checks across many Graders. The Checks in a Grader execute in order and share a working directory, so a Checker may create files - such as a rendered image - which are kept with the Grade as artifacts and available to later Checks in the sequence.
A Grade is the result of applying a Grader to a Run. It records the result of each Check and may contain an overall pass/fail outcome and/or a numeric score. Grades can also include additional notes which are not used for scoring but may help interpret the results in the future.
We may later change the Grader we use to evaluate Runs without executing the Runs again. A single Run can therefore be evaluated multiple times, producing multiple Grades using different Graders.
Building an Eval
An Eval is any directory containing an eval.yaml file:
my-eval/
├── eval.yaml # name and description
├── tasks/ # one YAML file per Task
├── configs/ # one YAML file per Config
├── graders/ # one YAML file per Grader
├── checkers/ # custom Checker executables (by convention)
├── run-llm # Runner executable (any name, any location)
└── runs/ # created by smevals run - never edit by hand
Example Eval: Grading Haikus
Here's how to structure a complete Eval that asks models to write haikus and grades them on their structure.
The Eval consists of five files.
my-eval/eval.yaml defines a name and description:
name: haiku
description: >-
Can the model write a haiku on demand? Graded on structure:
the reply must be exactly three lines.
An Eval must have one or more Tasks. Each of these is defined as a tasks/*.yaml YAML file.
my-eval/tasks/pelicans.yaml must have a name; a prompt is the common case, but any other keys are allowed and are passed to the Runner as environment variables:
name: pelicans
prompt: Write a haiku about pelicans. Reply with only the haiku, three lines.
An Eval also needs at least one Config, defined in configs/*.yaml. If there is just one of these it should be called default.
my-eval/configs/default.yaml - the Config named default is used when no -c option is passed to smevals run.
runner specifies a path to an executable program relative to this file:
name: default
runner: ../run-llm
model: gpt-4.1-mini
Here's that Runner script:
my-eval/run-llm - This one uses the llm CLI, but any executable honoring the contract below works. Make it executable with chmod +x:
#!/usr/bin/env bash
set -euo pipefail
llm -m "$SMEVALS_MODEL" "$SMEVALS_PROMPT"
llm logs -c --json > log.json
The Eval also needs a default Grader, which will be used to grade the results of each Run:
my-eval/graders/default.yaml:
name: default
checks:
- checker: ../checkers/three-lines
required: true
scoring:
pass_threshold: 1.0
The checker can be a relative path to a script - similar to runner: above - or can be the name of a built-in checker, listed below.
my-eval/checkers/three-lines - a custom Checker, also chmod +x:
#!/usr/bin/env python3
import json, os, pathlib, sys
raw = (pathlib.Path(os.environ["SMEVALS_RUN_DIR"]) / "output.txt").read_text()
lines = [line for line in raw.strip().splitlines() if line.strip()]
print(json.dumps({
"score": 1.0 if len(lines) == 3 else 0.0,
"metrics": {"line_count": len(lines)},
"notes": f"{len(lines)} non-empty line(s)",
}))
sys.exit(0 if len(lines) == 3 else 1)
To run the eval, grade it and then view the results:
smevals run my-eval -g # run every task, grade as each finishes
smevals run my-eval -m gpt-4.1-nano -m gemini-2.5-flash -g # more models
smevals report my-eval # markdown report in the terminal
smevals serve my-eval # live web UI on http://127.0.0.1:7001
The Runner contract
smevals run executes the Runner once per Task/model combination, with no arguments. Everything arrives through environment variables:
SMEVALS_MODEL- the model to use, from the Config or the-moption.SMEVALS_TASK- the Task's name.SMEVALS_PROMPT- the Task'sprompt, only set if the Task has one.SMEVALS_TASK_<KEY>- every scalar key of the Task, uppercased: a Task withsubmission: mutant-003providesSMEVALS_TASK_SUBMISSION=mutant-003.SMEVALS_RUN_DIR- absolute path to the Run's directory.
The working directory is the Run's directory. The contract:
- Standard output is captured as the Run's
output.txt- it should be the model's response. - Standard error is captured as
stderr.txt. - A non-zero exit code marks the Run as failed.
- Any other files the Runner writes to its working directory are kept as Run artifacts (the
log.jsonin the example above).
A Runner that drives an agent harness instead of a plain model call follows the same contract: assemble whatever inputs the Task's keys describe, run the harness, print the final result to standard output.
Graders
A Grader is a YAML file in graders/:
name: default
checks:
- checker: contains # a built-in Checker, by name
value: "<svg"
required: true
- checker: ../checkers/render-svg # a custom Checker, by path
input: extracted.svg
creates: render.png # smevals verifies this file gets created
required: true
- checker: ../checkers/llm-judge-image
image: render.png
model: gpt-4.1
rubric: Score this image from 0 to 10 ...
scoring:
pass_threshold: 0.5
Each entry in checks names a Checker plus its configuration. Reserved keys:
checker- a built-in name, or a path to an executable relative to the Grader file.required- if true and the Check fails, grading halts and the remaining Checks are recorded as skipped.creates- a filename, or list of filenames, the Checker promises to create in the shared workspace; the Check fails if any of them do not appear. A Checker may write any number of additional files beyond those promised - everything in the workspace is kept as a Grade artifact.
All other keys are configuration for the Checker, passed through via environment variables.
Built-in Checkers
contains- passes if the Run'soutput.txtcontainsvalue.xml-valid- passes iffile(looked up in the grade workspace, then the Run directory) parses as well-formed XML.
Outcomes and scores
The Grade's outcome and score are computed as follows:
- The Grade's score is the last
scoreemitted by any Check - typically the final, most expensive Check. However, if any Check fails without emitting a score of its own, the Grade's score is null: a stale score from an earlier Check never stands in for one that did not run. - The outcome is
failif any Check failed, otherwisepassif the score meetsscoring.pass_threshold(or if there is no threshold or no score).
The Checker contract
smevals grade executes each Check's Checker with no arguments and these environment variables:
SMEVALS_RUN_DIR- absolute path to the Run directory being graded. Read the model's output from$SMEVALS_RUN_DIR/output.txt.SMEVALS_CHECK- the full Check configuration as JSON, for structured values like lists.SMEVALS_CHECK_<KEY>- every scalar key of the Check, uppercased:rubric:becomesSMEVALS_CHECK_RUBRIC.SMEVALS_TASKandSMEVALS_TASK_<KEY>- the Task's name and scalar keys, so a Checker can locate per-Task resources such as expected-answer files.
The working directory is the grade workspace, shared by all Checks in the Grader in order: files written by one Check (a rendered image, an extracted document) are available to later Checks and are kept with the Grade as artifacts.
A Checker signals pass or fail with its exit code (0 is a pass). It can also emit a JSON object on standard output with up to five keys, which are recorded in the Grade:
score- a float from 0.0 to 1.0.metrics- an object mapping names to numbers or booleans, e.g.{"precision": 0.9, "status_correct": true}. Reports aggregate numbers as mean ± stderr and booleans as rates.tags- a list of short labels, e.g.["wearing_a_hat", "correct_bicycle_frame_shape"]. Tags are open vocabulary and presence-only: an absent tag means "not observed", not "false". They are normalized to lowercase snake_case, and the Grade records the union of all its Checks' tags. Reports aggregate them as counts and shares, and the web UI uses them for filtering.notes- a human-readable string explaining the result. Never aggregated.details- an object of structured diagnostics, such as predicted-versus-expected lists. Kept with the Grade but ignored by aggregation.
Any other keys are folded into details. A Checker that fails may still emit a score (a partial-credit measurement); a Checker that crashes before scoring leaves the Grade unscored, as described above.
Runs and Grades on disk
Every Run is a directory:
runs/<task>/<config>/<model>/<timestamp>/
├── run.yaml # the record: full task, resolved config, timing, exit code
├── output.txt # the model's response (runner stdout)
├── stderr.txt # only present if the runner wrote to stderr
├── ... # any other artifacts the runner wrote
└── grades/
└── <grader>/
├── grade.yaml # outcome, score, tags, per-check results
├── grader.yaml # snapshot of the Grader that produced this Grade
└── ... # artifacts written by Checkers
The model name is slugified for the path; the exact name is in run.yaml. run.yaml is written last, so its presence marks a complete Run. Runs are immutable - grading only ever adds files under grades/.
Each Grade includes a byte-for-byte snapshot of its Grader. smevals grade uses this for repeatability:
- By default it grades only Runs that have no Grade from the named Grader, and reports how many existing Grades were produced by an older version of the Grader spec.
--regradedeletes and re-creates every Grade for that Grader, so nothing stale survives. Use it after editing a Grader.- Multiple Graders coexist: each grades into its own
grades/<name>/directory, so an eval can have e.g. a cheap deterministicdefaultgrader and an LLM-judgejudgegrader side by side.
By default runs/ lives inside the Eval directory. Pass --runs-dir DIR to run, grade and report to keep runs elsewhere; they are then namespaced by Eval name.
Commands
smevals run EVAL [-m MODEL]... [-c CONFIG] [-t TASK]... [-g [GRADER]] [--runs-dir DIR]
Executes every Task (or just those named with -t) against every model given with -m (default: the Config's model), using the Config named by -c (default: default). -g grades each Run the moment it finishes; -g NAME uses that Grader, bare -g uses default. Exits non-zero if any Run fails or grades as fail.
smevals grade EVAL [-g GRADER] [--regrade] [--runs-dir DIR]
Applies the Grader to every ungraded Run. --regrade discards and redoes existing Grades from this Grader.
smevals report EVAL [-g GRADER] [--by-task] [--json] [--runs-dir DIR]
Prints a markdown report: leaderboard of config × model with mean ± stderr scores and failure counts, tag shares, and per-model blocks with metrics. --by-task adds per-task scores. --json emits the raw grade rows instead.
smevals serve EVAL_OR_SUITE... [-p PORT] [--host HOST] [-g GRADER]
Serves a live web UI (default port 7001) over one or more Evals. Data is re-read from disk on every poll, so the pages update as new Runs and Grades land. A directory that is not itself an Eval is treated as a Suite and searched recursively for Evals.
smevals build EVAL_OR_SUITE... [-o DIR] [-g GRADER]
Builds the same web UI as a self-contained static site (default build/), copying run artifacts into it. Each invocation adds or refreshes the given Evals in the output directory and leaves other Evals already built there untouched, so one site can aggregate Evals from many repositories.
smevals docs
Outputs this document.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file smevals-0.2.0.tar.gz.
File metadata
- Download URL: smevals-0.2.0.tar.gz
- Upload date:
- Size: 24.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4b6d755df8c7c71c29bdb2042ce615209e1d85a27ac7af9ae91c0b6539715cc7
|
|
| MD5 |
0ef4b9bc587c5f446abf91973b4f3603
|
|
| BLAKE2b-256 |
c8b7afd84f7b346592caeae911808b272dd25937e25ecd693447dbbd7d43204c
|
File details
Details for the file smevals-0.2.0-py3-none-any.whl.
File metadata
- Download URL: smevals-0.2.0-py3-none-any.whl
- Upload date:
- Size: 26.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ff4f52ba50efa16279738f00f4f2f0b760e04f815304e804d765d16853e7055b
|
|
| MD5 |
c4b2c51bca1dc5bb622a2552ed393d38
|
|
| BLAKE2b-256 |
eed15318dc62348687c6a0d7d88720b0fe2b069fb67309019d9eecd4f66fc69e
|