Skip to main content
Adding a benchmark is the primary way to extend Clousight Bench — for open-source and commercial suites (a commercial suite ships the same entry points from a private package). You implement two classes and register two entry points; no core change.
Copy an existing self-contained suite as your template: src/clousight_bench/suites/gsm8k/ (bundled fixture + evaluator + a tiny suite). SWE-bench is the most complete example but drags in Docker/HF — start from GSM8K.

The two contracts

  • BenchmarkSuite produces raw artifacts: resolve (pick the dataset, offline) → prepare (connect/provision) → run (produce a RawArtifacts dir+manifest) → teardown; plus mock_artifacts (the offline path CI uses).
  • Evaluator scores those artifacts into namespaced Measurements — a pure, offline function (no cloud, no credentials), so a stored run is re-scorable.
Import both from the stable surface — clousight_bench (or clousight_bench.api), never clousight_bench.core.*.

A minimal suite

Register + run

conformance enforces the contract: an official evaluator may only emit keys under <suite_id>., all official=True. That’s what makes a number trustworthy.

Reporting progress

A suite that runs for thirty minutes writes nothing observable until it finishes. driver.progress is where it can say what it is doing, so csbench serve can show a live view of the run. It defaults to an inert reporter (NULL_PROGRESS), so doing nothing is a valid choice: a suite written against plugin API 3.0, which never saw this field, is unaffected, and a unit test needs no wiring. Every call below costs a function call and returns on a suite that is not being watched. Six methods (core/progress.py::ProgressReporter):
phase starts a named unit of work (total=0 when the size is unknown); advance ticks through it; step records a finished interval so it can be drawn on the live waterfall; sample publishes an in-flight observation that nothing scored is ever derived from; log adds one line to the log pane (this package’s own log records are mirrored there automatically). Step timings are milliseconds from one origin per run. Use clousight_bench.suites._progress.StepClock so the steps emitted from prepare() and the ones emitted from run() share a frame — and build them from the marks a measurement already took, never from a second, disagreeing timing.

Never report from inside a measured window

A progress write is a file append: cheap, but not free. If a region’s own wall clock feeds a measurement, write nothing inside it. Per-query reporting is usually safe, because the metric is built from intervals measured tightly around each query and the report lands in the gap between two of them — which nothing measures. That is exactly how the DuckDB TPC suites do it (suites/_duckdb_tpc.py):
The TPC official-mode Throughput test is the counter-example: its elapsed_s is the metric (Throughput@Size, the T_TT term). Anything written inside that window would put the reporter’s I/O into the number. So the phase declares up front that it cannot tick, only the cancel poll runs inside, and the per-stream, per-query steps are replayed from the intervals the phase already measured once the window has closed (suites/_tpc_official/phases.py, _progress.TpcProgress.replay_throughput):
reports_progress=False means “I know my size but cannot tick through it”. The viewer then draws an indeterminate bar and says why, instead of a 0/396 bar that reads as a hang.

Name steps the way the sealed trajectory names them

The live waterfall and the sealed waterfall must call the same thing the same name, or two views of one run look like two runs. If your suite writes a trajectory artifact (or reconstructs spans from artifacts, as _tpc_official/trace.py does), use those span names verbatim for step(). TpcProgress.name() exists for exactly this: it builds <suite_id>.<leaf> so both sides derive the name from one place.

Cancelling: raise, never return

Poll should_cancel() in any loop that can run for minutes. It is a stat cached for 250 ms, so a per-item poll is fine. When it fires, raise. raise_if_cancelled(progress, where) raises RunCancelled, a KeyboardInterrupt subclass, which the orchestrator already handles correctly: teardown runs, an interrupted record is persisted, nothing provisioned is orphaned.
Never return early on a cancel. A truncated query set that returned normally would reach the evaluator looking like a finished run, and would be scored as one.

Going further (optional)

  • Per-item scores — implement Evaluator.items(raw) -> list[ItemResult] to emit per-example evidence; scalar measurements become their aggregation (with confidence intervals + per-group breakdowns). See Architecture.
  • Add-on metrics — bind extra metrics with extra_metric_ids (or a run’s params.extra_metrics); they score over your items and emit <suite_id>.<metric_id>.
  • Judge-based scoring — add the response-quality metric (or your own) and config-connect a judge via params.judge.
  • Config-connect a real SUT — read target.endpoint / target.credentials_ref in prepare/run; for LLM suites reuse clousight_bench.suites.llm_common (resolve_endpoint, chat_once, SSRF-guarded).