evalproof 0.3.0__tar.gz → 0.4.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.3.0 → evalproof-0.4.0}/CHANGELOG.md +67 -1
- {evalproof-0.3.0 → evalproof-0.4.0}/PKG-INFO +169 -5
- {evalproof-0.3.0 → evalproof-0.4.0}/README.md +168 -4
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/_version.py +1 -1
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/cli.py +59 -1
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/compare.py +29 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/config.py +3 -0
- evalproof-0.4.0/src/evalproof/doctor.py +130 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/gate.py +20 -6
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/providers/__init__.py +40 -2
- evalproof-0.4.0/src/evalproof/providers/agent_cli.py +617 -0
- evalproof-0.4.0/src/evalproof/providers/agent_cli_experimental.py +452 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/providers/base.py +8 -2
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/report.py +11 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/runner.py +73 -6
- evalproof-0.4.0/src/evalproof/scaffold.py +369 -0
- evalproof-0.4.0/src/evalproof/templates/SKILL.md +86 -0
- evalproof-0.4.0/src/evalproof/templates/cases.jsonl +3 -0
- evalproof-0.4.0/src/evalproof/templates/evals-readme.md +11 -0
- evalproof-0.4.0/src/evalproof/templates/gitignore +3 -0
- evalproof-0.4.0/src/evalproof/templates/suite.yaml +70 -0
- evalproof-0.4.0/src/evalproof/templates/workflow-api-key.yml +74 -0
- evalproof-0.4.0/src/evalproof/templates/workflow-keyless.yml +67 -0
- evalproof-0.4.0/tests/fakecli.py +260 -0
- evalproof-0.4.0/tests/fakecli_experimental.py +207 -0
- evalproof-0.4.0/tests/test_agent_cli.py +810 -0
- evalproof-0.4.0/tests/test_agent_cli_experimental.py +869 -0
- evalproof-0.4.0/tests/test_doctor.py +251 -0
- evalproof-0.4.0/tests/test_init.py +699 -0
- evalproof-0.4.0/tests/test_judge_fingerprint.py +272 -0
- evalproof-0.4.0/tests/test_live_agent_cli.py +47 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/.gitignore +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/CITATION.cff +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/LICENSE +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/NOTICE +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/pyproject.toml +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/__init__.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/cache.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/calibration.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/dataset.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/importers/__init__.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/importers/_common.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/importers/deepeval.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/importers/inspect_ai.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/importers/lm_eval.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/importers/promptfoo.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/importers/table.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/models.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/providers/anthropic.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/providers/ollama.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/providers/openai.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/providers/profiles.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/py.typed +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/scorers/__init__.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/scorers/deterministic.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/scorers/judge.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/scorers/retrieval.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/stats.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/src/evalproof/systems.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/tests/__init__.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/tests/conftest.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/tests/fixtures/README.md +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/tests/fixtures/deepeval/test_run.json +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/tests/fixtures/inspect/log_formats.json +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/tests/fixtures/lm-eval/gpt2/samples_hellaswag_2024-05-01T10-20-30.123456.jsonl +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/tests/fixtures/lm-eval/llama3/samples_gsm8k_2024-05-01T10-20-30.123456.jsonl +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/tests/fixtures/promptfoo/sample-export.json +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/tests/fixtures/table/long.csv +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/tests/fixtures/table/long.jsonl +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/tests/fixtures/table/wide.csv +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/tests/test_action.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/tests/test_cache_systems.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/tests/test_calibration.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/tests/test_compare_gate_report.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/tests/test_config_dataset.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/tests/test_holm_gate.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/tests/test_importers.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/tests/test_providers.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/tests/test_public_api.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/tests/test_release_fixes.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/tests/test_runner_cli.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/tests/test_scorers.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/tests/test_security.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.0}/tests/test_stats.py +0 -0
|
@@ -4,6 +4,71 @@ All notable changes are listed here. The project follows [Semantic Versioning](h
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.4.0] — 2026-11-13
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `claude-code:<model>` and `codex:<model>` providers: run your own logged-in Claude Code or
|
|
12
|
+
Codex CLI as a judge or a system under test, with no API key. Each call runs in a fresh
|
|
13
|
+
empty directory with the agent's tools, settings, MCP servers, skills and memory switched
|
|
14
|
+
off and no permission-bypass flag; API-key variables are removed from the child's
|
|
15
|
+
environment unless `api_key_env` names one or `EVALPROOF_CLI_AUTH=api-key` keeps them.
|
|
16
|
+
Neither CLI takes a temperature, seed or token limit, so those params are ignored (Codex
|
|
17
|
+
maps `think` to its reasoning effort). Claude Code runs with no tools; Codex still offers a
|
|
18
|
+
JavaScript isolate, sub-agents and a question to the user, so any item in its output other
|
|
19
|
+
than a message, reasoning, an error or a plan fails the call as an isolation break, and a
|
|
20
|
+
codex too old for the flags evalproof sets gets a message to upgrade. ADR 0004 has the
|
|
21
|
+
reasoning.
|
|
22
|
+
- `EVALPROOF_CLI_AUTH` (`login`, the default, or `api-key`) chooses whether a CLI provider
|
|
23
|
+
uses the subscription login or its own API key variable; `evalproof doctor` prints the mode
|
|
24
|
+
in effect and warns when `api-key` has no key to use. `api_key_env` still wins.
|
|
25
|
+
- `evalproof doctor [suite.yaml]`: reports each CLI's path, version, login mode and the
|
|
26
|
+
isolation flags it will use, without a model call, and exits 1 when a target of the suite
|
|
27
|
+
is not usable. Without a suite it surveys the installed CLIs and exits 1 only when none is
|
|
28
|
+
usable. It prints no account details.
|
|
29
|
+
- Run facts for CLI targets in `runner.environment`: `{judge,system}_cli`,
|
|
30
|
+
`{judge,system}_cli_version`, `{judge,system}_isolation` and `{judge,system}_model_resolved`
|
|
31
|
+
(the model ids that answered, sorted, cached replies included). The response cache keys on
|
|
32
|
+
the CLI fingerprint, so a CLI upgrade is a cache miss.
|
|
33
|
+
- `compare` reports `same_judge`, `judge_changes` and `system_changes`, and the gate refuses
|
|
34
|
+
to decide (exit 4) when the judge changed between the runs. `--allow-judge-change` or
|
|
35
|
+
`gate.allow_judge_change: true` accepts the change and keeps a warning.
|
|
36
|
+
- Experimental `opencode:<provider>/<model>`, `cursor:<model>` and `copilot:<model>`
|
|
37
|
+
providers, on the same footing as the CLI providers above (fresh empty directory, the CLI's
|
|
38
|
+
key variable removed unless `api_key_env` names one, bounded runtime, CLI fingerprint with the
|
|
39
|
+
isolation tags `opencode/0`, `cursor/0`, `copilot/0`). Each prints one warning on stderr the
|
|
40
|
+
first time it is used. Only OpenCode was exercised for real, and only against a local ollama
|
|
41
|
+
model; Cursor (not logged in) and Copilot (not installed) follow their documentation, and the
|
|
42
|
+
README's "Experimental providers" says what was and was not verified. `evalproof doctor`
|
|
43
|
+
without a suite now also lists whichever of these CLIs are on `PATH`.
|
|
44
|
+
- `evalproof.providers.TransientError`, a `ProviderError` that `with_retries` retries; the CLI
|
|
45
|
+
providers raise it for rate limits and server errors.
|
|
46
|
+
- `tests/test_live_agent_cli.py`: one real call per installed CLI, skipped unless
|
|
47
|
+
`EVALPROOF_LIVE=1`.
|
|
48
|
+
- `evalproof init [--dir PATH] [--judge TARGET] [--ci keyless|api-key|none] [--no-skill]
|
|
49
|
+
[--force]`: writes `evals/suite.yaml`, three sample cases in `evals/cases.jsonl`, an
|
|
50
|
+
`evals/README.md`, a GitHub Actions workflow at `.github/workflows/evalproof.yml` and an
|
|
51
|
+
agent skill at `.agents/skills/evalproof/SKILL.md` and `.claude/skills/evalproof/SKILL.md`.
|
|
52
|
+
The judge is the first of `claude`, `codex`, `ollama` found on `PATH`, else a hosted
|
|
53
|
+
Anthropic model. The keyless workflow makes no model calls and gates the committed
|
|
54
|
+
`evals/runs/latest.json` against the base branch; the api-key workflow runs the suite in CI
|
|
55
|
+
(with `EVALPROOF_CLI_AUTH: api-key` for a CLI judge, so no second suite file is written)
|
|
56
|
+
and pins the judge CLI to the version found at init time. Existing files are kept unless
|
|
57
|
+
`--force` is passed, nothing is written outside `--dir`, and no model is called.
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
|
|
61
|
+
- A gate that passed in 0.3.0 can now exit 4 when the judge's target, CLI version, isolation
|
|
62
|
+
or resolved model differs between two runs that both carry these keys. Runs written before
|
|
63
|
+
this release carry none and compare as before.
|
|
64
|
+
- A changed system target or CLI version between two runs now adds a warning, so the status
|
|
65
|
+
is `warn` instead of `pass` (the exit code stays 0).
|
|
66
|
+
- `runner.environment` records `judge_target` and `system_target` for every model target (not
|
|
67
|
+
only CLI ones), and the system target it records is the one in effect, with any override
|
|
68
|
+
applied.
|
|
69
|
+
- The gate's "too few paired cases" reason now says how to fix it (add cases, or lower
|
|
70
|
+
`gate.min_cases`).
|
|
71
|
+
|
|
7
72
|
## [0.3.0] — 2026-11-12
|
|
8
73
|
|
|
9
74
|
### Added
|
|
@@ -179,7 +244,8 @@ First release.
|
|
|
179
244
|
- Two examples: an offline ticket router (CI's gate contract) and a product support
|
|
180
245
|
assistant judged by a local model, with measurements.
|
|
181
246
|
|
|
182
|
-
[Unreleased]: https://github.com/technobasant/evalproof/compare/v0.
|
|
247
|
+
[Unreleased]: https://github.com/technobasant/evalproof/compare/v0.4.0...HEAD
|
|
248
|
+
[0.4.0]: https://github.com/technobasant/evalproof/compare/v0.3.0...v0.4.0
|
|
183
249
|
[0.3.0]: https://github.com/technobasant/evalproof/compare/v0.2.1...v0.3.0
|
|
184
250
|
[0.2.1]: https://github.com/technobasant/evalproof/compare/v0.2.0...v0.2.1
|
|
185
251
|
[0.2.0]: https://github.com/technobasant/evalproof/compare/v0.1.2...v0.2.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: evalproof
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -100,6 +100,23 @@ questions, one Apple M5. Full method and every table in [MEASUREMENTS.md](https:
|
|
|
100
100
|
|
|
101
101
|
## Five-minute quickstart
|
|
102
102
|
|
|
103
|
+
In your own repository, one command writes a starter suite, a CI workflow and an agent skill,
|
|
104
|
+
and a second runs it:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
uvx evalproof init
|
|
108
|
+
uvx evalproof run evals/suite.yaml --out evals/runs/latest.json
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`init` picks the judge from what is on your `PATH`: a logged-in `claude`, then `codex`, then
|
|
112
|
+
Ollama, then a hosted Anthropic model with `ANTHROPIC_API_KEY`. It writes `evals/` (the
|
|
113
|
+
suite, three sample cases, a README), `.github/workflows/evalproof.yml` (`--ci keyless`, the
|
|
114
|
+
default, makes no model calls in CI; `--ci api-key` runs the suite there; `--ci none` writes
|
|
115
|
+
no workflow) and a skill for coding agents at `.agents/skills/evalproof/SKILL.md` and
|
|
116
|
+
`.claude/skills/evalproof/SKILL.md` (`--no-skill` skips it). It prints one line per file,
|
|
117
|
+
never overwrites one without `--force`, and makes no model call. Replace the sample cases
|
|
118
|
+
with your own: the gate needs 20 before it will decide.
|
|
119
|
+
|
|
103
120
|
The offline example lives in the repository:
|
|
104
121
|
|
|
105
122
|
```bash
|
|
@@ -135,7 +152,11 @@ The target is `<provider>:<model>`; the model name is everything after the first
|
|
|
135
152
|
`openrouter:anthropic/claude-sonnet-4.5` and `ollama:qwen3:8b` both work. Two native adapters
|
|
136
153
|
(`ollama`, `anthropic`) and one OpenAI-compatible adapter driven by a table of provider
|
|
137
154
|
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.
|
|
155
|
+
provider's documentation). Plain HTTP throughout, no vendor SDK. Two more targets run a coding
|
|
156
|
+
agent you already have logged in, `claude-code` and `codex`: see
|
|
157
|
+
[Use your Claude Code or Codex login](#use-your-claude-code-or-codex-login). Three experimental
|
|
158
|
+
ones run OpenCode, Cursor and GitHub Copilot the same way: see
|
|
159
|
+
[Experimental providers](#experimental-providers).
|
|
139
160
|
|
|
140
161
|
| Provider | Example target | Key variable | Where |
|
|
141
162
|
|---|---|---|---|
|
|
@@ -156,6 +177,11 @@ provider's documentation). Plain HTTP throughout, no vendor SDK.
|
|
|
156
177
|
| `openrouter` | `openrouter:anthropic/claude-sonnet-4.5` | `OPENROUTER_API_KEY` | hosted |
|
|
157
178
|
| `cerebras` | `cerebras:gpt-oss-120b` | `CEREBRAS_API_KEY` | hosted |
|
|
158
179
|
| `openai-compatible` | `openai-compatible:<model>` | optional, via `api_key_env` | any server, needs `base_url` |
|
|
180
|
+
| `claude-code` | `claude-code:haiku` | none: your Claude Code login | local CLI, see below |
|
|
181
|
+
| `codex` | `codex:gpt-6-luna` | none: your ChatGPT login | local CLI, see below |
|
|
182
|
+
| `opencode` | `opencode:ollama/qwen3:8b` | none: OpenCode's own login, per model provider | local CLI, experimental |
|
|
183
|
+
| `cursor` | `cursor:auto` | none: your Cursor login | local CLI, experimental |
|
|
184
|
+
| `copilot` | `copilot:gpt-5` | none: your GitHub Copilot login | local CLI, experimental |
|
|
159
185
|
|
|
160
186
|
```yaml
|
|
161
187
|
system:
|
|
@@ -186,6 +212,134 @@ system:
|
|
|
186
212
|
* AWS Bedrock and Google Vertex AI sign requests with cloud credentials, which evalproof does
|
|
187
213
|
not implement. Use a gateway's OpenAI-compatible endpoint through `openai-compatible`.
|
|
188
214
|
|
|
215
|
+
## Use your Claude Code or Codex login
|
|
216
|
+
|
|
217
|
+
If Claude Code or Codex is already logged in on your machine, either can be the judge or the
|
|
218
|
+
system under test, with no API key:
|
|
219
|
+
|
|
220
|
+
```yaml
|
|
221
|
+
judge:
|
|
222
|
+
target: claude-code:haiku # an alias (haiku, sonnet, opus) or a full model id
|
|
223
|
+
system:
|
|
224
|
+
target: codex:gpt-6-luna
|
|
225
|
+
concurrency: 4
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Run `evalproof doctor suite.yaml` first. It prints each CLI's path, version, login mode and
|
|
229
|
+
the isolation flags it will pass, and exits 1 if a target of the suite is not usable. Without a
|
|
230
|
+
suite it surveys every CLI it knows that is installed, and exits 1 only when none is usable. It
|
|
231
|
+
makes no model call.
|
|
232
|
+
|
|
233
|
+
evalproof runs your own `claude` or `codex` binary as a subprocess, sends the prompt on stdin
|
|
234
|
+
and reads the JSON it prints. It never reads a token file or the keychain. The environment
|
|
235
|
+
variable `EVALPROOF_CLI_AUTH` says which credentials the CLI uses:
|
|
236
|
+
|
|
237
|
+
* `login` (the default, also when it is unset or empty) removes the CLI's key variables from
|
|
238
|
+
the call (`ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN` for Claude Code, `OPENAI_API_KEY`
|
|
239
|
+
and `CODEX_API_KEY` for Codex), so the login is what gets used.
|
|
240
|
+
* `api-key` leaves them in place, so the CLI uses its own key. This is the route for CI.
|
|
241
|
+
* Any other value is an error.
|
|
242
|
+
|
|
243
|
+
`api_key_env` in the suite works in either mode and wins: it passes that variable to the CLI as
|
|
244
|
+
its own key variable. The mode is not part of the run's fingerprint or the cache key, because
|
|
245
|
+
it is the same model either way. `evalproof doctor` prints the mode in effect and warns when
|
|
246
|
+
`api-key` is chosen but the CLI's key variable is not set.
|
|
247
|
+
|
|
248
|
+
* **Isolation is a requirement, not an option.** A coding agent is not a model endpoint. Left
|
|
249
|
+
alone it loads the project's `CLAUDE.md` or `AGENTS.md`, your memory, plugins, MCP servers
|
|
250
|
+
and skills into the prompt, and it can run commands. A judge that has read your project's
|
|
251
|
+
instructions is not a judge. So every call runs in a fresh empty directory, with the
|
|
252
|
+
agent's own system prompt replaced by yours (or one neutral line), its tools, settings, MCP
|
|
253
|
+
servers and skills switched off, and no permission-bypass flag. `evalproof doctor` prints
|
|
254
|
+
the exact flags. The two CLIs do not isolate equally well:
|
|
255
|
+
* Claude Code is run with **no tools at all** (`--tools ''`).
|
|
256
|
+
* Codex runs in a read-only sandbox with its shell, plugins, apps, hooks, multi-agent mode,
|
|
257
|
+
sleep and image tools, bundled skills and your user config and rules switched off, but
|
|
258
|
+
codex-cli 0.167.2 has no flag that removes every tool. Still offered are a JavaScript
|
|
259
|
+
isolate (`exec`: no Node, no file system, no network, with a few built-in helpers such as
|
|
260
|
+
the current time, MCP resource listing and `apply_patch`), sub-agents that get the same
|
|
261
|
+
restricted tools, and a question to the user, which cannot be answered in exec mode. **Any
|
|
262
|
+
tool use fails the call**: if the output holds an item that is not a message, reasoning,
|
|
263
|
+
an error or a plan, evalproof raises `codex used a tool (<type>); isolation broken` and
|
|
264
|
+
discards the reply. A codex too old to know a flag evalproof sets exits with `Unknown
|
|
265
|
+
feature flag`, and evalproof says to upgrade it (it was tested with codex-cli 0.167.2).
|
|
266
|
+
* **No temperature, seed or token limit.** Neither CLI exposes one, so `temperature`, `seed`
|
|
267
|
+
and `max_tokens` are ignored. `think` maps to Codex's reasoning effort (`false` is `low`,
|
|
268
|
+
`true` is `medium`; `params.extra.reasoning_effort` sets it directly). A run is therefore
|
|
269
|
+
not repeatable the way a seeded local run is, so the run records the CLI, its version, the
|
|
270
|
+
isolation scheme and the model id that actually answered, and the cache keys on the CLI
|
|
271
|
+
version. To see how much a judge wobbles, score the same outputs twice with `--no-cache`, or
|
|
272
|
+
[calibrate](#concepts) it against labels.
|
|
273
|
+
* **A changed judge is not comparable.** `compare` and the gate refuse (exit 4) when the judge's
|
|
274
|
+
target, CLI version, isolation or resolved model differs between the two runs, because the
|
|
275
|
+
scores then come from a different instrument. Pass `--allow-judge-change` or set
|
|
276
|
+
`gate.allow_judge_change: true` to accept it. A changed system target or CLI version only
|
|
277
|
+
warns, since that is usually the change under test. Runs written before 0.4.0 carry none of
|
|
278
|
+
these keys and compare as before.
|
|
279
|
+
* **Latency and cost.** A call takes about 1.4 s on `claude-code:haiku` and about 4 s on
|
|
280
|
+
Codex, mostly start-up, so set `concurrency`. There is no API bill; the calls count against
|
|
281
|
+
your subscription's usage limits.
|
|
282
|
+
* **Terms.** OpenAI documents Codex CLI use on a ChatGPT plan, including automation.
|
|
283
|
+
Anthropic's terms allow a user to run the unmodified Claude Code binary on their own
|
|
284
|
+
subscription; its plan limits assume ordinary individual use, and it forbids third-party
|
|
285
|
+
products from routing other users' requests through subscription credentials. evalproof
|
|
286
|
+
never touches credentials: it runs your own `claude` binary, on your machine, for your own
|
|
287
|
+
evals. For CI or team-wide use, use an API key (`EVALPROOF_CLI_AUTH=api-key`, or
|
|
288
|
+
`api_key_env`). See Anthropic's
|
|
289
|
+
[legal and compliance page](https://code.claude.com/docs/en/legal-and-compliance). Nothing
|
|
290
|
+
uses a CLI unless a suite names a `claude-code:` or `codex:` target (or an experimental one,
|
|
291
|
+
below).
|
|
292
|
+
|
|
293
|
+
### Experimental providers
|
|
294
|
+
|
|
295
|
+
`opencode:`, `cursor:` and `copilot:` follow the same rules as the two above: your own logged-in
|
|
296
|
+
binary, run as a subprocess in a fresh empty directory, its API-key variable removed from the
|
|
297
|
+
child's environment unless `api_key_env` names one, a bounded runtime, and the CLI, its version
|
|
298
|
+
and an isolation tag (`opencode/0`, `cursor/0`, `copilot/0`) in the run and in the cache key.
|
|
299
|
+
They are experimental because their isolation is less proven than Claude Code's and Codex's.
|
|
300
|
+
Each prints `evalproof: <name> provider is experimental` on stderr the first time it is used in
|
|
301
|
+
a process. Like the other two they ignore `temperature`, `seed` and `max_tokens`, and
|
|
302
|
+
`evalproof doctor` without a suite lists each of them whose binary is on `PATH`. What was
|
|
303
|
+
checked, and what was not:
|
|
304
|
+
|
|
305
|
+
* **`opencode:<provider>/<model>`** runs `opencode run --standalone --archived --format json`
|
|
306
|
+
with a custom agent that has every tool off and every permission denied, your system text as
|
|
307
|
+
that agent's prompt, and project config, the global config directory and the Claude Code
|
|
308
|
+
compatibility files switched off. The prompt is on stdin. *Verified* only against local
|
|
309
|
+
ollama models (`qwen3:8b`, a handful of calls): no tool event, a canary `AGENTS.md` in the
|
|
310
|
+
project not read, a canary `AGENTS.md` in the global config directory read by default and not
|
|
311
|
+
read with these settings, the parser and the error path. *Not verified:* any hosted model;
|
|
312
|
+
whether `~/.claude/CLAUDE.md` is read (that test was inconclusive); whether the empty
|
|
313
|
+
`OPENCODE_CONFIG_DIR` hides a login stored there; the token counts (input adds cache reads
|
|
314
|
+
and writes, output adds reasoning, summed per field name). OpenCode has no single login, so
|
|
315
|
+
`doctor` prints `auth: not checked`. Only `OPENCODE_API_KEY` is removed from the environment:
|
|
316
|
+
the key of the model's own provider (`ANTHROPIC_API_KEY` for `opencode:anthropic/...`) is
|
|
317
|
+
left, because naming that provider in the model id is choosing its key, and removing it would
|
|
318
|
+
break CI. `--archived` keeps a session out of OpenCode's lists but does not delete it, so
|
|
319
|
+
sessions accumulate in OpenCode's data directory (each run is a private `--standalone`
|
|
320
|
+
server in a temporary project); prune them yourself.
|
|
321
|
+
* **`cursor:<model>`** runs `cursor-agent -p --mode ask --trust` with an empty `--workspace`.
|
|
322
|
+
*Not verified:* Cursor was not logged in on the test machine, so no call succeeded. The flags
|
|
323
|
+
come from the CLI's help and documentation, and the parser from its source; it is lenient (a
|
|
324
|
+
JSON result object, an array of events, terminal noise around it) and an unexpected shape
|
|
325
|
+
fails with a message that says what was expected. The empty workspace is the only isolation:
|
|
326
|
+
no flag ignores rules files, user-level rules were not checked, and whether ask mode really
|
|
327
|
+
has no tools is untested. There is no system-prompt option, so your system text goes in front
|
|
328
|
+
of the prompt under a heading, and the prompt is an argument (nothing says the CLI reads
|
|
329
|
+
stdin), so it is visible in `ps` while the call runs and a prompt over 100,000 bytes is
|
|
330
|
+
refused. The key variable is `CURSOR_API_KEY`, passed through the environment, never as
|
|
331
|
+
`--api-key`. `doctor` reads `cursor-agent about` for the login state (`status` hung) and
|
|
332
|
+
prints no account.
|
|
333
|
+
* **`copilot:<model>`** runs `copilot -s --no-auto-update --no-ask-user --available-tools= -p`.
|
|
334
|
+
*Not verified:* Copilot CLI was not installed on the test machine; every flag comes from
|
|
335
|
+
GitHub's documentation. `--available-tools=` is an allow-list with nothing in it, the most
|
|
336
|
+
restrictive documented tool option; whether the CLI accepts an empty list was not tried. No
|
|
337
|
+
`--allow-*` flag is ever passed, so a tool that needs approval cannot get it. The reply is
|
|
338
|
+
plain text, so token counts are 0. `COPILOT_HOME` is not redirected, so user-level Copilot
|
|
339
|
+
instructions may still be read. System text and prompt travel as for Cursor. Only
|
|
340
|
+
`COPILOT_GITHUB_TOKEN` is removed; `GH_TOKEN` and `GITHUB_TOKEN` are left because `gh` and CI
|
|
341
|
+
set them for other reasons. `doctor` prints `auth: not checked`.
|
|
342
|
+
|
|
189
343
|
## Concepts
|
|
190
344
|
|
|
191
345
|
**Suite** — one YAML file: a JSONL golden set, the system under test, scorers, an optional
|
|
@@ -210,7 +364,8 @@ gate: {metric: judge, max_drop: 0.03, alpha: 0.05, min_cases: 40, max_p95_latenc
|
|
|
210
364
|
|
|
211
365
|
**Run** — `evalproof run suite.yaml` writes one self-describing JSON file: every case's
|
|
212
366
|
output, latency, tokens and scores, plus the dataset hash, seed, Ollama version and model
|
|
213
|
-
digests
|
|
367
|
+
digests, and for a `claude-code` or `codex` target the CLI, its version and the model that
|
|
368
|
+
answered.
|
|
214
369
|
|
|
215
370
|
**Scorers** — `exact_match`, `contains`, `regex` (accept/reject answer keys),
|
|
216
371
|
`json_schema`, `numeric_tolerance`, `recall_at_k`, `mrr`, `ndcg`, and `llm_judge`: a strict
|
|
@@ -235,7 +390,7 @@ orders and reports order-dependent verdicts as ties.
|
|
|
235
390
|
| 0 | pass — possibly with warnings (a big drop that is not significant; a significant drop within tolerance) |
|
|
236
391
|
| 1 | quality regression: drop ≥ `max_drop` **and** p < `alpha` |
|
|
237
392
|
| 3 | budget breach: candidate p95 latency or cost over the limit |
|
|
238
|
-
| 4 | cannot gate: fewer than `min_cases` paired cases,
|
|
393
|
+
| 4 | cannot gate: fewer than `min_cases` paired cases, the runs used different data, or the judge changed (unless `--allow-judge-change`) |
|
|
239
394
|
| 2 | usage or configuration error |
|
|
240
395
|
|
|
241
396
|
The gate checks one metric by default. To gate several, repeat `--metric` or list them in
|
|
@@ -322,6 +477,7 @@ The architecture diagram is in the [README on GitHub](https://github.com/technob
|
|
|
322
477
|
| 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) |
|
|
323
478
|
| 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) |
|
|
324
479
|
| One metric by default; several with Holm correction | Gate every metric at α each | Testing many metrics at α = 0.05 each inflates false failures | 0003 |
|
|
480
|
+
| Run a coding-agent CLI as a provider only in isolation, and gate on its fingerprint | Pass the CLI's defaults through, or ignore its version | A CLI that reads your project's instructions is not a judge, and a silent upgrade changes the instrument | [0004](https://github.com/technobasant/evalproof/blob/main/docs/adr/0004-agent-clis-as-providers.md) |
|
|
325
481
|
| 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) |
|
|
326
482
|
| numpy and the standard library for statistics | SciPy, statsmodels | Small enough to read and to test against hand-worked values | — |
|
|
327
483
|
|
|
@@ -331,7 +487,7 @@ The composite action runs `compare --gate`, appends the report to the job summar
|
|
|
331
487
|
fails the job on exit codes 1, 3 or 4:
|
|
332
488
|
|
|
333
489
|
```yaml
|
|
334
|
-
- uses: technobasant/evalproof@v0.
|
|
490
|
+
- uses: technobasant/evalproof@v0.4.0
|
|
335
491
|
with:
|
|
336
492
|
baseline: eval/baseline.json # produced on main, e.g. as a build artifact
|
|
337
493
|
candidate: eval/candidate.json # produced on this branch
|
|
@@ -342,6 +498,14 @@ fails the job on exit codes 1, 3 or 4:
|
|
|
342
498
|
This repository's own CI runs the offline example on every commit and asserts that the
|
|
343
499
|
gate returns 1 for the real regression and 0 for the noisy and refactored candidates.
|
|
344
500
|
|
|
501
|
+
If the judge is a Claude Code or Codex CLI, `evalproof init --ci api-key` writes a workflow that
|
|
502
|
+
installs the CLI at the version found at init time and runs `evals/suite.yaml` as it is. The run
|
|
503
|
+
step sets `EVALPROOF_CLI_AUTH: api-key` and the repository secret (`ANTHROPIC_API_KEY` for
|
|
504
|
+
Claude Code, `OPENAI_API_KEY` for Codex), so the CLI uses that key instead of a login. The
|
|
505
|
+
suite names no key and there is no second suite file; a suite with an `api_key_env` still uses
|
|
506
|
+
that variable. Keep the pinned CLI version equal to the one in your baseline run, because the
|
|
507
|
+
gate exits 4 when the judge's CLI version differs.
|
|
508
|
+
|
|
345
509
|
## How it relates to other tools
|
|
346
510
|
|
|
347
511
|
[promptfoo](https://www.promptfoo.dev/docs/intro/) (open-source, local evals of prompts,
|
|
@@ -64,6 +64,23 @@ questions, one Apple M5. Full method and every table in [MEASUREMENTS.md](MEASUR
|
|
|
64
64
|
|
|
65
65
|
## Five-minute quickstart
|
|
66
66
|
|
|
67
|
+
In your own repository, one command writes a starter suite, a CI workflow and an agent skill,
|
|
68
|
+
and a second runs it:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
uvx evalproof init
|
|
72
|
+
uvx evalproof run evals/suite.yaml --out evals/runs/latest.json
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`init` picks the judge from what is on your `PATH`: a logged-in `claude`, then `codex`, then
|
|
76
|
+
Ollama, then a hosted Anthropic model with `ANTHROPIC_API_KEY`. It writes `evals/` (the
|
|
77
|
+
suite, three sample cases, a README), `.github/workflows/evalproof.yml` (`--ci keyless`, the
|
|
78
|
+
default, makes no model calls in CI; `--ci api-key` runs the suite there; `--ci none` writes
|
|
79
|
+
no workflow) and a skill for coding agents at `.agents/skills/evalproof/SKILL.md` and
|
|
80
|
+
`.claude/skills/evalproof/SKILL.md` (`--no-skill` skips it). It prints one line per file,
|
|
81
|
+
never overwrites one without `--force`, and makes no model call. Replace the sample cases
|
|
82
|
+
with your own: the gate needs 20 before it will decide.
|
|
83
|
+
|
|
67
84
|
The offline example lives in the repository:
|
|
68
85
|
|
|
69
86
|
```bash
|
|
@@ -99,7 +116,11 @@ The target is `<provider>:<model>`; the model name is everything after the first
|
|
|
99
116
|
`openrouter:anthropic/claude-sonnet-4.5` and `ollama:qwen3:8b` both work. Two native adapters
|
|
100
117
|
(`ollama`, `anthropic`) and one OpenAI-compatible adapter driven by a table of provider
|
|
101
118
|
profiles ([`providers/profiles.py`](src/evalproof/providers/profiles.py), each row cites the
|
|
102
|
-
provider's documentation). Plain HTTP throughout, no vendor SDK.
|
|
119
|
+
provider's documentation). Plain HTTP throughout, no vendor SDK. Two more targets run a coding
|
|
120
|
+
agent you already have logged in, `claude-code` and `codex`: see
|
|
121
|
+
[Use your Claude Code or Codex login](#use-your-claude-code-or-codex-login). Three experimental
|
|
122
|
+
ones run OpenCode, Cursor and GitHub Copilot the same way: see
|
|
123
|
+
[Experimental providers](#experimental-providers).
|
|
103
124
|
|
|
104
125
|
| Provider | Example target | Key variable | Where |
|
|
105
126
|
|---|---|---|---|
|
|
@@ -120,6 +141,11 @@ provider's documentation). Plain HTTP throughout, no vendor SDK.
|
|
|
120
141
|
| `openrouter` | `openrouter:anthropic/claude-sonnet-4.5` | `OPENROUTER_API_KEY` | hosted |
|
|
121
142
|
| `cerebras` | `cerebras:gpt-oss-120b` | `CEREBRAS_API_KEY` | hosted |
|
|
122
143
|
| `openai-compatible` | `openai-compatible:<model>` | optional, via `api_key_env` | any server, needs `base_url` |
|
|
144
|
+
| `claude-code` | `claude-code:haiku` | none: your Claude Code login | local CLI, see below |
|
|
145
|
+
| `codex` | `codex:gpt-6-luna` | none: your ChatGPT login | local CLI, see below |
|
|
146
|
+
| `opencode` | `opencode:ollama/qwen3:8b` | none: OpenCode's own login, per model provider | local CLI, experimental |
|
|
147
|
+
| `cursor` | `cursor:auto` | none: your Cursor login | local CLI, experimental |
|
|
148
|
+
| `copilot` | `copilot:gpt-5` | none: your GitHub Copilot login | local CLI, experimental |
|
|
123
149
|
|
|
124
150
|
```yaml
|
|
125
151
|
system:
|
|
@@ -150,6 +176,134 @@ system:
|
|
|
150
176
|
* AWS Bedrock and Google Vertex AI sign requests with cloud credentials, which evalproof does
|
|
151
177
|
not implement. Use a gateway's OpenAI-compatible endpoint through `openai-compatible`.
|
|
152
178
|
|
|
179
|
+
## Use your Claude Code or Codex login
|
|
180
|
+
|
|
181
|
+
If Claude Code or Codex is already logged in on your machine, either can be the judge or the
|
|
182
|
+
system under test, with no API key:
|
|
183
|
+
|
|
184
|
+
```yaml
|
|
185
|
+
judge:
|
|
186
|
+
target: claude-code:haiku # an alias (haiku, sonnet, opus) or a full model id
|
|
187
|
+
system:
|
|
188
|
+
target: codex:gpt-6-luna
|
|
189
|
+
concurrency: 4
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Run `evalproof doctor suite.yaml` first. It prints each CLI's path, version, login mode and
|
|
193
|
+
the isolation flags it will pass, and exits 1 if a target of the suite is not usable. Without a
|
|
194
|
+
suite it surveys every CLI it knows that is installed, and exits 1 only when none is usable. It
|
|
195
|
+
makes no model call.
|
|
196
|
+
|
|
197
|
+
evalproof runs your own `claude` or `codex` binary as a subprocess, sends the prompt on stdin
|
|
198
|
+
and reads the JSON it prints. It never reads a token file or the keychain. The environment
|
|
199
|
+
variable `EVALPROOF_CLI_AUTH` says which credentials the CLI uses:
|
|
200
|
+
|
|
201
|
+
* `login` (the default, also when it is unset or empty) removes the CLI's key variables from
|
|
202
|
+
the call (`ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN` for Claude Code, `OPENAI_API_KEY`
|
|
203
|
+
and `CODEX_API_KEY` for Codex), so the login is what gets used.
|
|
204
|
+
* `api-key` leaves them in place, so the CLI uses its own key. This is the route for CI.
|
|
205
|
+
* Any other value is an error.
|
|
206
|
+
|
|
207
|
+
`api_key_env` in the suite works in either mode and wins: it passes that variable to the CLI as
|
|
208
|
+
its own key variable. The mode is not part of the run's fingerprint or the cache key, because
|
|
209
|
+
it is the same model either way. `evalproof doctor` prints the mode in effect and warns when
|
|
210
|
+
`api-key` is chosen but the CLI's key variable is not set.
|
|
211
|
+
|
|
212
|
+
* **Isolation is a requirement, not an option.** A coding agent is not a model endpoint. Left
|
|
213
|
+
alone it loads the project's `CLAUDE.md` or `AGENTS.md`, your memory, plugins, MCP servers
|
|
214
|
+
and skills into the prompt, and it can run commands. A judge that has read your project's
|
|
215
|
+
instructions is not a judge. So every call runs in a fresh empty directory, with the
|
|
216
|
+
agent's own system prompt replaced by yours (or one neutral line), its tools, settings, MCP
|
|
217
|
+
servers and skills switched off, and no permission-bypass flag. `evalproof doctor` prints
|
|
218
|
+
the exact flags. The two CLIs do not isolate equally well:
|
|
219
|
+
* Claude Code is run with **no tools at all** (`--tools ''`).
|
|
220
|
+
* Codex runs in a read-only sandbox with its shell, plugins, apps, hooks, multi-agent mode,
|
|
221
|
+
sleep and image tools, bundled skills and your user config and rules switched off, but
|
|
222
|
+
codex-cli 0.167.2 has no flag that removes every tool. Still offered are a JavaScript
|
|
223
|
+
isolate (`exec`: no Node, no file system, no network, with a few built-in helpers such as
|
|
224
|
+
the current time, MCP resource listing and `apply_patch`), sub-agents that get the same
|
|
225
|
+
restricted tools, and a question to the user, which cannot be answered in exec mode. **Any
|
|
226
|
+
tool use fails the call**: if the output holds an item that is not a message, reasoning,
|
|
227
|
+
an error or a plan, evalproof raises `codex used a tool (<type>); isolation broken` and
|
|
228
|
+
discards the reply. A codex too old to know a flag evalproof sets exits with `Unknown
|
|
229
|
+
feature flag`, and evalproof says to upgrade it (it was tested with codex-cli 0.167.2).
|
|
230
|
+
* **No temperature, seed or token limit.** Neither CLI exposes one, so `temperature`, `seed`
|
|
231
|
+
and `max_tokens` are ignored. `think` maps to Codex's reasoning effort (`false` is `low`,
|
|
232
|
+
`true` is `medium`; `params.extra.reasoning_effort` sets it directly). A run is therefore
|
|
233
|
+
not repeatable the way a seeded local run is, so the run records the CLI, its version, the
|
|
234
|
+
isolation scheme and the model id that actually answered, and the cache keys on the CLI
|
|
235
|
+
version. To see how much a judge wobbles, score the same outputs twice with `--no-cache`, or
|
|
236
|
+
[calibrate](#concepts) it against labels.
|
|
237
|
+
* **A changed judge is not comparable.** `compare` and the gate refuse (exit 4) when the judge's
|
|
238
|
+
target, CLI version, isolation or resolved model differs between the two runs, because the
|
|
239
|
+
scores then come from a different instrument. Pass `--allow-judge-change` or set
|
|
240
|
+
`gate.allow_judge_change: true` to accept it. A changed system target or CLI version only
|
|
241
|
+
warns, since that is usually the change under test. Runs written before 0.4.0 carry none of
|
|
242
|
+
these keys and compare as before.
|
|
243
|
+
* **Latency and cost.** A call takes about 1.4 s on `claude-code:haiku` and about 4 s on
|
|
244
|
+
Codex, mostly start-up, so set `concurrency`. There is no API bill; the calls count against
|
|
245
|
+
your subscription's usage limits.
|
|
246
|
+
* **Terms.** OpenAI documents Codex CLI use on a ChatGPT plan, including automation.
|
|
247
|
+
Anthropic's terms allow a user to run the unmodified Claude Code binary on their own
|
|
248
|
+
subscription; its plan limits assume ordinary individual use, and it forbids third-party
|
|
249
|
+
products from routing other users' requests through subscription credentials. evalproof
|
|
250
|
+
never touches credentials: it runs your own `claude` binary, on your machine, for your own
|
|
251
|
+
evals. For CI or team-wide use, use an API key (`EVALPROOF_CLI_AUTH=api-key`, or
|
|
252
|
+
`api_key_env`). See Anthropic's
|
|
253
|
+
[legal and compliance page](https://code.claude.com/docs/en/legal-and-compliance). Nothing
|
|
254
|
+
uses a CLI unless a suite names a `claude-code:` or `codex:` target (or an experimental one,
|
|
255
|
+
below).
|
|
256
|
+
|
|
257
|
+
### Experimental providers
|
|
258
|
+
|
|
259
|
+
`opencode:`, `cursor:` and `copilot:` follow the same rules as the two above: your own logged-in
|
|
260
|
+
binary, run as a subprocess in a fresh empty directory, its API-key variable removed from the
|
|
261
|
+
child's environment unless `api_key_env` names one, a bounded runtime, and the CLI, its version
|
|
262
|
+
and an isolation tag (`opencode/0`, `cursor/0`, `copilot/0`) in the run and in the cache key.
|
|
263
|
+
They are experimental because their isolation is less proven than Claude Code's and Codex's.
|
|
264
|
+
Each prints `evalproof: <name> provider is experimental` on stderr the first time it is used in
|
|
265
|
+
a process. Like the other two they ignore `temperature`, `seed` and `max_tokens`, and
|
|
266
|
+
`evalproof doctor` without a suite lists each of them whose binary is on `PATH`. What was
|
|
267
|
+
checked, and what was not:
|
|
268
|
+
|
|
269
|
+
* **`opencode:<provider>/<model>`** runs `opencode run --standalone --archived --format json`
|
|
270
|
+
with a custom agent that has every tool off and every permission denied, your system text as
|
|
271
|
+
that agent's prompt, and project config, the global config directory and the Claude Code
|
|
272
|
+
compatibility files switched off. The prompt is on stdin. *Verified* only against local
|
|
273
|
+
ollama models (`qwen3:8b`, a handful of calls): no tool event, a canary `AGENTS.md` in the
|
|
274
|
+
project not read, a canary `AGENTS.md` in the global config directory read by default and not
|
|
275
|
+
read with these settings, the parser and the error path. *Not verified:* any hosted model;
|
|
276
|
+
whether `~/.claude/CLAUDE.md` is read (that test was inconclusive); whether the empty
|
|
277
|
+
`OPENCODE_CONFIG_DIR` hides a login stored there; the token counts (input adds cache reads
|
|
278
|
+
and writes, output adds reasoning, summed per field name). OpenCode has no single login, so
|
|
279
|
+
`doctor` prints `auth: not checked`. Only `OPENCODE_API_KEY` is removed from the environment:
|
|
280
|
+
the key of the model's own provider (`ANTHROPIC_API_KEY` for `opencode:anthropic/...`) is
|
|
281
|
+
left, because naming that provider in the model id is choosing its key, and removing it would
|
|
282
|
+
break CI. `--archived` keeps a session out of OpenCode's lists but does not delete it, so
|
|
283
|
+
sessions accumulate in OpenCode's data directory (each run is a private `--standalone`
|
|
284
|
+
server in a temporary project); prune them yourself.
|
|
285
|
+
* **`cursor:<model>`** runs `cursor-agent -p --mode ask --trust` with an empty `--workspace`.
|
|
286
|
+
*Not verified:* Cursor was not logged in on the test machine, so no call succeeded. The flags
|
|
287
|
+
come from the CLI's help and documentation, and the parser from its source; it is lenient (a
|
|
288
|
+
JSON result object, an array of events, terminal noise around it) and an unexpected shape
|
|
289
|
+
fails with a message that says what was expected. The empty workspace is the only isolation:
|
|
290
|
+
no flag ignores rules files, user-level rules were not checked, and whether ask mode really
|
|
291
|
+
has no tools is untested. There is no system-prompt option, so your system text goes in front
|
|
292
|
+
of the prompt under a heading, and the prompt is an argument (nothing says the CLI reads
|
|
293
|
+
stdin), so it is visible in `ps` while the call runs and a prompt over 100,000 bytes is
|
|
294
|
+
refused. The key variable is `CURSOR_API_KEY`, passed through the environment, never as
|
|
295
|
+
`--api-key`. `doctor` reads `cursor-agent about` for the login state (`status` hung) and
|
|
296
|
+
prints no account.
|
|
297
|
+
* **`copilot:<model>`** runs `copilot -s --no-auto-update --no-ask-user --available-tools= -p`.
|
|
298
|
+
*Not verified:* Copilot CLI was not installed on the test machine; every flag comes from
|
|
299
|
+
GitHub's documentation. `--available-tools=` is an allow-list with nothing in it, the most
|
|
300
|
+
restrictive documented tool option; whether the CLI accepts an empty list was not tried. No
|
|
301
|
+
`--allow-*` flag is ever passed, so a tool that needs approval cannot get it. The reply is
|
|
302
|
+
plain text, so token counts are 0. `COPILOT_HOME` is not redirected, so user-level Copilot
|
|
303
|
+
instructions may still be read. System text and prompt travel as for Cursor. Only
|
|
304
|
+
`COPILOT_GITHUB_TOKEN` is removed; `GH_TOKEN` and `GITHUB_TOKEN` are left because `gh` and CI
|
|
305
|
+
set them for other reasons. `doctor` prints `auth: not checked`.
|
|
306
|
+
|
|
153
307
|
## Concepts
|
|
154
308
|
|
|
155
309
|
**Suite** — one YAML file: a JSONL golden set, the system under test, scorers, an optional
|
|
@@ -174,7 +328,8 @@ gate: {metric: judge, max_drop: 0.03, alpha: 0.05, min_cases: 40, max_p95_latenc
|
|
|
174
328
|
|
|
175
329
|
**Run** — `evalproof run suite.yaml` writes one self-describing JSON file: every case's
|
|
176
330
|
output, latency, tokens and scores, plus the dataset hash, seed, Ollama version and model
|
|
177
|
-
digests
|
|
331
|
+
digests, and for a `claude-code` or `codex` target the CLI, its version and the model that
|
|
332
|
+
answered.
|
|
178
333
|
|
|
179
334
|
**Scorers** — `exact_match`, `contains`, `regex` (accept/reject answer keys),
|
|
180
335
|
`json_schema`, `numeric_tolerance`, `recall_at_k`, `mrr`, `ndcg`, and `llm_judge`: a strict
|
|
@@ -199,7 +354,7 @@ orders and reports order-dependent verdicts as ties.
|
|
|
199
354
|
| 0 | pass — possibly with warnings (a big drop that is not significant; a significant drop within tolerance) |
|
|
200
355
|
| 1 | quality regression: drop ≥ `max_drop` **and** p < `alpha` |
|
|
201
356
|
| 3 | budget breach: candidate p95 latency or cost over the limit |
|
|
202
|
-
| 4 | cannot gate: fewer than `min_cases` paired cases,
|
|
357
|
+
| 4 | cannot gate: fewer than `min_cases` paired cases, the runs used different data, or the judge changed (unless `--allow-judge-change`) |
|
|
203
358
|
| 2 | usage or configuration error |
|
|
204
359
|
|
|
205
360
|
The gate checks one metric by default. To gate several, repeat `--metric` or list them in
|
|
@@ -304,6 +459,7 @@ flowchart LR
|
|
|
304
459
|
| 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) |
|
|
305
460
|
| 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) |
|
|
306
461
|
| One metric by default; several with Holm correction | Gate every metric at α each | Testing many metrics at α = 0.05 each inflates false failures | 0003 |
|
|
462
|
+
| Run a coding-agent CLI as a provider only in isolation, and gate on its fingerprint | Pass the CLI's defaults through, or ignore its version | A CLI that reads your project's instructions is not a judge, and a silent upgrade changes the instrument | [0004](docs/adr/0004-agent-clis-as-providers.md) |
|
|
307
463
|
| 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) |
|
|
308
464
|
| numpy and the standard library for statistics | SciPy, statsmodels | Small enough to read and to test against hand-worked values | — |
|
|
309
465
|
|
|
@@ -313,7 +469,7 @@ The composite action runs `compare --gate`, appends the report to the job summar
|
|
|
313
469
|
fails the job on exit codes 1, 3 or 4:
|
|
314
470
|
|
|
315
471
|
```yaml
|
|
316
|
-
- uses: technobasant/evalproof@v0.
|
|
472
|
+
- uses: technobasant/evalproof@v0.4.0
|
|
317
473
|
with:
|
|
318
474
|
baseline: eval/baseline.json # produced on main, e.g. as a build artifact
|
|
319
475
|
candidate: eval/candidate.json # produced on this branch
|
|
@@ -324,6 +480,14 @@ fails the job on exit codes 1, 3 or 4:
|
|
|
324
480
|
This repository's own CI runs the offline example on every commit and asserts that the
|
|
325
481
|
gate returns 1 for the real regression and 0 for the noisy and refactored candidates.
|
|
326
482
|
|
|
483
|
+
If the judge is a Claude Code or Codex CLI, `evalproof init --ci api-key` writes a workflow that
|
|
484
|
+
installs the CLI at the version found at init time and runs `evals/suite.yaml` as it is. The run
|
|
485
|
+
step sets `EVALPROOF_CLI_AUTH: api-key` and the repository secret (`ANTHROPIC_API_KEY` for
|
|
486
|
+
Claude Code, `OPENAI_API_KEY` for Codex), so the CLI uses that key instead of a login. The
|
|
487
|
+
suite names no key and there is no second suite file; a suite with an `api_key_env` still uses
|
|
488
|
+
that variable. Keep the pinned CLI version equal to the one in your baseline run, because the
|
|
489
|
+
gate exits 4 when the judge's CLI version differs.
|
|
490
|
+
|
|
327
491
|
## How it relates to other tools
|
|
328
492
|
|
|
329
493
|
[promptfoo](https://www.promptfoo.dev/docs/intro/) (open-source, local evals of prompts,
|