evalproof 0.2.1__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {evalproof-0.2.1 → evalproof-0.3.0}/CHANGELOG.md +80 -3
- {evalproof-0.2.1 → evalproof-0.3.0}/CITATION.cff +2 -2
- {evalproof-0.2.1 → evalproof-0.3.0}/PKG-INFO +128 -15
- {evalproof-0.2.1 → evalproof-0.3.0}/README.md +128 -15
- {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/__init__.py +5 -1
- {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/_version.py +1 -1
- {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/cache.py +10 -2
- {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/cli.py +65 -4
- {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/config.py +48 -6
- evalproof-0.3.0/src/evalproof/gate.py +135 -0
- evalproof-0.3.0/src/evalproof/importers/__init__.py +75 -0
- evalproof-0.3.0/src/evalproof/importers/_common.py +309 -0
- evalproof-0.3.0/src/evalproof/importers/deepeval.py +127 -0
- evalproof-0.3.0/src/evalproof/importers/inspect_ai.py +207 -0
- evalproof-0.3.0/src/evalproof/importers/lm_eval.py +114 -0
- evalproof-0.3.0/src/evalproof/importers/promptfoo.py +256 -0
- evalproof-0.3.0/src/evalproof/importers/table.py +246 -0
- {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/providers/__init__.py +32 -7
- {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/providers/anthropic.py +38 -11
- {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/providers/base.py +27 -0
- {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/providers/ollama.py +10 -2
- evalproof-0.3.0/src/evalproof/providers/openai.py +163 -0
- evalproof-0.3.0/src/evalproof/providers/profiles.py +164 -0
- {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/report.py +23 -12
- {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/stats.py +20 -0
- evalproof-0.3.0/tests/fixtures/README.md +9 -0
- evalproof-0.3.0/tests/fixtures/deepeval/test_run.json +167 -0
- evalproof-0.3.0/tests/fixtures/inspect/log_formats.json +399 -0
- evalproof-0.3.0/tests/fixtures/lm-eval/gpt2/samples_hellaswag_2024-05-01T10-20-30.123456.jsonl +4 -0
- evalproof-0.3.0/tests/fixtures/lm-eval/llama3/samples_gsm8k_2024-05-01T10-20-30.123456.jsonl +6 -0
- evalproof-0.3.0/tests/fixtures/promptfoo/sample-export.json +692 -0
- evalproof-0.3.0/tests/fixtures/table/long.csv +9 -0
- evalproof-0.3.0/tests/fixtures/table/long.jsonl +8 -0
- evalproof-0.3.0/tests/fixtures/table/wide.csv +4 -0
- evalproof-0.3.0/tests/test_action.py +85 -0
- {evalproof-0.2.1 → evalproof-0.3.0}/tests/test_cache_systems.py +22 -0
- evalproof-0.3.0/tests/test_holm_gate.py +168 -0
- evalproof-0.3.0/tests/test_importers.py +795 -0
- evalproof-0.3.0/tests/test_providers.py +517 -0
- {evalproof-0.2.1 → evalproof-0.3.0}/tests/test_public_api.py +14 -0
- evalproof-0.3.0/tests/test_release_fixes.py +162 -0
- {evalproof-0.2.1 → evalproof-0.3.0}/tests/test_security.py +23 -9
- evalproof-0.2.1/src/evalproof/gate.py +0 -107
- evalproof-0.2.1/src/evalproof/providers/openai.py +0 -76
- evalproof-0.2.1/tests/test_providers.py +0 -161
- {evalproof-0.2.1 → evalproof-0.3.0}/.gitignore +0 -0
- {evalproof-0.2.1 → evalproof-0.3.0}/LICENSE +0 -0
- {evalproof-0.2.1 → evalproof-0.3.0}/NOTICE +0 -0
- {evalproof-0.2.1 → evalproof-0.3.0}/pyproject.toml +0 -0
- {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/calibration.py +0 -0
- {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/compare.py +0 -0
- {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/dataset.py +0 -0
- {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/models.py +0 -0
- {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/py.typed +0 -0
- {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/runner.py +0 -0
- {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/scorers/__init__.py +0 -0
- {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/scorers/deterministic.py +0 -0
- {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/scorers/judge.py +0 -0
- {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/scorers/retrieval.py +0 -0
- {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/systems.py +0 -0
- {evalproof-0.2.1 → evalproof-0.3.0}/tests/__init__.py +0 -0
- {evalproof-0.2.1 → evalproof-0.3.0}/tests/conftest.py +0 -0
- {evalproof-0.2.1 → evalproof-0.3.0}/tests/test_calibration.py +0 -0
- {evalproof-0.2.1 → evalproof-0.3.0}/tests/test_compare_gate_report.py +0 -0
- {evalproof-0.2.1 → evalproof-0.3.0}/tests/test_config_dataset.py +0 -0
- {evalproof-0.2.1 → evalproof-0.3.0}/tests/test_runner_cli.py +0 -0
- {evalproof-0.2.1 → evalproof-0.3.0}/tests/test_scorers.py +0 -0
- {evalproof-0.2.1 → evalproof-0.3.0}/tests/test_stats.py +0 -0
|
@@ -2,6 +2,81 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes are listed here. The project follows [Semantic Versioning](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.3.0] — 2026-11-12
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `evalproof import` and `evalproof.import_run`: per-case results from promptfoo (JSON and
|
|
12
|
+
JSONL), Inspect (`.json` and `.eval` logs), DeepEval, lm-evaluation-harness
|
|
13
|
+
(`samples_*.jsonl`) and generic long or wide `jsonl` and `csv` files become a run that
|
|
14
|
+
`compare` and the gate read. Case ids are stable across runs, system errors score as
|
|
15
|
+
failures, repeats collapse by `--epochs`, and NaN or infinite scores are refused with the
|
|
16
|
+
case and metric named. Raises `evalproof.ImportFormatError` (a `ValueError`; exit 2 on the
|
|
17
|
+
command line).
|
|
18
|
+
- Gating several metrics: `gate.metrics` in the suite or `--metric` repeated on
|
|
19
|
+
`compare`, with Holm-adjusted p-values (`evalproof.holm`). The decision carries
|
|
20
|
+
`adjusted_p`, the report gains a Holm-adjusted p column, and a single metric behaves as
|
|
21
|
+
before. ADR 0003 has the reasoning.
|
|
22
|
+
- The composite action's `metric` input takes several names, separated by newlines or commas.
|
|
23
|
+
- `examples/retrieval`: an offline retrieval example for recall@k, MRR and nDCG, with a
|
|
24
|
+
regression that recall@5 cannot see and MRR and nDCG@5 can. Contributed by
|
|
25
|
+
@HarshRajSinghania in #5. CI asserts its gate exits 1 on the regression and 0 on no change.
|
|
26
|
+
- MEASUREMENTS §8: the support-qa judge with thinking on (#3), with its runs, suite and
|
|
27
|
+
analysis in `measure.py`. Against labels audited case by case it passes no wrong answer
|
|
28
|
+
and κ rises, at 6.7× the judge time; the gate's decision does not change.
|
|
29
|
+
- A provider profile table (`providers/profiles.py`) behind one OpenAI-compatible adapter:
|
|
30
|
+
`openai`, `azure`, `gemini`, `mistral`, `groq`, `together`, `fireworks`, `deepseek`, `xai`,
|
|
31
|
+
`openrouter`, `cerebras`, the local servers `vllm`, `lmstudio` and `llamacpp`, and
|
|
32
|
+
`openai-compatible` for any other server (set `base_url`). Each profile carries its base
|
|
33
|
+
URL, key variable, auth header, token-limit field and a link to the provider's docs.
|
|
34
|
+
`OpenAIProvider` stays importable.
|
|
35
|
+
- `TargetConfig.api_key_env` names the environment variable that holds the key, and
|
|
36
|
+
`TargetConfig.headers` adds request headers to every HTTP provider; suite headers win over
|
|
37
|
+
a provider's own, and neither is part of the cache key or written to a run. A named key
|
|
38
|
+
variable that is not set is an error rather than a 401.
|
|
39
|
+
- Anthropic: a `refusal` stop reason is recorded as a case error naming its category, and so
|
|
40
|
+
is `max_tokens` reached before any text, with a hint to raise `params.max_tokens`. The
|
|
41
|
+
OpenAI-compatible adapter does the same for `finish_reason` `length` with no text, and
|
|
42
|
+
for `content_filter`, and reads replies that arrive as a list of typed content parts.
|
|
43
|
+
- A test pins the cache key of an existing Ollama call, so a change to it cannot slip in.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- Hosted providers (everything except Ollama and the local servers) send `temperature` and
|
|
48
|
+
`seed` only when the suite sets them. Before, `openai` sent `temperature: 0` and
|
|
49
|
+
`seed: 0` on every call, which reasoning models reject; now an unset value is left to the
|
|
50
|
+
provider's default. A hosted run that relied on the implicit `temperature: 0` should set
|
|
51
|
+
it in `params`. Ollama and the local servers still send both, so local runs repeat as
|
|
52
|
+
before and keep their cache keys; `GenerationParams` defaults are unchanged. A hosted call
|
|
53
|
+
that now leaves a value out gets a new cache key, so a reply cached at the old implicit
|
|
54
|
+
`temperature: 0` is not reused for it. To point `openai` at an older local server through
|
|
55
|
+
`base_url`, use `openai-compatible` or a local profile instead, which send both values.
|
|
56
|
+
- `openai` sends the token limit as `max_completion_tokens`, which OpenAI's reasoning models
|
|
57
|
+
require (they reject `max_tokens`), and `mistral` sends its seed as `random_seed`. The
|
|
58
|
+
default limit is still 512.
|
|
59
|
+
- The llama.cpp profile defaults to port 9931, the one its server documents now; for an older
|
|
60
|
+
build on 8080, set `base_url`.
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
|
|
64
|
+
- Anthropic calls to current Claude models failed with HTTP 400, because `temperature` was
|
|
65
|
+
sent on every request. It goes out only when the suite sets it, and no seed is sent, since
|
|
66
|
+
the Messages API has none.
|
|
67
|
+
- A gate built without validation and given no metric passed by gating nothing; it now
|
|
68
|
+
cannot gate (exit 4), and `compare --metric ""` is a usage error (exit 2).
|
|
69
|
+
- Found in review before release, in the new importers: two promptfoo prompts whose first 60
|
|
70
|
+
characters matched merged into one system (the label is no longer truncated, and two
|
|
71
|
+
prompts that share a label are an error), promptfoo output with repeated tests but no
|
|
72
|
+
`repeatIndex` now warns that it reads as separate cases, Inspect message ids no longer
|
|
73
|
+
change the dataset hash, a `null` promptfoo `testIdx` no longer crashes, and under
|
|
74
|
+
`--epochs any` or `mean` a case with one good repeat is no longer marked as an error.
|
|
75
|
+
- `examples/support-qa/cases.jsonl`: the q21 and q22 references named Enterprise, which the
|
|
76
|
+
product facts do not give SSO or an admin console (#7). The original set is kept as
|
|
77
|
+
`cases.v1.jsonl` so the recorded runs stay reproducible; the runs judged on the revised set
|
|
78
|
+
are in `runs/v2/`, and MEASUREMENTS §8 reports both.
|
|
79
|
+
|
|
5
80
|
## [0.2.1] — 2026-10-16
|
|
6
81
|
|
|
7
82
|
### Fixed
|
|
@@ -26,7 +101,7 @@ The first release on PyPI: `pip install evalproof`.
|
|
|
26
101
|
stored anywhere. Each file on PyPI carries a PEP 740 attestation, and the GitHub release
|
|
27
102
|
carries the same files with a build-provenance attestation.
|
|
28
103
|
- `SECURITY.md` with private vulnerability reporting and the trust boundaries, backed by a
|
|
29
|
-
test that runs both hosted providers with a
|
|
104
|
+
test that runs both hosted providers with a placeholder key and finds it in no file written.
|
|
30
105
|
- `CODE_OF_CONDUCT.md` (Contributor Covenant 2.1), issue and pull-request templates,
|
|
31
106
|
`CITATION.cff`, `CODEOWNERS`, Dependabot for actions and Python dependencies with a
|
|
32
107
|
three-day cooldown, and a pre-commit configuration.
|
|
@@ -101,9 +176,11 @@ First release.
|
|
|
101
176
|
`alpha`, plus p95-latency and cost budgets, with distinct exit codes.
|
|
102
177
|
- Content-addressed response cache; runs record the Ollama version and model digests.
|
|
103
178
|
- Markdown reports for GitHub step summaries, JSON reports, and a composite GitHub Action.
|
|
104
|
-
- Two examples: an offline ticket router (CI's gate contract) and a support
|
|
105
|
-
|
|
179
|
+
- Two examples: an offline ticket router (CI's gate contract) and a product support
|
|
180
|
+
assistant judged by a local model, with measurements.
|
|
106
181
|
|
|
182
|
+
[Unreleased]: https://github.com/technobasant/evalproof/compare/v0.3.0...HEAD
|
|
183
|
+
[0.3.0]: https://github.com/technobasant/evalproof/compare/v0.2.1...v0.3.0
|
|
107
184
|
[0.2.1]: https://github.com/technobasant/evalproof/compare/v0.2.0...v0.2.1
|
|
108
185
|
[0.2.0]: https://github.com/technobasant/evalproof/compare/v0.1.2...v0.2.0
|
|
109
186
|
[0.1.2]: https://github.com/technobasant/evalproof/compare/v0.1.1...v0.1.2
|
|
@@ -11,8 +11,8 @@ authors:
|
|
|
11
11
|
- family-names: Bhattarai
|
|
12
12
|
given-names: Basant
|
|
13
13
|
website: "https://basantbhattarai.com.np"
|
|
14
|
-
version: 0.
|
|
15
|
-
date-released: "2026-
|
|
14
|
+
version: 0.3.0
|
|
15
|
+
date-released: "2026-11-12"
|
|
16
16
|
license: Apache-2.0
|
|
17
17
|
repository-code: "https://github.com/technobasant/evalproof"
|
|
18
18
|
url: "https://pypi.org/project/evalproof/"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: evalproof
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Treat LLM evaluation as measurement: calibrate the judge, correct the pass rate, gate CI on significant regressions.
|
|
5
5
|
Project-URL: Homepage, https://github.com/technobasant/evalproof
|
|
6
6
|
Project-URL: Documentation, https://github.com/technobasant/evalproof#readme
|
|
@@ -67,8 +67,9 @@ evalproof:
|
|
|
67
67
|
4. **gates CI** only when a drop is both larger than your tolerance *and* statistically
|
|
68
68
|
significant, plus latency and cost budgets, with distinct exit codes.
|
|
69
69
|
|
|
70
|
-
It is local-first: Ollama is the default
|
|
71
|
-
every model call is cached by content so re-runs are free
|
|
70
|
+
It is local-first: Ollama is the default, every major hosted provider is one `target:` line
|
|
71
|
+
away ([Providers](#providers)), and every model call is cached by content so re-runs are free
|
|
72
|
+
and reproducible.
|
|
72
73
|
|
|
73
74
|
## Install
|
|
74
75
|
|
|
@@ -78,12 +79,12 @@ uv tool install evalproof # just the CLI
|
|
|
78
79
|
```
|
|
79
80
|
|
|
80
81
|
Python 3.11 to 3.14. `compare`, `calibrate` and `report` work on run files and need no
|
|
81
|
-
model. `run` needs a system to evaluate: a Python function, an HTTP endpoint, a local
|
|
82
|
-
Ollama
|
|
82
|
+
model. `run` needs a system to evaluate: a Python function, an HTTP endpoint, a local model
|
|
83
|
+
(Ollama, vLLM, LM Studio, llama.cpp), or a key for a hosted provider.
|
|
83
84
|
|
|
84
85
|
## What it found on a real run
|
|
85
86
|
|
|
86
|
-
A local `qwen3:1.7b` support assistant
|
|
87
|
+
A local `qwen3:1.7b` product support assistant, judged by `qwen3:8b`, 83
|
|
87
88
|
questions, one Apple M5. Full method and every table in [MEASUREMENTS.md](https://github.com/technobasant/evalproof/blob/main/MEASUREMENTS.md).
|
|
88
89
|
|
|
89
90
|
| Question | Measured |
|
|
@@ -94,7 +95,8 @@ questions, one Apple M5. Full method and every table in [MEASUREMENTS.md](https:
|
|
|
94
95
|
| Does correcting the judge's rate help? | On average yes (offset 4.8 → ~3.8 pts), but with 30 labels the interval doubles; at 83 cases the judge's bias is smaller than its own noise |
|
|
95
96
|
| Was the gold trustworthy? | No: of 13 judge–key disagreements the key was wrong 7 times, the judge 4. Most of the "bias" corrected was the gold's |
|
|
96
97
|
| Is pairwise judging safer? | No: 42% of verdicts flipped with the order of the two answers, and the judge preferred the candidate 42 to 6 — mostly where both answers were right and the candidate's was five times longer |
|
|
97
|
-
|
|
|
98
|
+
| Does a reasoning judge help? | Against audited labels, yes: thinking on passed no wrong answer (three without it) and κ rose from 0.86–0.88 to 0.90–0.94, at 6.7× the judge time. Against the raw answer key it looked worse, because the key is wrong on exactly those cases. The gate's decision did not change |
|
|
99
|
+
| Is it reproducible? | 20/20 identical outputs and verdicts with the cache off; a warm-cache re-run took 0.05 s instead of 363 s; a re-judge three days later matched 83/83 |
|
|
98
100
|
|
|
99
101
|
## Five-minute quickstart
|
|
100
102
|
|
|
@@ -115,6 +117,11 @@ uv run evalproof compare runs/baseline.json runs/noisy.json \
|
|
|
115
117
|
--suite examples/deterministic/suite.yaml --gate; echo "exit $?" # 0: 2 of 60 is noise
|
|
116
118
|
```
|
|
117
119
|
|
|
120
|
+
[`examples/retrieval`](https://github.com/technobasant/evalproof/blob/main/examples/retrieval) runs the same gate on a retrieval change: recall@5
|
|
121
|
+
does not move when the top hit drops to rank 5, while MRR and nDCG@5 do, and the gate fails
|
|
122
|
+
it. It was contributed by [@HarshRajSinghania](https://github.com/HarshRajSinghania), and CI
|
|
123
|
+
checks its exit codes too.
|
|
124
|
+
|
|
118
125
|
With Ollama installed, the LLM example is
|
|
119
126
|
[`examples/support-qa`](https://github.com/technobasant/evalproof/blob/main/examples/support-qa) (`ollama pull qwen3:1.7b` and
|
|
120
127
|
`ollama pull qwen3:8b`; about 6–10 minutes per run on an Apple M5).
|
|
@@ -122,6 +129,63 @@ With Ollama installed, the LLM example is
|
|
|
122
129
|
The browser app in [`app/`](https://github.com/technobasant/evalproof/blob/main/app) runs locally too:
|
|
123
130
|
`uv run --with-requirements app/requirements.txt streamlit run app/streamlit_app.py`.
|
|
124
131
|
|
|
132
|
+
## Providers
|
|
133
|
+
|
|
134
|
+
The target is `<provider>:<model>`; the model name is everything after the first colon, so
|
|
135
|
+
`openrouter:anthropic/claude-sonnet-4.5` and `ollama:qwen3:8b` both work. Two native adapters
|
|
136
|
+
(`ollama`, `anthropic`) and one OpenAI-compatible adapter driven by a table of provider
|
|
137
|
+
profiles ([`providers/profiles.py`](https://github.com/technobasant/evalproof/blob/main/src/evalproof/providers/profiles.py), each row cites the
|
|
138
|
+
provider's documentation). Plain HTTP throughout, no vendor SDK.
|
|
139
|
+
|
|
140
|
+
| Provider | Example target | Key variable | Where |
|
|
141
|
+
|---|---|---|---|
|
|
142
|
+
| `ollama` | `ollama:qwen3:8b` | none | local |
|
|
143
|
+
| `vllm` | `vllm:meta-llama/Llama-3.1-8B-Instruct` | `VLLM_API_KEY`, optional | local |
|
|
144
|
+
| `lmstudio` | `lmstudio:qwen3-8b` | optional, via `api_key_env` | local |
|
|
145
|
+
| `llamacpp` | `llamacpp:qwen3-8b` | `LLAMA_API_KEY`, optional | local |
|
|
146
|
+
| `anthropic` | `anthropic:claude-sonnet-4-5` | `ANTHROPIC_API_KEY` | hosted |
|
|
147
|
+
| `openai` | `openai:gpt-5` | `OPENAI_API_KEY` | hosted |
|
|
148
|
+
| `azure` | `azure:<deployment>` | `AZURE_OPENAI_API_KEY` | hosted, needs `base_url` |
|
|
149
|
+
| `gemini` | `gemini:gemini-2.5-flash` | `GEMINI_API_KEY` | hosted |
|
|
150
|
+
| `mistral` | `mistral:mistral-large-latest` | `MISTRAL_API_KEY` | hosted |
|
|
151
|
+
| `groq` | `groq:llama-3.3-70b-versatile` | `GROQ_API_KEY` | hosted |
|
|
152
|
+
| `together` | `together:meta-llama/Llama-3.3-70B-Instruct-Turbo` | `TOGETHER_API_KEY` | hosted |
|
|
153
|
+
| `fireworks` | `fireworks:accounts/fireworks/models/llama-v3p3-70b-instruct` | `FIREWORKS_API_KEY` | hosted |
|
|
154
|
+
| `deepseek` | `deepseek:deepseek-chat` | `DEEPSEEK_API_KEY` | hosted |
|
|
155
|
+
| `xai` | `xai:grok-4` | `XAI_API_KEY` | hosted |
|
|
156
|
+
| `openrouter` | `openrouter:anthropic/claude-sonnet-4.5` | `OPENROUTER_API_KEY` | hosted |
|
|
157
|
+
| `cerebras` | `cerebras:gpt-oss-120b` | `CEREBRAS_API_KEY` | hosted |
|
|
158
|
+
| `openai-compatible` | `openai-compatible:<model>` | optional, via `api_key_env` | any server, needs `base_url` |
|
|
159
|
+
|
|
160
|
+
```yaml
|
|
161
|
+
system:
|
|
162
|
+
target: openai-compatible:gpt-4o # a gateway, LiteLLM proxy, TGI, SGLang, ...
|
|
163
|
+
base_url: https://llm-gateway.internal/v1
|
|
164
|
+
api_key_env: TEAM_GATEWAY_KEY # the variable to read; the key itself never goes in the suite
|
|
165
|
+
headers: {x-team: search} # extra HTTP headers on every call, never secrets
|
|
166
|
+
params: {max_tokens: 2048}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
* `base_url` overrides any profile's default (a regional endpoint, a proxy, an older
|
|
170
|
+
`llama-server`, which listened on port 8080 before October 2026 and now defaults to 9931).
|
|
171
|
+
* `api_key_env` replaces the profile's variable for any provider; a variable named there
|
|
172
|
+
must be set.
|
|
173
|
+
* `headers` merge into every request and win over the adapter's own, for example
|
|
174
|
+
`anthropic-version`. They are not part of the cache key, and a suite file is usually
|
|
175
|
+
committed, so keep keys in the environment.
|
|
176
|
+
* Sampling: local servers (`ollama`, `vllm`, `lmstudio`, `llamacpp`) receive `temperature` and
|
|
177
|
+
`seed` on every call, defaults included, so a local run repeats. Hosted providers receive
|
|
178
|
+
them only when `params` sets them, because reasoning models reject a non-default
|
|
179
|
+
temperature; Anthropic never receives a seed, and since its current models also reject
|
|
180
|
+
`temperature`, it gets one only when the suite asks. `params.extra` adds any other body
|
|
181
|
+
field, such as `{thinking: {type: adaptive}, output_config: {effort: low}}`.
|
|
182
|
+
* Thinking models spend output tokens on thinking inside `max_tokens`, which defaults to 512
|
|
183
|
+
(changing it would change every cache key). Set `params.max_tokens` higher for them; a reply
|
|
184
|
+
cut off before its first word, a refusal, or a content-filter block is recorded as that
|
|
185
|
+
case's error rather than as an empty answer.
|
|
186
|
+
* AWS Bedrock and Google Vertex AI sign requests with cloud credentials, which evalproof does
|
|
187
|
+
not implement. Use a gateway's OpenAI-compatible endpoint through `openai-compatible`.
|
|
188
|
+
|
|
125
189
|
## Concepts
|
|
126
190
|
|
|
127
191
|
**Suite** — one YAML file: a JSONL golden set, the system under test, scorers, an optional
|
|
@@ -131,7 +195,7 @@ judge, calibration settings and a gate.
|
|
|
131
195
|
name: support-qa
|
|
132
196
|
dataset: cases.jsonl # {"id", "input", "reference", ...extra fields}
|
|
133
197
|
system:
|
|
134
|
-
target: ollama:qwen3:1.7b # or
|
|
198
|
+
target: ollama:qwen3:1.7b # or groq:…, anthropic:…, python:module:func, https://…
|
|
135
199
|
system_prompt_file: prompt.md
|
|
136
200
|
prompt_template: "{question}"
|
|
137
201
|
params: {temperature: 0, seed: 0, think: false}
|
|
@@ -174,6 +238,56 @@ orders and reports order-dependent verdicts as ties.
|
|
|
174
238
|
| 4 | cannot gate: fewer than `min_cases` paired cases, or the runs used different data |
|
|
175
239
|
| 2 | usage or configuration error |
|
|
176
240
|
|
|
241
|
+
The gate checks one metric by default. To gate several, repeat `--metric` or list them in
|
|
242
|
+
the suite; each metric's p-value is then Holm-adjusted for the number of metrics gated, so
|
|
243
|
+
the chance of failing a build on noise stays at `alpha` for the set as a whole, and the
|
|
244
|
+
report shows the raw and the adjusted p-value side by side. Metrics that are not gated are
|
|
245
|
+
still reported.
|
|
246
|
+
|
|
247
|
+
```yaml
|
|
248
|
+
gate:
|
|
249
|
+
metrics: [judge, answer_key] # one metric: `metric: judge`
|
|
250
|
+
max_drop: 0.03
|
|
251
|
+
alpha: 0.05
|
|
252
|
+
min_cases: 40
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
evalproof compare baseline.json candidate.json --gate --metric judge --metric answer_key
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Import results from other tools
|
|
260
|
+
|
|
261
|
+
You do not have to run your evals with evalproof to gate them. `evalproof import` turns the
|
|
262
|
+
per-case results another tool wrote into a run file that `compare` and the gate read:
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
evalproof import promptfoo output.json --system openai:gpt-5 --out runs/candidate.json
|
|
266
|
+
evalproof import inspect logs/task.eval --out runs/candidate.json
|
|
267
|
+
evalproof import deepeval .deepeval/.latest_test_run.json --out runs/candidate.json
|
|
268
|
+
evalproof import lm-eval results/gpt2/samples_hellaswag_2026-11-12T10-20-30.jsonl --out runs/gpt2.json
|
|
269
|
+
evalproof import csv results.csv --system a --out runs/a.json # id,metric,passed,score,...
|
|
270
|
+
evalproof import jsonl results.jsonl --wide --id-field case --out runs/a.json # one column per metric
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Import each side with the same importer, then gate as usual, for example
|
|
274
|
+
`evalproof compare runs/baseline.json runs/candidate.json --gate --metric pass`. What to know:
|
|
275
|
+
|
|
276
|
+
* Case ids are stable between runs of the same dataset (they come from the test's inputs, or
|
|
277
|
+
the tool's own sample id, never from its position), which is what lets two files pair.
|
|
278
|
+
The dataset hash covers the ids and inputs as the tool recorded them, so runs imported by
|
|
279
|
+
different importers refuse to gate (exit 4) instead of pairing on a coincidence.
|
|
280
|
+
* A system error counts as a failure on every metric, as in a native run. Repeats of one case
|
|
281
|
+
(epochs, repeated tests) collapse by `--epochs`: `mean` (the default), `first`, `all`
|
|
282
|
+
or `any`. Under `mean`, `first` and `any`, a case with at least one repeat that ran is not
|
|
283
|
+
counted as a system error; its errored repeats still score as failures.
|
|
284
|
+
* A NaN or infinite score is refused at import, naming the case and metric, because it would
|
|
285
|
+
make the bootstrap report a significant difference that is not there.
|
|
286
|
+
* promptfoo files can hold several providers and `--system` picks one. Inspect `.eval` logs
|
|
287
|
+
compressed with zstd need Python 3.14 or newer; otherwise run
|
|
288
|
+
`inspect log convert --to json <file>` first. DeepEval's conversational cases are skipped
|
|
289
|
+
with a warning.
|
|
290
|
+
|
|
177
291
|
## From Python
|
|
178
292
|
|
|
179
293
|
The CLI is a thin layer over the same functions:
|
|
@@ -207,7 +321,7 @@ The architecture diagram is in the [README on GitHub](https://github.com/technob
|
|
|
207
321
|
| Treat the judge as a classifier to be calibrated | Trust the judge's pass rate | Its count is biased by its own error rates, and the bias moves with the true rate | [0001](https://github.com/technobasant/evalproof/blob/main/docs/adr/0001-judge-is-a-classifier.md) |
|
|
208
322
|
| Local Ollama by default, HTTP-only providers, content-addressed cache | Hosted APIs and vendor SDKs | Reproducible, free to re-run, tied to a model digest; no SDK dependency | [0002](https://github.com/technobasant/evalproof/blob/main/docs/adr/0002-local-first-providers.md) |
|
|
209
323
|
| Gate on "large AND significant", on paired cases | "Fail if the score dropped by X" | On a few dozen cases, X-point drops are mostly noise, and a noisy gate gets switched off | [0003](https://github.com/technobasant/evalproof/blob/main/docs/adr/0003-gate-on-significance-not-delta.md) |
|
|
210
|
-
|
|
|
324
|
+
| One metric by default; several with Holm correction | Gate every metric at α each | Testing many metrics at α = 0.05 each inflates false failures | 0003 |
|
|
211
325
|
| Pointwise verdicts for gating; pairwise as a diagnostic | Pairwise preference for gating | Measured: 42% of pairwise verdicts flipped with answer order, with a strong length preference | [MEASUREMENTS §6](https://github.com/technobasant/evalproof/blob/main/MEASUREMENTS.md) |
|
|
212
326
|
| numpy and the standard library for statistics | SciPy, statsmodels | Small enough to read and to test against hand-worked values | — |
|
|
213
327
|
|
|
@@ -217,11 +331,12 @@ The composite action runs `compare --gate`, appends the report to the job summar
|
|
|
217
331
|
fails the job on exit codes 1, 3 or 4:
|
|
218
332
|
|
|
219
333
|
```yaml
|
|
220
|
-
- uses: technobasant/evalproof@v0.
|
|
334
|
+
- uses: technobasant/evalproof@v0.3.0
|
|
221
335
|
with:
|
|
222
336
|
baseline: eval/baseline.json # produced on main, e.g. as a build artifact
|
|
223
337
|
candidate: eval/candidate.json # produced on this branch
|
|
224
338
|
suite: eval/suite.yaml
|
|
339
|
+
# metric: "judge, answer_key" # optional: gate several metrics, newline- or comma-separated
|
|
225
340
|
```
|
|
226
341
|
|
|
227
342
|
This repository's own CI runs the offline example on every commit and asserts that the
|
|
@@ -241,11 +356,12 @@ frameworks.
|
|
|
241
356
|
evalproof is deliberately narrow: it is the measurement layer — how much a judge's verdict
|
|
242
357
|
is worth, and whether a difference between two runs is real. Its run file is plain,
|
|
243
358
|
documented JSON, so it can sit beside an existing harness: produce the per-case verdicts
|
|
244
|
-
however you like, then calibrate and gate here.
|
|
359
|
+
however you like, then calibrate and gate here. For promptfoo, Inspect, DeepEval and
|
|
360
|
+
lm-eval you do not even need to do that: evalproof [imports their results](#import-results-from-other-tools).
|
|
245
361
|
|
|
246
362
|
## Limits
|
|
247
363
|
|
|
248
|
-
* The measurements cover one machine, two small local models, one
|
|
364
|
+
* The measurements cover one machine, two small local models, one product domain and
|
|
249
365
|
83 cases. They show the method; they are not a benchmark of those models.
|
|
250
366
|
* Calibration is only as good as its gold labels — [MEASUREMENTS §4](https://github.com/technobasant/evalproof/blob/main/MEASUREMENTS.md)
|
|
251
367
|
shows a regex answer key being wrong more often than the judge. Use human labels when
|
|
@@ -260,9 +376,6 @@ however you like, then calibrate and gate here.
|
|
|
260
376
|
## Roadmap
|
|
261
377
|
|
|
262
378
|
* Stratified gold sampling (by tag) and per-tag calibration.
|
|
263
|
-
* Multiple-comparison correction when more than one metric is gated.
|
|
264
|
-
* A reasoning judge (`think: true`) measured against the same answer key.
|
|
265
|
-
* Importers for other harnesses' per-case results.
|
|
266
379
|
|
|
267
380
|
## Project
|
|
268
381
|
|
|
@@ -31,8 +31,9 @@ evalproof:
|
|
|
31
31
|
4. **gates CI** only when a drop is both larger than your tolerance *and* statistically
|
|
32
32
|
significant, plus latency and cost budgets, with distinct exit codes.
|
|
33
33
|
|
|
34
|
-
It is local-first: Ollama is the default
|
|
35
|
-
every model call is cached by content so re-runs are free
|
|
34
|
+
It is local-first: Ollama is the default, every major hosted provider is one `target:` line
|
|
35
|
+
away ([Providers](#providers)), and every model call is cached by content so re-runs are free
|
|
36
|
+
and reproducible.
|
|
36
37
|
|
|
37
38
|
## Install
|
|
38
39
|
|
|
@@ -42,12 +43,12 @@ uv tool install evalproof # just the CLI
|
|
|
42
43
|
```
|
|
43
44
|
|
|
44
45
|
Python 3.11 to 3.14. `compare`, `calibrate` and `report` work on run files and need no
|
|
45
|
-
model. `run` needs a system to evaluate: a Python function, an HTTP endpoint, a local
|
|
46
|
-
Ollama
|
|
46
|
+
model. `run` needs a system to evaluate: a Python function, an HTTP endpoint, a local model
|
|
47
|
+
(Ollama, vLLM, LM Studio, llama.cpp), or a key for a hosted provider.
|
|
47
48
|
|
|
48
49
|
## What it found on a real run
|
|
49
50
|
|
|
50
|
-
A local `qwen3:1.7b` support assistant
|
|
51
|
+
A local `qwen3:1.7b` product support assistant, judged by `qwen3:8b`, 83
|
|
51
52
|
questions, one Apple M5. Full method and every table in [MEASUREMENTS.md](MEASUREMENTS.md).
|
|
52
53
|
|
|
53
54
|
| Question | Measured |
|
|
@@ -58,7 +59,8 @@ questions, one Apple M5. Full method and every table in [MEASUREMENTS.md](MEASUR
|
|
|
58
59
|
| Does correcting the judge's rate help? | On average yes (offset 4.8 → ~3.8 pts), but with 30 labels the interval doubles; at 83 cases the judge's bias is smaller than its own noise |
|
|
59
60
|
| Was the gold trustworthy? | No: of 13 judge–key disagreements the key was wrong 7 times, the judge 4. Most of the "bias" corrected was the gold's |
|
|
60
61
|
| Is pairwise judging safer? | No: 42% of verdicts flipped with the order of the two answers, and the judge preferred the candidate 42 to 6 — mostly where both answers were right and the candidate's was five times longer |
|
|
61
|
-
|
|
|
62
|
+
| Does a reasoning judge help? | Against audited labels, yes: thinking on passed no wrong answer (three without it) and κ rose from 0.86–0.88 to 0.90–0.94, at 6.7× the judge time. Against the raw answer key it looked worse, because the key is wrong on exactly those cases. The gate's decision did not change |
|
|
63
|
+
| Is it reproducible? | 20/20 identical outputs and verdicts with the cache off; a warm-cache re-run took 0.05 s instead of 363 s; a re-judge three days later matched 83/83 |
|
|
62
64
|
|
|
63
65
|
## Five-minute quickstart
|
|
64
66
|
|
|
@@ -79,6 +81,11 @@ uv run evalproof compare runs/baseline.json runs/noisy.json \
|
|
|
79
81
|
--suite examples/deterministic/suite.yaml --gate; echo "exit $?" # 0: 2 of 60 is noise
|
|
80
82
|
```
|
|
81
83
|
|
|
84
|
+
[`examples/retrieval`](examples/retrieval) runs the same gate on a retrieval change: recall@5
|
|
85
|
+
does not move when the top hit drops to rank 5, while MRR and nDCG@5 do, and the gate fails
|
|
86
|
+
it. It was contributed by [@HarshRajSinghania](https://github.com/HarshRajSinghania), and CI
|
|
87
|
+
checks its exit codes too.
|
|
88
|
+
|
|
82
89
|
With Ollama installed, the LLM example is
|
|
83
90
|
[`examples/support-qa`](examples/support-qa) (`ollama pull qwen3:1.7b` and
|
|
84
91
|
`ollama pull qwen3:8b`; about 6–10 minutes per run on an Apple M5).
|
|
@@ -86,6 +93,63 @@ With Ollama installed, the LLM example is
|
|
|
86
93
|
The browser app in [`app/`](app) runs locally too:
|
|
87
94
|
`uv run --with-requirements app/requirements.txt streamlit run app/streamlit_app.py`.
|
|
88
95
|
|
|
96
|
+
## Providers
|
|
97
|
+
|
|
98
|
+
The target is `<provider>:<model>`; the model name is everything after the first colon, so
|
|
99
|
+
`openrouter:anthropic/claude-sonnet-4.5` and `ollama:qwen3:8b` both work. Two native adapters
|
|
100
|
+
(`ollama`, `anthropic`) and one OpenAI-compatible adapter driven by a table of provider
|
|
101
|
+
profiles ([`providers/profiles.py`](src/evalproof/providers/profiles.py), each row cites the
|
|
102
|
+
provider's documentation). Plain HTTP throughout, no vendor SDK.
|
|
103
|
+
|
|
104
|
+
| Provider | Example target | Key variable | Where |
|
|
105
|
+
|---|---|---|---|
|
|
106
|
+
| `ollama` | `ollama:qwen3:8b` | none | local |
|
|
107
|
+
| `vllm` | `vllm:meta-llama/Llama-3.1-8B-Instruct` | `VLLM_API_KEY`, optional | local |
|
|
108
|
+
| `lmstudio` | `lmstudio:qwen3-8b` | optional, via `api_key_env` | local |
|
|
109
|
+
| `llamacpp` | `llamacpp:qwen3-8b` | `LLAMA_API_KEY`, optional | local |
|
|
110
|
+
| `anthropic` | `anthropic:claude-sonnet-4-5` | `ANTHROPIC_API_KEY` | hosted |
|
|
111
|
+
| `openai` | `openai:gpt-5` | `OPENAI_API_KEY` | hosted |
|
|
112
|
+
| `azure` | `azure:<deployment>` | `AZURE_OPENAI_API_KEY` | hosted, needs `base_url` |
|
|
113
|
+
| `gemini` | `gemini:gemini-2.5-flash` | `GEMINI_API_KEY` | hosted |
|
|
114
|
+
| `mistral` | `mistral:mistral-large-latest` | `MISTRAL_API_KEY` | hosted |
|
|
115
|
+
| `groq` | `groq:llama-3.3-70b-versatile` | `GROQ_API_KEY` | hosted |
|
|
116
|
+
| `together` | `together:meta-llama/Llama-3.3-70B-Instruct-Turbo` | `TOGETHER_API_KEY` | hosted |
|
|
117
|
+
| `fireworks` | `fireworks:accounts/fireworks/models/llama-v3p3-70b-instruct` | `FIREWORKS_API_KEY` | hosted |
|
|
118
|
+
| `deepseek` | `deepseek:deepseek-chat` | `DEEPSEEK_API_KEY` | hosted |
|
|
119
|
+
| `xai` | `xai:grok-4` | `XAI_API_KEY` | hosted |
|
|
120
|
+
| `openrouter` | `openrouter:anthropic/claude-sonnet-4.5` | `OPENROUTER_API_KEY` | hosted |
|
|
121
|
+
| `cerebras` | `cerebras:gpt-oss-120b` | `CEREBRAS_API_KEY` | hosted |
|
|
122
|
+
| `openai-compatible` | `openai-compatible:<model>` | optional, via `api_key_env` | any server, needs `base_url` |
|
|
123
|
+
|
|
124
|
+
```yaml
|
|
125
|
+
system:
|
|
126
|
+
target: openai-compatible:gpt-4o # a gateway, LiteLLM proxy, TGI, SGLang, ...
|
|
127
|
+
base_url: https://llm-gateway.internal/v1
|
|
128
|
+
api_key_env: TEAM_GATEWAY_KEY # the variable to read; the key itself never goes in the suite
|
|
129
|
+
headers: {x-team: search} # extra HTTP headers on every call, never secrets
|
|
130
|
+
params: {max_tokens: 2048}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
* `base_url` overrides any profile's default (a regional endpoint, a proxy, an older
|
|
134
|
+
`llama-server`, which listened on port 8080 before October 2026 and now defaults to 9931).
|
|
135
|
+
* `api_key_env` replaces the profile's variable for any provider; a variable named there
|
|
136
|
+
must be set.
|
|
137
|
+
* `headers` merge into every request and win over the adapter's own, for example
|
|
138
|
+
`anthropic-version`. They are not part of the cache key, and a suite file is usually
|
|
139
|
+
committed, so keep keys in the environment.
|
|
140
|
+
* Sampling: local servers (`ollama`, `vllm`, `lmstudio`, `llamacpp`) receive `temperature` and
|
|
141
|
+
`seed` on every call, defaults included, so a local run repeats. Hosted providers receive
|
|
142
|
+
them only when `params` sets them, because reasoning models reject a non-default
|
|
143
|
+
temperature; Anthropic never receives a seed, and since its current models also reject
|
|
144
|
+
`temperature`, it gets one only when the suite asks. `params.extra` adds any other body
|
|
145
|
+
field, such as `{thinking: {type: adaptive}, output_config: {effort: low}}`.
|
|
146
|
+
* Thinking models spend output tokens on thinking inside `max_tokens`, which defaults to 512
|
|
147
|
+
(changing it would change every cache key). Set `params.max_tokens` higher for them; a reply
|
|
148
|
+
cut off before its first word, a refusal, or a content-filter block is recorded as that
|
|
149
|
+
case's error rather than as an empty answer.
|
|
150
|
+
* AWS Bedrock and Google Vertex AI sign requests with cloud credentials, which evalproof does
|
|
151
|
+
not implement. Use a gateway's OpenAI-compatible endpoint through `openai-compatible`.
|
|
152
|
+
|
|
89
153
|
## Concepts
|
|
90
154
|
|
|
91
155
|
**Suite** — one YAML file: a JSONL golden set, the system under test, scorers, an optional
|
|
@@ -95,7 +159,7 @@ judge, calibration settings and a gate.
|
|
|
95
159
|
name: support-qa
|
|
96
160
|
dataset: cases.jsonl # {"id", "input", "reference", ...extra fields}
|
|
97
161
|
system:
|
|
98
|
-
target: ollama:qwen3:1.7b # or
|
|
162
|
+
target: ollama:qwen3:1.7b # or groq:…, anthropic:…, python:module:func, https://…
|
|
99
163
|
system_prompt_file: prompt.md
|
|
100
164
|
prompt_template: "{question}"
|
|
101
165
|
params: {temperature: 0, seed: 0, think: false}
|
|
@@ -138,6 +202,56 @@ orders and reports order-dependent verdicts as ties.
|
|
|
138
202
|
| 4 | cannot gate: fewer than `min_cases` paired cases, or the runs used different data |
|
|
139
203
|
| 2 | usage or configuration error |
|
|
140
204
|
|
|
205
|
+
The gate checks one metric by default. To gate several, repeat `--metric` or list them in
|
|
206
|
+
the suite; each metric's p-value is then Holm-adjusted for the number of metrics gated, so
|
|
207
|
+
the chance of failing a build on noise stays at `alpha` for the set as a whole, and the
|
|
208
|
+
report shows the raw and the adjusted p-value side by side. Metrics that are not gated are
|
|
209
|
+
still reported.
|
|
210
|
+
|
|
211
|
+
```yaml
|
|
212
|
+
gate:
|
|
213
|
+
metrics: [judge, answer_key] # one metric: `metric: judge`
|
|
214
|
+
max_drop: 0.03
|
|
215
|
+
alpha: 0.05
|
|
216
|
+
min_cases: 40
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
evalproof compare baseline.json candidate.json --gate --metric judge --metric answer_key
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Import results from other tools
|
|
224
|
+
|
|
225
|
+
You do not have to run your evals with evalproof to gate them. `evalproof import` turns the
|
|
226
|
+
per-case results another tool wrote into a run file that `compare` and the gate read:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
evalproof import promptfoo output.json --system openai:gpt-5 --out runs/candidate.json
|
|
230
|
+
evalproof import inspect logs/task.eval --out runs/candidate.json
|
|
231
|
+
evalproof import deepeval .deepeval/.latest_test_run.json --out runs/candidate.json
|
|
232
|
+
evalproof import lm-eval results/gpt2/samples_hellaswag_2026-11-12T10-20-30.jsonl --out runs/gpt2.json
|
|
233
|
+
evalproof import csv results.csv --system a --out runs/a.json # id,metric,passed,score,...
|
|
234
|
+
evalproof import jsonl results.jsonl --wide --id-field case --out runs/a.json # one column per metric
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Import each side with the same importer, then gate as usual, for example
|
|
238
|
+
`evalproof compare runs/baseline.json runs/candidate.json --gate --metric pass`. What to know:
|
|
239
|
+
|
|
240
|
+
* Case ids are stable between runs of the same dataset (they come from the test's inputs, or
|
|
241
|
+
the tool's own sample id, never from its position), which is what lets two files pair.
|
|
242
|
+
The dataset hash covers the ids and inputs as the tool recorded them, so runs imported by
|
|
243
|
+
different importers refuse to gate (exit 4) instead of pairing on a coincidence.
|
|
244
|
+
* A system error counts as a failure on every metric, as in a native run. Repeats of one case
|
|
245
|
+
(epochs, repeated tests) collapse by `--epochs`: `mean` (the default), `first`, `all`
|
|
246
|
+
or `any`. Under `mean`, `first` and `any`, a case with at least one repeat that ran is not
|
|
247
|
+
counted as a system error; its errored repeats still score as failures.
|
|
248
|
+
* A NaN or infinite score is refused at import, naming the case and metric, because it would
|
|
249
|
+
make the bootstrap report a significant difference that is not there.
|
|
250
|
+
* promptfoo files can hold several providers and `--system` picks one. Inspect `.eval` logs
|
|
251
|
+
compressed with zstd need Python 3.14 or newer; otherwise run
|
|
252
|
+
`inspect log convert --to json <file>` first. DeepEval's conversational cases are skipped
|
|
253
|
+
with a warning.
|
|
254
|
+
|
|
141
255
|
## From Python
|
|
142
256
|
|
|
143
257
|
The CLI is a thin layer over the same functions:
|
|
@@ -167,7 +281,7 @@ flowchart LR
|
|
|
167
281
|
suite[suite.yaml] --> runner
|
|
168
282
|
data[(cases.jsonl)] --> runner
|
|
169
283
|
subgraph runner[evalproof run]
|
|
170
|
-
sys[system under test<br/>ollama · anthropic · openai<br/>python · http] --> scorers
|
|
284
|
+
sys[system under test<br/>ollama · anthropic · openai-compatible<br/>python · http] --> scorers
|
|
171
285
|
scorers[scorers<br/>deterministic · retrieval · llm_judge]
|
|
172
286
|
cache[(content-addressed<br/>cache)]
|
|
173
287
|
sys <--> cache
|
|
@@ -189,7 +303,7 @@ flowchart LR
|
|
|
189
303
|
| Treat the judge as a classifier to be calibrated | Trust the judge's pass rate | Its count is biased by its own error rates, and the bias moves with the true rate | [0001](docs/adr/0001-judge-is-a-classifier.md) |
|
|
190
304
|
| Local Ollama by default, HTTP-only providers, content-addressed cache | Hosted APIs and vendor SDKs | Reproducible, free to re-run, tied to a model digest; no SDK dependency | [0002](docs/adr/0002-local-first-providers.md) |
|
|
191
305
|
| Gate on "large AND significant", on paired cases | "Fail if the score dropped by X" | On a few dozen cases, X-point drops are mostly noise, and a noisy gate gets switched off | [0003](docs/adr/0003-gate-on-significance-not-delta.md) |
|
|
192
|
-
|
|
|
306
|
+
| One metric by default; several with Holm correction | Gate every metric at α each | Testing many metrics at α = 0.05 each inflates false failures | 0003 |
|
|
193
307
|
| Pointwise verdicts for gating; pairwise as a diagnostic | Pairwise preference for gating | Measured: 42% of pairwise verdicts flipped with answer order, with a strong length preference | [MEASUREMENTS §6](MEASUREMENTS.md) |
|
|
194
308
|
| numpy and the standard library for statistics | SciPy, statsmodels | Small enough to read and to test against hand-worked values | — |
|
|
195
309
|
|
|
@@ -199,11 +313,12 @@ The composite action runs `compare --gate`, appends the report to the job summar
|
|
|
199
313
|
fails the job on exit codes 1, 3 or 4:
|
|
200
314
|
|
|
201
315
|
```yaml
|
|
202
|
-
- uses: technobasant/evalproof@v0.
|
|
316
|
+
- uses: technobasant/evalproof@v0.3.0
|
|
203
317
|
with:
|
|
204
318
|
baseline: eval/baseline.json # produced on main, e.g. as a build artifact
|
|
205
319
|
candidate: eval/candidate.json # produced on this branch
|
|
206
320
|
suite: eval/suite.yaml
|
|
321
|
+
# metric: "judge, answer_key" # optional: gate several metrics, newline- or comma-separated
|
|
207
322
|
```
|
|
208
323
|
|
|
209
324
|
This repository's own CI runs the offline example on every commit and asserts that the
|
|
@@ -223,11 +338,12 @@ frameworks.
|
|
|
223
338
|
evalproof is deliberately narrow: it is the measurement layer — how much a judge's verdict
|
|
224
339
|
is worth, and whether a difference between two runs is real. Its run file is plain,
|
|
225
340
|
documented JSON, so it can sit beside an existing harness: produce the per-case verdicts
|
|
226
|
-
however you like, then calibrate and gate here.
|
|
341
|
+
however you like, then calibrate and gate here. For promptfoo, Inspect, DeepEval and
|
|
342
|
+
lm-eval you do not even need to do that: evalproof [imports their results](#import-results-from-other-tools).
|
|
227
343
|
|
|
228
344
|
## Limits
|
|
229
345
|
|
|
230
|
-
* The measurements cover one machine, two small local models, one
|
|
346
|
+
* The measurements cover one machine, two small local models, one product domain and
|
|
231
347
|
83 cases. They show the method; they are not a benchmark of those models.
|
|
232
348
|
* Calibration is only as good as its gold labels — [MEASUREMENTS §4](MEASUREMENTS.md)
|
|
233
349
|
shows a regex answer key being wrong more often than the judge. Use human labels when
|
|
@@ -242,9 +358,6 @@ however you like, then calibrate and gate here.
|
|
|
242
358
|
## Roadmap
|
|
243
359
|
|
|
244
360
|
* Stratified gold sampling (by tag) and per-tag calibration.
|
|
245
|
-
* Multiple-comparison correction when more than one metric is gated.
|
|
246
|
-
* A reasoning judge (`think: true`) measured against the same answer key.
|
|
247
|
-
* Importers for other harnesses' per-case results.
|
|
248
361
|
|
|
249
362
|
## Project
|
|
250
363
|
|
|
@@ -23,10 +23,11 @@ from evalproof.gate import (
|
|
|
23
23
|
GateDecision,
|
|
24
24
|
decide,
|
|
25
25
|
)
|
|
26
|
+
from evalproof.importers import ImportFormatError, import_run
|
|
26
27
|
from evalproof.models import Case, CaseResult, Run, RunInfo, Score, SystemOutput
|
|
27
28
|
from evalproof.report import calibration_markdown, comparison_markdown, run_markdown
|
|
28
29
|
from evalproof.runner import load_run, run_suite, save_run
|
|
29
|
-
from evalproof.stats import mcnemar_exact, paired_bootstrap, wilson
|
|
30
|
+
from evalproof.stats import holm, mcnemar_exact, paired_bootstrap, wilson
|
|
30
31
|
|
|
31
32
|
__all__ = [
|
|
32
33
|
"EXIT_BUDGET",
|
|
@@ -44,6 +45,7 @@ __all__ = [
|
|
|
44
45
|
"Estimate",
|
|
45
46
|
"GateConfig",
|
|
46
47
|
"GateDecision",
|
|
48
|
+
"ImportFormatError",
|
|
47
49
|
"MetricComparison",
|
|
48
50
|
"Run",
|
|
49
51
|
"RunInfo",
|
|
@@ -57,6 +59,8 @@ __all__ = [
|
|
|
57
59
|
"compare_runs",
|
|
58
60
|
"comparison_markdown",
|
|
59
61
|
"decide",
|
|
62
|
+
"holm",
|
|
63
|
+
"import_run",
|
|
60
64
|
"load_config",
|
|
61
65
|
"load_dataset",
|
|
62
66
|
"load_run",
|