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.
Files changed (35) hide show
  1. fastevals-0.1.0/LICENSE +21 -0
  2. fastevals-0.1.0/PKG-INFO +244 -0
  3. fastevals-0.1.0/README.md +207 -0
  4. fastevals-0.1.0/fastevals/__init__.py +26 -0
  5. fastevals-0.1.0/fastevals/cli.py +125 -0
  6. fastevals-0.1.0/fastevals/config.py +130 -0
  7. fastevals-0.1.0/fastevals/dataset.py +74 -0
  8. fastevals-0.1.0/fastevals/evaluators.py +54 -0
  9. fastevals-0.1.0/fastevals/exceptions.py +19 -0
  10. fastevals-0.1.0/fastevals/mcp_server.py +157 -0
  11. fastevals-0.1.0/fastevals/models.py +80 -0
  12. fastevals-0.1.0/fastevals/pricing.py +54 -0
  13. fastevals-0.1.0/fastevals/providers.py +138 -0
  14. fastevals-0.1.0/fastevals/py.typed +0 -0
  15. fastevals-0.1.0/fastevals/registry.py +75 -0
  16. fastevals-0.1.0/fastevals/report.py +796 -0
  17. fastevals-0.1.0/fastevals/runner.py +104 -0
  18. fastevals-0.1.0/fastevals/structured.py +115 -0
  19. fastevals-0.1.0/fastevals.egg-info/PKG-INFO +244 -0
  20. fastevals-0.1.0/fastevals.egg-info/SOURCES.txt +33 -0
  21. fastevals-0.1.0/fastevals.egg-info/dependency_links.txt +1 -0
  22. fastevals-0.1.0/fastevals.egg-info/entry_points.txt +3 -0
  23. fastevals-0.1.0/fastevals.egg-info/requires.txt +16 -0
  24. fastevals-0.1.0/fastevals.egg-info/top_level.txt +1 -0
  25. fastevals-0.1.0/pyproject.toml +99 -0
  26. fastevals-0.1.0/setup.cfg +4 -0
  27. fastevals-0.1.0/tests/test_attachments.py +64 -0
  28. fastevals-0.1.0/tests/test_cli.py +95 -0
  29. fastevals-0.1.0/tests/test_evaluators.py +112 -0
  30. fastevals-0.1.0/tests/test_mcp_server.py +144 -0
  31. fastevals-0.1.0/tests/test_pricing.py +46 -0
  32. fastevals-0.1.0/tests/test_registry.py +85 -0
  33. fastevals-0.1.0/tests/test_report.py +61 -0
  34. fastevals-0.1.0/tests/test_runner_run.py +199 -0
  35. fastevals-0.1.0/tests/test_structured.py +95 -0
@@ -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.
@@ -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
+ [![CI](https://github.com/semenovdv/fastevals/actions/workflows/ci.yml/badge.svg)](https://github.com/semenovdv/fastevals/actions/workflows/ci.yml)
53
+ ![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13-blue)
54
+ ![Coverage](https://img.shields.io/badge/coverage-89%25-brightgreen)
55
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
56
+ ![mypy](https://img.shields.io/badge/mypy-strict-blue)
57
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](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
+ [![CI](https://github.com/semenovdv/fastevals/actions/workflows/ci.yml/badge.svg)](https://github.com/semenovdv/fastevals/actions/workflows/ci.yml)
16
+ ![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13-blue)
17
+ ![Coverage](https://img.shields.io/badge/coverage-89%25-brightgreen)
18
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
19
+ ![mypy](https://img.shields.io/badge/mypy-strict-blue)
20
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](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())