fastevals 0.1.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.
- fastevals-0.1.0/LICENSE +21 -0
- fastevals-0.1.0/PKG-INFO +244 -0
- fastevals-0.1.0/README.md +207 -0
- fastevals-0.1.0/fastevals/__init__.py +26 -0
- fastevals-0.1.0/fastevals/cli.py +125 -0
- fastevals-0.1.0/fastevals/config.py +130 -0
- fastevals-0.1.0/fastevals/dataset.py +74 -0
- fastevals-0.1.0/fastevals/evaluators.py +54 -0
- fastevals-0.1.0/fastevals/exceptions.py +19 -0
- fastevals-0.1.0/fastevals/mcp_server.py +157 -0
- fastevals-0.1.0/fastevals/models.py +80 -0
- fastevals-0.1.0/fastevals/pricing.py +54 -0
- fastevals-0.1.0/fastevals/providers.py +138 -0
- fastevals-0.1.0/fastevals/py.typed +0 -0
- fastevals-0.1.0/fastevals/registry.py +75 -0
- fastevals-0.1.0/fastevals/report.py +796 -0
- fastevals-0.1.0/fastevals/runner.py +104 -0
- fastevals-0.1.0/fastevals/structured.py +115 -0
- fastevals-0.1.0/fastevals.egg-info/PKG-INFO +244 -0
- fastevals-0.1.0/fastevals.egg-info/SOURCES.txt +33 -0
- fastevals-0.1.0/fastevals.egg-info/dependency_links.txt +1 -0
- fastevals-0.1.0/fastevals.egg-info/entry_points.txt +3 -0
- fastevals-0.1.0/fastevals.egg-info/requires.txt +16 -0
- fastevals-0.1.0/fastevals.egg-info/top_level.txt +1 -0
- fastevals-0.1.0/pyproject.toml +99 -0
- fastevals-0.1.0/setup.cfg +4 -0
- fastevals-0.1.0/tests/test_attachments.py +64 -0
- fastevals-0.1.0/tests/test_cli.py +95 -0
- fastevals-0.1.0/tests/test_evaluators.py +112 -0
- fastevals-0.1.0/tests/test_mcp_server.py +144 -0
- fastevals-0.1.0/tests/test_pricing.py +46 -0
- fastevals-0.1.0/tests/test_registry.py +85 -0
- fastevals-0.1.0/tests/test_report.py +61 -0
- fastevals-0.1.0/tests/test_runner_run.py +199 -0
- fastevals-0.1.0/tests/test_structured.py +95 -0
fastevals-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dmitry Semenov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
fastevals-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastevals
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Small, provider-agnostic evaluation runner for LLM applications.
|
|
5
|
+
Author: Dmitry Semenov
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: llm,evaluation,benchmark,litellm,mcp
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: jsonschema>=4.23
|
|
24
|
+
Provides-Extra: native
|
|
25
|
+
Requires-Dist: litellm>=1.74; extra == "native"
|
|
26
|
+
Provides-Extra: mcp
|
|
27
|
+
Requires-Dist: mcp>=1.2; extra == "mcp"
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
|
|
32
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
33
|
+
Requires-Dist: mypy>=1.11; extra == "dev"
|
|
34
|
+
Requires-Dist: types-jsonschema>=4.23; extra == "dev"
|
|
35
|
+
Requires-Dist: mcp>=1.2; extra == "dev"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# fastevals
|
|
39
|
+
|
|
40
|
+
**Evaluation tooling your AI agents can drive.**
|
|
41
|
+
|
|
42
|
+
fastevals is a small, provider-agnostic evaluation runner for LLM
|
|
43
|
+
applications. Run one prompt — or a whole dataset — across a matrix of
|
|
44
|
+
models, reasoning efforts and providers, save every response, and get a
|
|
45
|
+
readable standalone HTML comparison report with cost, latency and token
|
|
46
|
+
metrics.
|
|
47
|
+
|
|
48
|
+
It ships as an **MCP server**, so Claude Desktop, Claude Code or any other
|
|
49
|
+
MCP client can run evaluations as a native tool: your agent decides *what*
|
|
50
|
+
to test, fastevals answers *which model does it best*.
|
|
51
|
+
|
|
52
|
+
[](https://github.com/semenovdv/fastevals/actions/workflows/ci.yml)
|
|
53
|
+

|
|
54
|
+

|
|
55
|
+
[](https://github.com/astral-sh/ruff)
|
|
56
|
+

|
|
57
|
+
[](LICENSE)
|
|
58
|
+
|
|
59
|
+
<p align="center">
|
|
60
|
+
<img src="docs/assets/report.png" alt="fastevals HTML report" width="820">
|
|
61
|
+
</p>
|
|
62
|
+
|
|
63
|
+
## Drive it from Claude (MCP)
|
|
64
|
+
|
|
65
|
+
Install the server extras and register the entry point with any MCP client:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
python3 -m pip install 'fastevals[mcp,native]'
|
|
69
|
+
claude mcp add fastevals -- fastevals-mcp # Claude Code
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Claude Desktop (`claude_desktop_config.json`):
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"mcpServers": { "fastevals": { "command": "fastevals-mcp" } }
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Exposed tools:
|
|
81
|
+
|
|
82
|
+
| Tool | Purpose |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `run_evaluation` | Run a prompt or dataset across providers; returns JSON summary + report paths |
|
|
85
|
+
| `list_models` | Registry inspector: models, reasoning efforts, pricing |
|
|
86
|
+
| `get_run` | Summarize a saved run: pass rate, errors, total cost |
|
|
87
|
+
|
|
88
|
+
Example agent prompts that now just work:
|
|
89
|
+
|
|
90
|
+
> Use fastevals to compare gpt-5.6-luna at reasoning low and high on "Summarize
|
|
91
|
+
> this contract in 5 bullets" — which one is cheaper per correct answer?
|
|
92
|
+
|
|
93
|
+
> List my registered models, then evaluate cases.jsonl on terra and report
|
|
94
|
+
> the pass rate per effort level.
|
|
95
|
+
|
|
96
|
+
Because the CLI is fully non-interactive and returns structured JSON, agents
|
|
97
|
+
can also drive evaluations through plain shell execution without MCP.
|
|
98
|
+
|
|
99
|
+
## Why fastevals
|
|
100
|
+
|
|
101
|
+
- **Structured output that verifies** — compact schema syntax compiles to JSON Schema, is sent to the provider, and every response is validated locally before it reaches `run.json`.
|
|
102
|
+
- **Honest metrics** — disjoint token buckets (input / output / reasoning / cached), per-bucket pricing from your registry, no fake TTFT without streaming.
|
|
103
|
+
- **Real evaluation loop** — JSONL/CSV datasets, deterministic evaluators (`exact_match`, `contains`, `json_valid`, `regex`), repeated runs for stability.
|
|
104
|
+
- **Boring engineering** — strict typing, ~90% branch coverage, ruff + mypy + coverage gates in CI, single-file reports with zero telemetry.
|
|
105
|
+
|
|
106
|
+
## Install
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
python3 -m pip install 'fastevals[native]' # from PyPI once released
|
|
110
|
+
# or from source:
|
|
111
|
+
git clone https://github.com/semenovdv/fastevals && python3 -m pip install -e '.[native]'
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## CLI quick start
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
export OPENAI_API_KEY=... # keys live in the environment only
|
|
118
|
+
fastevals --prompt "Explain evaluation in three bullets" \
|
|
119
|
+
--providers openai --out runs
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Every run writes a timestamped directory under `--out` containing
|
|
123
|
+
`run.json` (machine-readable) and `report.html` (a standalone dashboard you
|
|
124
|
+
can open or send to anyone). Exit codes: `0` when every model completed,
|
|
125
|
+
`1` otherwise — easy to script.
|
|
126
|
+
|
|
127
|
+
### Models and reasoning efforts
|
|
128
|
+
|
|
129
|
+
Entries in `config/models.toml` become cells in the matrix:
|
|
130
|
+
|
|
131
|
+
```toml
|
|
132
|
+
["openai:gpt-5.6-luna"]
|
|
133
|
+
provider = "openai"
|
|
134
|
+
model = "gpt-5.6-luna"
|
|
135
|
+
api_key_env = "OPENAI_API_KEY"
|
|
136
|
+
reasoning_efforts = "none|low" # expands into two runs
|
|
137
|
+
input_cost_usd_per_mtok = 1.0 # USD per 1M tokens
|
|
138
|
+
output_cost_usd_per_mtok = 6.0
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Providers are validated against the registry; unknown names fail fast with a
|
|
142
|
+
helpful message. API keys are read from environment variables only — never
|
|
143
|
+
from the registry, never logged, and scrubbed from error messages.
|
|
144
|
+
|
|
145
|
+
### Structured output
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
fastevals \
|
|
149
|
+
--prompt "Extract all relevant invoice fields" \
|
|
150
|
+
--structured-output 'invoice_number:str("Unique identifier"),total:float("Amount incl. tax"),line_items:str[]("Items"),notes:str?' \
|
|
151
|
+
--providers openai --out runs/invoice
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`?` marks optional fields, `[]` arrays, `"..."` descriptions passed to the
|
|
155
|
+
model (`str|int|float|bool` with aliases supported).
|
|
156
|
+
|
|
157
|
+
### Files and images
|
|
158
|
+
|
|
159
|
+
Images become vision parts, PDFs OpenAI-style file parts, text files inline:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
fastevals --image screenshot.png --structured-output 'x:int,y:int,width:int,height:int' \
|
|
163
|
+
--prompt "Bounding box of the main widget" --providers openai --out runs/image
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Datasets, evaluators, consistency
|
|
167
|
+
|
|
168
|
+
```jsonl
|
|
169
|
+
{"id": "capital-france", "prompt": "Capital of France? City name only.", "expected": "Paris", "evaluator": "exact_match"}
|
|
170
|
+
{"id": "json-output", "prompt": "Return {\"status\": \"ok\"} as JSON.", "evaluator": "json_valid"}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
fastevals --dataset cases.jsonl --nruns 3 --providers openai --out runs/dataset
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Reports aggregate pass rates, latency and cost per model across all attempts.
|
|
178
|
+
|
|
179
|
+
## The report
|
|
180
|
+
|
|
181
|
+
Each `report.html` is a self-contained dashboard (Chart.js from CDN, no
|
|
182
|
+
build step, no telemetry): summary cards with fastest / cheapest /
|
|
183
|
+
top-throughput runs, sortable and filterable comparison table with CSV and
|
|
184
|
+
Markdown export, latency / throughput / token / cost charts, detailed result
|
|
185
|
+
cards, per-model aggregates for datasets.
|
|
186
|
+
|
|
187
|
+
## Python API
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
import asyncio
|
|
191
|
+
from fastevals import RunConfig, run, save_report
|
|
192
|
+
|
|
193
|
+
config = RunConfig(prompt="Summarize eval best practices", providers=frozenset({"openai"}))
|
|
194
|
+
results = asyncio.run(run(config))
|
|
195
|
+
save_report(config, results, "runs")
|
|
196
|
+
print(results[0].output, results[0].latency_ms, results[0].total_cost_usd)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Architecture
|
|
200
|
+
|
|
201
|
+
```mermaid
|
|
202
|
+
flowchart LR
|
|
203
|
+
CLI["cli.py"] --> RC["RunConfig"]
|
|
204
|
+
RC --> Runner["runner.py"]
|
|
205
|
+
DS["dataset.py"] --> Runner
|
|
206
|
+
EV["evaluators.py"] --> Runner
|
|
207
|
+
Runner --> Reg["registry.py"]
|
|
208
|
+
Reg --> Specs["ModelSpec"]
|
|
209
|
+
Runner --> Prov["providers.py<br/>LiteLLM adapter"]
|
|
210
|
+
Prov --> ST["structured.py<br/>schema · validation"]
|
|
211
|
+
Runner --> PR["pricing.py"]
|
|
212
|
+
Runner --> Rep["report.py<br/>single-file HTML"]
|
|
213
|
+
Rep --> Out["run.json + report.html"]
|
|
214
|
+
|
|
215
|
+
MCP["mcp_server.py"] --> Runner
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Adding a provider means implementing the single `call_model` contract in
|
|
219
|
+
`providers.py`; adding a model means adding five lines to the TOML registry.
|
|
220
|
+
No other layers need to change.
|
|
221
|
+
|
|
222
|
+
## Development
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
make dev # install with dev tooling
|
|
226
|
+
make check # ruff + mypy --strict + tests with an 85% coverage floor
|
|
227
|
+
make format # auto-fix style
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The test suite is fully offline: provider calls are replaced by a recorded
|
|
231
|
+
stub at the LiteLLM boundary; live API calls never run in CI.
|
|
232
|
+
|
|
233
|
+
## Limitations (by design)
|
|
234
|
+
|
|
235
|
+
- No streaming yet — TTFT is reported as unavailable rather than faked; latency and throughput are end-to-end.
|
|
236
|
+
- One prompt template per case; no few-shot templating or conversation history.
|
|
237
|
+
- Evaluators are deterministic heuristics; LLM-as-judge scoring is not included.
|
|
238
|
+
- Pricing comes from your registry, not a live price feed — keep it current.
|
|
239
|
+
|
|
240
|
+
See [`docs/ROADMAP.md`](docs/ROADMAP.md) for where this is heading.
|
|
241
|
+
|
|
242
|
+
## License
|
|
243
|
+
|
|
244
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# fastevals
|
|
2
|
+
|
|
3
|
+
**Evaluation tooling your AI agents can drive.**
|
|
4
|
+
|
|
5
|
+
fastevals is a small, provider-agnostic evaluation runner for LLM
|
|
6
|
+
applications. Run one prompt — or a whole dataset — across a matrix of
|
|
7
|
+
models, reasoning efforts and providers, save every response, and get a
|
|
8
|
+
readable standalone HTML comparison report with cost, latency and token
|
|
9
|
+
metrics.
|
|
10
|
+
|
|
11
|
+
It ships as an **MCP server**, so Claude Desktop, Claude Code or any other
|
|
12
|
+
MCP client can run evaluations as a native tool: your agent decides *what*
|
|
13
|
+
to test, fastevals answers *which model does it best*.
|
|
14
|
+
|
|
15
|
+
[](https://github.com/semenovdv/fastevals/actions/workflows/ci.yml)
|
|
16
|
+

|
|
17
|
+

|
|
18
|
+
[](https://github.com/astral-sh/ruff)
|
|
19
|
+

|
|
20
|
+
[](LICENSE)
|
|
21
|
+
|
|
22
|
+
<p align="center">
|
|
23
|
+
<img src="docs/assets/report.png" alt="fastevals HTML report" width="820">
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
## Drive it from Claude (MCP)
|
|
27
|
+
|
|
28
|
+
Install the server extras and register the entry point with any MCP client:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
python3 -m pip install 'fastevals[mcp,native]'
|
|
32
|
+
claude mcp add fastevals -- fastevals-mcp # Claude Code
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Claude Desktop (`claude_desktop_config.json`):
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"mcpServers": { "fastevals": { "command": "fastevals-mcp" } }
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Exposed tools:
|
|
44
|
+
|
|
45
|
+
| Tool | Purpose |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `run_evaluation` | Run a prompt or dataset across providers; returns JSON summary + report paths |
|
|
48
|
+
| `list_models` | Registry inspector: models, reasoning efforts, pricing |
|
|
49
|
+
| `get_run` | Summarize a saved run: pass rate, errors, total cost |
|
|
50
|
+
|
|
51
|
+
Example agent prompts that now just work:
|
|
52
|
+
|
|
53
|
+
> Use fastevals to compare gpt-5.6-luna at reasoning low and high on "Summarize
|
|
54
|
+
> this contract in 5 bullets" — which one is cheaper per correct answer?
|
|
55
|
+
|
|
56
|
+
> List my registered models, then evaluate cases.jsonl on terra and report
|
|
57
|
+
> the pass rate per effort level.
|
|
58
|
+
|
|
59
|
+
Because the CLI is fully non-interactive and returns structured JSON, agents
|
|
60
|
+
can also drive evaluations through plain shell execution without MCP.
|
|
61
|
+
|
|
62
|
+
## Why fastevals
|
|
63
|
+
|
|
64
|
+
- **Structured output that verifies** — compact schema syntax compiles to JSON Schema, is sent to the provider, and every response is validated locally before it reaches `run.json`.
|
|
65
|
+
- **Honest metrics** — disjoint token buckets (input / output / reasoning / cached), per-bucket pricing from your registry, no fake TTFT without streaming.
|
|
66
|
+
- **Real evaluation loop** — JSONL/CSV datasets, deterministic evaluators (`exact_match`, `contains`, `json_valid`, `regex`), repeated runs for stability.
|
|
67
|
+
- **Boring engineering** — strict typing, ~90% branch coverage, ruff + mypy + coverage gates in CI, single-file reports with zero telemetry.
|
|
68
|
+
|
|
69
|
+
## Install
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
python3 -m pip install 'fastevals[native]' # from PyPI once released
|
|
73
|
+
# or from source:
|
|
74
|
+
git clone https://github.com/semenovdv/fastevals && python3 -m pip install -e '.[native]'
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## CLI quick start
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
export OPENAI_API_KEY=... # keys live in the environment only
|
|
81
|
+
fastevals --prompt "Explain evaluation in three bullets" \
|
|
82
|
+
--providers openai --out runs
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Every run writes a timestamped directory under `--out` containing
|
|
86
|
+
`run.json` (machine-readable) and `report.html` (a standalone dashboard you
|
|
87
|
+
can open or send to anyone). Exit codes: `0` when every model completed,
|
|
88
|
+
`1` otherwise — easy to script.
|
|
89
|
+
|
|
90
|
+
### Models and reasoning efforts
|
|
91
|
+
|
|
92
|
+
Entries in `config/models.toml` become cells in the matrix:
|
|
93
|
+
|
|
94
|
+
```toml
|
|
95
|
+
["openai:gpt-5.6-luna"]
|
|
96
|
+
provider = "openai"
|
|
97
|
+
model = "gpt-5.6-luna"
|
|
98
|
+
api_key_env = "OPENAI_API_KEY"
|
|
99
|
+
reasoning_efforts = "none|low" # expands into two runs
|
|
100
|
+
input_cost_usd_per_mtok = 1.0 # USD per 1M tokens
|
|
101
|
+
output_cost_usd_per_mtok = 6.0
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Providers are validated against the registry; unknown names fail fast with a
|
|
105
|
+
helpful message. API keys are read from environment variables only — never
|
|
106
|
+
from the registry, never logged, and scrubbed from error messages.
|
|
107
|
+
|
|
108
|
+
### Structured output
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
fastevals \
|
|
112
|
+
--prompt "Extract all relevant invoice fields" \
|
|
113
|
+
--structured-output 'invoice_number:str("Unique identifier"),total:float("Amount incl. tax"),line_items:str[]("Items"),notes:str?' \
|
|
114
|
+
--providers openai --out runs/invoice
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`?` marks optional fields, `[]` arrays, `"..."` descriptions passed to the
|
|
118
|
+
model (`str|int|float|bool` with aliases supported).
|
|
119
|
+
|
|
120
|
+
### Files and images
|
|
121
|
+
|
|
122
|
+
Images become vision parts, PDFs OpenAI-style file parts, text files inline:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
fastevals --image screenshot.png --structured-output 'x:int,y:int,width:int,height:int' \
|
|
126
|
+
--prompt "Bounding box of the main widget" --providers openai --out runs/image
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Datasets, evaluators, consistency
|
|
130
|
+
|
|
131
|
+
```jsonl
|
|
132
|
+
{"id": "capital-france", "prompt": "Capital of France? City name only.", "expected": "Paris", "evaluator": "exact_match"}
|
|
133
|
+
{"id": "json-output", "prompt": "Return {\"status\": \"ok\"} as JSON.", "evaluator": "json_valid"}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
fastevals --dataset cases.jsonl --nruns 3 --providers openai --out runs/dataset
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Reports aggregate pass rates, latency and cost per model across all attempts.
|
|
141
|
+
|
|
142
|
+
## The report
|
|
143
|
+
|
|
144
|
+
Each `report.html` is a self-contained dashboard (Chart.js from CDN, no
|
|
145
|
+
build step, no telemetry): summary cards with fastest / cheapest /
|
|
146
|
+
top-throughput runs, sortable and filterable comparison table with CSV and
|
|
147
|
+
Markdown export, latency / throughput / token / cost charts, detailed result
|
|
148
|
+
cards, per-model aggregates for datasets.
|
|
149
|
+
|
|
150
|
+
## Python API
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
import asyncio
|
|
154
|
+
from fastevals import RunConfig, run, save_report
|
|
155
|
+
|
|
156
|
+
config = RunConfig(prompt="Summarize eval best practices", providers=frozenset({"openai"}))
|
|
157
|
+
results = asyncio.run(run(config))
|
|
158
|
+
save_report(config, results, "runs")
|
|
159
|
+
print(results[0].output, results[0].latency_ms, results[0].total_cost_usd)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Architecture
|
|
163
|
+
|
|
164
|
+
```mermaid
|
|
165
|
+
flowchart LR
|
|
166
|
+
CLI["cli.py"] --> RC["RunConfig"]
|
|
167
|
+
RC --> Runner["runner.py"]
|
|
168
|
+
DS["dataset.py"] --> Runner
|
|
169
|
+
EV["evaluators.py"] --> Runner
|
|
170
|
+
Runner --> Reg["registry.py"]
|
|
171
|
+
Reg --> Specs["ModelSpec"]
|
|
172
|
+
Runner --> Prov["providers.py<br/>LiteLLM adapter"]
|
|
173
|
+
Prov --> ST["structured.py<br/>schema · validation"]
|
|
174
|
+
Runner --> PR["pricing.py"]
|
|
175
|
+
Runner --> Rep["report.py<br/>single-file HTML"]
|
|
176
|
+
Rep --> Out["run.json + report.html"]
|
|
177
|
+
|
|
178
|
+
MCP["mcp_server.py"] --> Runner
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Adding a provider means implementing the single `call_model` contract in
|
|
182
|
+
`providers.py`; adding a model means adding five lines to the TOML registry.
|
|
183
|
+
No other layers need to change.
|
|
184
|
+
|
|
185
|
+
## Development
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
make dev # install with dev tooling
|
|
189
|
+
make check # ruff + mypy --strict + tests with an 85% coverage floor
|
|
190
|
+
make format # auto-fix style
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The test suite is fully offline: provider calls are replaced by a recorded
|
|
194
|
+
stub at the LiteLLM boundary; live API calls never run in CI.
|
|
195
|
+
|
|
196
|
+
## Limitations (by design)
|
|
197
|
+
|
|
198
|
+
- No streaming yet — TTFT is reported as unavailable rather than faked; latency and throughput are end-to-end.
|
|
199
|
+
- One prompt template per case; no few-shot templating or conversation history.
|
|
200
|
+
- Evaluators are deterministic heuristics; LLM-as-judge scoring is not included.
|
|
201
|
+
- Pricing comes from your registry, not a live price feed — keep it current.
|
|
202
|
+
|
|
203
|
+
See [`docs/ROADMAP.md`](docs/ROADMAP.md) for where this is heading.
|
|
204
|
+
|
|
205
|
+
## License
|
|
206
|
+
|
|
207
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Fast, provider-agnostic LLM evaluation toolkit."""
|
|
2
|
+
|
|
3
|
+
from .config import SUPPORTED_PROVIDERS, ModelSpec, RunConfig
|
|
4
|
+
from .exceptions import ConfigError, FastEvalError, ProviderError, StructuredOutputError
|
|
5
|
+
from .models import ModelResponse, RunResult
|
|
6
|
+
from .registry import load_registry
|
|
7
|
+
from .report import save_report
|
|
8
|
+
from .runner import run
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"SUPPORTED_PROVIDERS",
|
|
14
|
+
"ConfigError",
|
|
15
|
+
"FastEvalError",
|
|
16
|
+
"ModelResponse",
|
|
17
|
+
"ModelSpec",
|
|
18
|
+
"ProviderError",
|
|
19
|
+
"RunConfig",
|
|
20
|
+
"RunResult",
|
|
21
|
+
"StructuredOutputError",
|
|
22
|
+
"__version__",
|
|
23
|
+
"load_registry",
|
|
24
|
+
"run",
|
|
25
|
+
"save_report",
|
|
26
|
+
]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Command-line interface for fastevals."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .config import DEFAULT_MAX_CONCURRENCY, SUPPORTED_PROVIDERS, RunConfig
|
|
10
|
+
from .exceptions import FastEvalError
|
|
11
|
+
from .report import save_report
|
|
12
|
+
from .runner import run
|
|
13
|
+
from .structured import shorthand_to_schema
|
|
14
|
+
|
|
15
|
+
ALL_PROVIDERS = "all"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _dotenv_candidates() -> list[Path]:
|
|
19
|
+
return [Path.cwd() / ".env", Path(__file__).resolve().parents[1] / ".env"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _load_dotenv() -> None:
|
|
23
|
+
"""Load simple KEY=VALUE entries from a project .env if present."""
|
|
24
|
+
for env_path in _dotenv_candidates():
|
|
25
|
+
if not env_path.exists():
|
|
26
|
+
continue
|
|
27
|
+
for raw_line in env_path.read_text().splitlines():
|
|
28
|
+
line = raw_line.strip()
|
|
29
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
30
|
+
continue
|
|
31
|
+
key, value = line.split("=", 1)
|
|
32
|
+
key = key.strip()
|
|
33
|
+
value = value.strip().strip('"').strip("'")
|
|
34
|
+
if key and value and key not in os.environ:
|
|
35
|
+
os.environ[key] = value
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _parse_providers(raw: str) -> frozenset[str]:
|
|
39
|
+
providers = {item.strip().lower() for item in raw.split("|") if item.strip()}
|
|
40
|
+
unknown = sorted(providers - set(SUPPORTED_PROVIDERS) - {ALL_PROVIDERS})
|
|
41
|
+
if unknown:
|
|
42
|
+
supported = ", ".join((*SUPPORTED_PROVIDERS, ALL_PROVIDERS))
|
|
43
|
+
raise argparse.ArgumentTypeError(f"Unknown provider(s): {', '.join(unknown)}. Supported: {supported}")
|
|
44
|
+
return frozenset(providers)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
48
|
+
parser = argparse.ArgumentParser(
|
|
49
|
+
prog="fastevals",
|
|
50
|
+
description="Compare one task results across LLM providers and models.",
|
|
51
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
52
|
+
epilog="""Examples:
|
|
53
|
+
fastevals --prompt \"Summarize this\" --providers \"openai|gemini\" --out runs
|
|
54
|
+
fastevals --image image.png --prompt \"Find widget bboxes\" \\
|
|
55
|
+
--structured-output \"x:int(X coord),y:int(Y coord),width:int(Width),height:int(Height)\" \\
|
|
56
|
+
--providers openai
|
|
57
|
+
fastevals --dataset cases.jsonl --nruns 3 --providers openai --out runs/dataset
|
|
58
|
+
|
|
59
|
+
""",
|
|
60
|
+
)
|
|
61
|
+
parser.add_argument("-p", "--prompt", help="Task prompt (omit when --dataset provides the prompts)")
|
|
62
|
+
parser.add_argument("-s", "--structured-output", help="Structured output compact schema for the response")
|
|
63
|
+
parser.add_argument("-f", "--file", type=Path, help="Input document (sent to the model as an attachment)")
|
|
64
|
+
parser.add_argument("-i", "--image", type=Path, help="Input image")
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"-pr",
|
|
67
|
+
"--providers",
|
|
68
|
+
type=_parse_providers,
|
|
69
|
+
default=frozenset({ALL_PROVIDERS}),
|
|
70
|
+
help=f"Pipe-separated providers: {'|'.join(SUPPORTED_PROVIDERS)}|all (default: all)",
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"-r", "--registry", type=Path, help="Path to the model registry TOML (default: config/models.toml)"
|
|
74
|
+
)
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"-d",
|
|
77
|
+
"--dataset",
|
|
78
|
+
type=Path,
|
|
79
|
+
help="JSONL or CSV file with evaluation cases (columns: prompt, expected, evaluator, pattern)",
|
|
80
|
+
)
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"-n", "--nruns", type=int, default=1, help="Repeat every case this many times for consistency checks"
|
|
83
|
+
)
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
"-c", "--concurrency", type=int, default=DEFAULT_MAX_CONCURRENCY, help="Max parallel model calls"
|
|
86
|
+
)
|
|
87
|
+
parser.add_argument("-o", "--out", type=Path, default=Path("runs"), help="Output directory")
|
|
88
|
+
return parser
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main(argv: list[str] | None = None) -> int:
|
|
92
|
+
_load_dotenv()
|
|
93
|
+
args = build_parser().parse_args(argv)
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
config = RunConfig(
|
|
97
|
+
prompt=args.prompt or "",
|
|
98
|
+
providers=args.providers,
|
|
99
|
+
file=str(args.file) if args.file else None,
|
|
100
|
+
image=str(args.image) if args.image else None,
|
|
101
|
+
structured_output=shorthand_to_schema(args.structured_output) if args.structured_output else None,
|
|
102
|
+
dataset=str(args.dataset) if args.dataset else None,
|
|
103
|
+
nruns=max(1, args.nruns),
|
|
104
|
+
registry=str(args.registry) if args.registry else None,
|
|
105
|
+
max_concurrency=max(1, args.concurrency),
|
|
106
|
+
out=str(args.out),
|
|
107
|
+
)
|
|
108
|
+
results = asyncio.run(run(config))
|
|
109
|
+
except (FastEvalError, ValueError) as exc:
|
|
110
|
+
print(json.dumps({"ok": False, "error": str(exc), "results": []}, ensure_ascii=False))
|
|
111
|
+
return 1
|
|
112
|
+
|
|
113
|
+
json_path, html_path = save_report(config, results, args.out)
|
|
114
|
+
payload = {
|
|
115
|
+
"ok": all(row.ok for row in results),
|
|
116
|
+
"json_path": str(json_path),
|
|
117
|
+
"html_path": str(html_path) if html_path else None,
|
|
118
|
+
"results": [row.as_dict() for row in results],
|
|
119
|
+
}
|
|
120
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
121
|
+
return 0 if payload["ok"] else 1
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
raise SystemExit(main())
|