Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

BenchLoop is a local-first CLI + web app for benchmarking LLMs running on your own hardware or cloud providers. It scores models across eight repeatable suites — quality, speed, long-context recall, agentic tool use, coding, instruction following — and gives you receipts: per-task outputs, latency, token counts, machine info, scores.

Local runs need no account or API key; cloud providers use standard OpenAI-compatible auth. Publishing is optional (`BENCHLOOP_NO_SUBMIT=1`). Your model, your machine (or your provider), your numbers.
Local runs need no account or BenchLoop API key; cloud model providers use standard OpenAI-compatible auth. Results stay on your machine unless you explicitly publish them through a paired BenchLoop account. Your model, your machine (or your provider), your numbers.

```
$ benchloop run --model qwen3:8b --suites speed,toolcall,agent
Expand All @@ -26,7 +26,7 @@ Speed 78.9 █████████░
Agent 96.9 █████████▌
```

Published runs live at <https://bench-loop.com/leaderboard>. Every completed local benchmark auto-publishes there.
Published runs live at <https://bench-loop.com/leaderboard>. Local benchmark data is private by default.
## Why

Hosted LLM leaderboards answer *"which model wins on a server farm someone else paid for?"* BenchLoop answers *"which model + harness + hardware combination actually works for me right now?"* — the question you have when picking a local stack.
Expand Down Expand Up @@ -243,20 +243,32 @@ Tabs: Models, Benchmark, Leaderboard, Compare runs, Chat, agent trace viewer.

## Publish a run

By default, every completed benchmark publishes to <https://bench-loop.com/leaderboard> via `https://api.bench-loop.com/submit`. Runs are deduped by `(machine_id, run_id)` so the same run from the same machine won't be double-counted.
Pair each Runner once through GitHub in your browser. The scoped device token is stored in the operating-system keychain—not in the run JSON or shell history:

Opt out:
```bash
benchloop auth login
benchloop auth status
```

Runs remain local until you choose to publish one:

```bash
export BENCHLOOP_NO_SUBMIT=1
benchloop publish # latest captured v3 run
benchloop publish <run-id> # a named run under ~/.bench-loop/runs
benchloop publish ./path/to/run.json # an explicit file
benchloop run --model qwen3:8b --publish # publish after a successful run
```

You can still manually export a snapshot for sharing / archiving:
Use `--visibility public` or `private`, and `--no-post` if the run should not create a feed post. Only v3 runs with complete provenance and a SHA-256 manifest can use authenticated publishing.

You can also export a local snapshot for sharing or archiving without pairing:

```bash
benchloop export --output my-runs.json
```

Operators temporarily depending on the pre-account submission endpoint can opt into it with `BENCHLOOP_LEGACY_AUTO_SUBMIT=1`. It is disabled by default and will be removed after the authenticated ingestion path is fully deployed.

## Architecture

```
Expand Down
119 changes: 117 additions & 2 deletions bench_loop/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

import click

from bench_loop import __version__
from bench_loop import __version__, cloud
from bench_loop.benchmark_manifest import DEFAULT_PROFILE, PROFILES, classify_suites
from bench_loop.harness import list_harnesses
from bench_loop.report.console import print_run_report
Expand Down Expand Up @@ -67,6 +67,66 @@ def info() -> None:
click.echo(f" {harness_name}")


@main.group("auth", help="Pair this Runner with your BenchLoop account.")
def auth_group() -> None:
"""Manage the Runner's scoped BenchLoop device credential."""


@auth_group.command("login")
@click.option("--api-url", default=None, help="BenchLoop API base URL.")
@click.option(
"--device-name", default=None, help="Name shown for this Runner in your lab."
)
@click.option(
"--no-browser", is_flag=True, help="Print the verification URL without opening it."
)
def auth_login(api_url: str | None, device_name: str | None, no_browser: bool) -> None:
"""Pair this machine through a short-lived browser code."""
try:
pairing = cloud.start_pairing(base_url=api_url, device_name=device_name)
click.echo("\nPair this BenchLoop Runner:")
click.secho(f" {pairing.user_code}", fg="green", bold=True)
click.echo(f" {pairing.verification_uri}")
if not no_browser:
cloud.open_verification_page(pairing)
click.echo("\nWaiting for approval", nl=False)
account = cloud.await_pairing(
pairing,
base_url=api_url,
on_wait=lambda: click.echo(".", nl=False),
)
except cloud.CloudError as exc:
raise click.ClickException(str(exc)) from exc
click.secho(f"\n✓ Paired as @{account.handle}", fg="green")
click.echo("The scoped Runner token is stored in your operating-system keychain.")


@auth_group.command("status")
def auth_status() -> None:
"""Show the locally paired BenchLoop account."""
try:
account = cloud.load_account()
token = cloud.load_token(account.api_url if account else None)
except cloud.CloudError as exc:
raise click.ClickException(str(exc)) from exc
if not account or not token:
click.echo("Not paired. Run `benchloop auth login`.")
return
click.echo(f"Paired account: @{account.handle}")
click.echo(f"Runner device: {account.device_id}")
click.echo(f"API: {account.api_url}")


@auth_group.command("logout")
def auth_logout() -> None:
"""Remove this Runner's local credential."""
try:
cloud.clear_account()
except cloud.CloudError as exc:
raise click.ClickException(str(exc)) from exc
click.echo("Runner credential removed from this machine.")


@main.command()
@click.option("--model", required=True, help="Model name to benchmark.")
@click.option(
Expand Down Expand Up @@ -171,6 +231,12 @@ def info() -> None:
"when benchmarking a reasoning model to rule that out before trusting a low score."
),
)
@click.option(
"--publish/--no-publish",
"publish_result",
default=False,
help="Publish this completed run through your paired BenchLoop account.",
)
def run(
model: str,
endpoint: str,
Expand All @@ -190,6 +256,7 @@ def run(
force_local: bool,
api_key: str | None,
max_tokens: int | None,
publish_result: bool,
) -> None:
"""Run a benchmark."""
# Set API key from CLI flag if provided (takes precedence over env var)
Expand Down Expand Up @@ -394,12 +461,23 @@ def on_progress(event: dict) -> None:
raise SystemExit(1)
# Save before printing -- a console-rendering crash (e.g. legacy Windows
# cp1252 terminals choking on an emoji) must not cost the whole run's data.
save_run(
output_path = save_run(
benchmark,
endpoint=endpoint,
publish_profile=publish_profile,
command_used=command_used,
)
if publish_result:
try:
published = cloud.publish_run(output_path)
url = published.get("url") or "https://bench-loop.com/runs"
click.secho(f"✓ Published: {url}", fg="green")
except cloud.CloudError as exc:
click.secho(
f"Publish failed: {exc}\nThe benchmark remains saved locally at {output_path}",
fg="yellow",
err=True,
)
try:
print_run_report(benchmark)
except Exception as exc: # noqa: BLE001
Expand All @@ -414,6 +492,43 @@ def suites() -> None:
click.echo(f"{summary.name}: {summary.task_count} tasks")


@main.command("publish")
@click.argument("run_ref", required=False)
@click.option("--api-url", default=None, help="BenchLoop API base URL.")
@click.option(
"--visibility",
type=click.Choice(["public", "private"]),
default="public",
show_default=True,
)
@click.option(
"--post/--no-post",
"create_post",
default=True,
help="Add the run to your builder feed.",
)
def publish_command(
run_ref: str | None,
api_url: str | None,
visibility: str,
create_post: bool,
) -> None:
"""Publish a captured v3 run by path, run ID, or latest when omitted."""
try:
path = cloud.resolve_run_path(run_ref, RUNS_DIR)
result = cloud.publish_run(
path,
base_url=api_url,
visibility=visibility,
create_post=create_post,
)
except cloud.CloudError as exc:
raise click.ClickException(str(exc)) from exc
url = result.get("url") or "https://bench-loop.com/runs"
click.secho(f"✓ Published {path.parent.name}", fg="green")
click.echo(url)


@main.command()
@click.option(
"--output",
Expand Down
Loading
Loading