BenchKit is a Python 3.11 package using a src/ layout. src/benchkit/cli.py parses flags and either launches the TUI or runs headless. engine.py owns benchmark execution (jobs, slices, pause/skip/stop controls, events) and is shared by both front-ends; client.py talks to inference servers, executor.py sandboxes generated code, report.py writes artifacts, and runner.py renders headless progress. The terminal UI lives in src/benchkit/tui/ (app.py, app.tcss, theme.py, widgets.py, screens/), and demo.py provides the offline client behind --demo. Benchmark implementations belong in src/benchkit/benchmarks/, and bundled benchmark data lives in src/benchkit/datasets/ as JSONL files. Runtime outputs are written to results/<timestamp>/ and should not be committed.
uv sync: install and lock project dependencies frompyproject.tomlanduv.lock.uv run benchkit: launch the terminal app against the configured host.uv run benchkit --demo: drive the whole UI offline with fake models - the fastest way to check TUI changes.OLLAMA_HOST=http://localhost:11434 uv run benchkit: run against an explicit local or remote Ollama endpoint.uv run benchkit --headless --models MODEL --benchmarks sanity:5 --verbose: scripted run that prints per-task prompts and responses.uv run benchkit --list: print the benchmark registry with task counts.uv sync --extra browser && uv run playwright install chromium: install the headless browser used by thetreejs-arenasuite.uv run pre-commit install: install the hooks (ruff on commit, pytest on push).uv run ruff check ./uv run ruff format .: lint and format.uv run pre-commit run -a: run every hook against the whole tree.
The package metadata and CLI entrypoint are defined in pyproject.toml; uv build produces the sdist and wheel. Two workflows run on every pull request: .github/workflows/lint.yml (ruff check and format) and .github/workflows/tests.yml (the test suite on Python 3.11-3.13, an offline --demo smoke run and the package build).
Ruff owns formatting and linting; its configuration lives in pyproject.toml (88 columns, py311 target). Run uv run ruff format . rather than hand-wrapping, and fence hand-aligned data tables with # fmt: off / # fmt: on when the layout matters. Use 4-space indentation, type hints, and short module docstrings consistent with the existing codebase. Prefer snake_case for modules and functions, PascalCase for classes, and lowercase benchmark registry keys such as sanity or humaneval. Keep new benchmark code narrow in scope: implement load_tasks(), build_prompt(), and evaluate(), store dataset payloads as JSONL, and register new classes in src/benchkit/benchmarks/__init__.py.
Streaming, parser, and loop-detection behavior is covered by tests/; run it with uv run pytest. Everything else depends on manual validation. Before opening a PR, run uv run benchkit, confirm model discovery and benchmark execution, and inspect generated results.json, results.csv, and results.md for correctness. For scoring or parser changes, validate with a small benchmark slice before running larger suites. TUI changes can be driven without a terminal via Textual's pilot (async with BenchKitApp(demo=True).run_test() as pilot), which also captures SVG screenshots with app.save_screenshot().
See CONTRIBUTING.md for the tooling walkthrough. Recent commits use short, imperative subjects such as Add per-task details and prompt/response to report and Format code and improve CLI selection. Follow that pattern and keep each commit focused on one change. PRs should describe user-visible behavior, list touched benchmark or dataset files, include the command used for validation, and attach a terminal or report snippet when CLI output changes.
Copy .env.example to .env and set OLLAMA_HOST before local runs. Do not commit .env, local caches, or generated results/ artifacts. Treat bundled dataset files as source data: update benchmark logic and dataset contents together when schema or evaluation behavior changes.
Keep the public README short and human-oriented. Put implementation guidance,
edge cases, and agent-facing operational detail here or next to the relevant
code. Treat the CLI's --help, .env.example, and the benchmark registry as
the canonical, version-matched references rather than duplicating large option
tables in the README.
- The TUI moves through Connect, Setup, Run, and Results screens. It supports model and benchmark filters, per-benchmark slices, live task inspection, pause, skip, stop, sortable results, and report drill-down.
--demoexercises the TUI offline, including healthy and deliberately looping traces. Demo mode does not support the Pi harness.--headlessuses the same engine and report pipeline as the TUI.--verboseprints prompts, available reasoning traces, and responses.benchkit historyserves completed benchmark and performance reports from one or more results directories on localhost. It also serves the files inside those run directories, so a run's gallery and HTML report open from the dashboard. Artifact URLs name their results root by index and are resolved and checked to stay inside it; pages the model wrote (pages/) are served with a sandbox CSP so they cannot read anything else the server exposes.benchkit perf MODELprofiles prompt processing, generation speed, time to first token, wall time, and client overhead across configurable contexts.
- Generation streams have no BenchKit token cap.
BENCHKIT_TIMEOUTis the hard per-task deadline; partial traces are retained on timeout, loop kill, stop, and other recoverable terminal states. - Reasoning comes from Ollama
thinking, OpenAI-compatiblereasoning_content, or inline<think>blocks. Providers that hide it must be reported asNO TRACE, not as producing no reasoning. - Loop killing is controlled by
BENCHKIT_LOOP_KILL,BENCHKIT_LOOP_KILL_PERCENT, andBENCHKIT_LOOP_KILL_SECONDS. Detection remains visible when killing is disabled. Only a confirmed, continuously growing suffix cycle is actionable; global repetition and code similarity are advisory. - Transient gateway, rate-limit, DNS, connection, and dropped-stream failures
use configurable exponential backoff. Never replay a generation after it has
emitted tokens. Client errors are not retried. See
.env.exampleandclient.pyfor the current knobs and exact policy.
--harness direct,pi, andbothcompare raw generation with the stock Pi coding agent. Paired scoring uses only items valid on both sides.- Pi requires Docker. Each task gets a fresh persistent
/workspacein an isolated container plus a restricted inference proxy. Do not add host mounts, the Docker socket, direct network egress, hidden answers, or hidden tests to the agent environment. --repair-attempts 1gives an incorrect answer one sanitized verifier message and one full replacement attempt. Feedback must never expose the expected answer or hidden test bodies.aider-polyglotis Pi-only and uses pinned task content and toolchains. It measures the stock Pi protocol on Aider tasks, not Aider's edit formats.--perturbation choice-orderruns supported MCQ tasks clean and with a deterministic permutation whose correct option moves. Perturbed jobs are paired with the baseline and excluded from the overall model score.
treejs-arenahas no ground truth. Frozen prompts ask for one self-contained HTML file each; the file is opened in headless Chromium and the automatic score is binary: no uncaught exception, and a sized canvas that acquired a drawing context. Console output and blocked hosts are recorded and fed back for repair but do not decide the verdict - a scene that draws has rendered even if a library logged a warning. The screenshots are the real output and are meant for human comparison.- The prompt set is versioned (
PROMPT_SET_VERSION). Never edit a shipped prompt in place; add a version so old screenshots stay comparable. - Generated pages are untrusted. Requests are aborted unless the host is on the
module-CDN allowlist (
BENCHKIT_RENDER_ALLOWED_HOSTS, or nothing at all withBENCHKIT_RENDER_OFFLINE), downloads and service workers are blocked, and navigation, settle and capture each run under an explicit deadline. Do not widen the allowlist to make a scene pass, and do not add host mounts or file access to the browser context. --repair-attemptsfeeds the captured console and network diagnostics back as ordinary verifier feedback and re-renders the replacement file.- Headless machines without a browser, without a working WebGL stack (checked
once per process by
browser.probe_environment), or without a route to an allowlisted module CDN are ordinary places to run BenchKit. The render check is skipped there and the task keeps its credit:render_statusisskippedwith askip_reason, never a failure and never a harness error. Only faults inside the page the model wrote are scored failures. Skips are excluded from the gallery's render rate so they cannot inflate it, andbenchkit render-checkreports both preconditions directly. Render rates stay out of the overall average, like RULER. - Every scored task keeps an artifact: the extracted page, or the raw answer when no HTML could be found. Tasks that never reached the browser (timeout, loop kill, output limit) still appear in the gallery with that status, so the denominator is the tasks attempted rather than the ones that got far enough to render.
- Screenshots and generated pages are staged under
results/.artifacts/during the run and collected intoscreenshots/andpages/inside the run directory at save time, with the paths inresults.jsonrewritten relative to the report. - The gallery is its own page,
arena.html(templatetemplates/arena.html), written next to the assets it links to whenever a run has render-scored rows. It carries the pass rate, per-model render rates, and large previews, so it is deliberately not standalone.results.htmlkeeps the numbers and links to the gallery rather than embedding images. - A preview runs the generated page itself, in an iframe sandboxed to
allow-scripts allow-pointer-lock- an opaque origin, because the page is the model's own code - so a scene the benchmark machine could not render still plays in whatever browser opens the gallery. Frames load and unload with an IntersectionObserver and are capped, because browsers keep only a handful of WebGL contexts alive; the captured screenshot sits underneath as the poster and the record of what the run actually saw.Open ↗opens the page in a full tab, and the header toggle turns live previews off.
mc-arenahas no ground truth either, but "it rendered" is deliberately not the score: a block list renders whether it is a watchtower or three blocks of dirt. Frozen prompts ask for one self-contained PEP 723 Python script that prints a JSON array of{x, y, z, block}into a fixed 32x32x32 volume with its origin at (0, 0, 0). The prompt shows a full block id, because without one smaller models answeroak plankand every block fails validation for the wrong reason.- The script is untrusted and runs through
sandbox.run_python_script: a throwaway container with no network, no capabilities, a read-only rootfs and memory, process and time limits. Do not give it network, host mounts, or the answer to anything. ASandboxErroris a harness error, not a wrong answer. - Scoring is deterministic and never uses a judge. Passing means the block list is usable: the script ran, the ids all exist, everything stayed in the volume, the entries were well formed and something was built. Quality is reported rather than scored - block count, palette size and evenness, the dominant block's share, floating (unsupported) fraction, duplicate positions, bounding box and fill. Keep it that way: these numbers are the signal, and a pass rate on its own would say almost nothing.
- Repairs come from the engine's
--repair-attempts, fed by the verifier feedback (a traceback, a parse failure, or the offending ids).mc-arenais excluded from the overall average, like RULER and treejs-arena. - Block ids are validated against
datasets/mc_blocks_1_20_1.jsonl, the same Minecraft version the renderer draws, so a build cannot pass with an id the renderer would silently drop. Update both together. - Rendering is prismarine-viewer, pinned and committed under
mc_viewer/; seemc_viewer/build/README.md. Each build is photographed from three fixed cameras (isometric, side, top-down) at a fixed distance, lens and size, and the rig lives inmc_viewer/build/entry.js. Changing the rig or the prompt set makes old screenshots incomparable - version it, do not edit in place. - The viewer is served over a loopback HTTP server because Chromium refuses to
start a web worker from a
file://page. The page is BenchKit's own code; the model contributes block coordinates, never markup or script. - A machine with no browser or no WebGL is an ordinary place to run mc-arena.
The render is skipped,
render_statusisskippedwith askip_reason, and the build still scores from its block list - the picture is an illustration, never the verdict. - The gallery can also explore a build in 3D:
viewer.html?interactive=1&build=drops the three-camera rig for one full-window canvas with orbit, pan and zoom. The card loads it in place of a page preview, with the screenshot as the poster underneath. Unlike a treejs scene this frame is BenchKit's own code - the model contributed block coordinates, not markup - so it getsallow-scripts allow-same-origin; prismarine-viewer meshes in a web worker and an opaque origin cannot start one. - Interactive viewing only works over HTTP, for the same reason the renderer
runs its own loopback server: browsers refuse to start a worker from a
file://page.benchkit historyserves the viewer straight from the installed package at/viewer/, so it is never copied into a run directory. A gallery opened straight off disk keeps the screenshots and loses only the 3D - never the scores, which never depended on it.
- Request concurrency is detected from server slot endpoints or explicit metadata and is bounded by task count. Model jobs remain sequential; tasks within the active job may run concurrently.
- Pause waits for active requests to drain before withholding new work. Skip, stop, and interrupt cancel in-flight requests.
- Reports distinguish aggregate throughput, per-stream throughput, effective
concurrency, and throughput coverage. Preserve raw timing/token denominators
and the legacy
tok_scompatibility alias when changing report schemas. - Harness errors are excluded from scores and paired comparisons. Ordinary incorrect answers, timeouts, length limits, and loop kills remain scored failures when applicable.
- Every run writes
results.json,results.csv,results.md, and a standaloneresults.html. Performance runs write matchingperf.*artifacts. Keep old reports readable when adding fields; missing historical fields mean “not captured,” not zero. - Benchmark metadata, counts, perturbation support, and descriptions are
canonical in
src/benchkit/benchmarks/__init__.py; inspect it or runuv run benchkit --listinstead of maintaining a second table. - RULER is generated deterministically at runtime and reported by context bucket. It is excluded from overall-score averaging so its degradation curve stays visible.
- EvalPlus datasets may be downloaded and cached on first use. Generated code is untrusted; preserve evaluator guards and process isolation.