Harbor Benchmarks
August 18, 2026 ยท View on GitHub
Use this guide to run Harbor Terminal-Bench Lite from a fresh Switchyard clone. It covers the two smoke paths most people need first:
- Direct upstream: Harbor calls the provider directly. Switchyard is disabled.
- Switchyard routing: Harbor calls Switchyard, and Switchyard routes across two model tiers.
Both paths use the same generated dataset, task proxy, pinned agent versions, and run artifact
layout. Passing --server-config starts the Rust server; omitting it disables Switchyard and points
Harbor directly at the upstream provider.
Prerequisites
From the repo root:
uv sync
You also need Docker with Compose support, because baseline runs launch task containers and use the
generated benchmark proxy topology. Runs with --server-config also start Switchyard inside
Docker.
Harbor is installed as a dev dependency. Check that the CLI resolves from the uv environment:
uv run --no-sync harbor --help
Configure Your Provider
The checked-in smoke commands use OpenRouter's OpenAI-compatible endpoint by default:
export OPENROUTER_API_KEY="..."
To use another OpenAI-compatible provider, either export a generic upstream key:
export UPSTREAM_API_KEY="..."
export UPSTREAM_BASE_URL="https://provider.example/v1"
or pass the provider-specific key variable explicitly:
bash benchmark/run-baseline.sh \
--upstream-base-url https://provider.example/v1 \
--upstream-api-key-env PROVIDER_API_KEY \
...
Rust server TOML files refer to the credential through api_key_env. For another provider, copy a
config and update its api_key_env, base_url, and model ids to match that provider.
One-Time Setup
run-baseline.sh has a blanket preflight check for the current patch file. It reverse-checks the
exact diff against the installed Harbor tree, so stale or partial patch applications fail before
launching Harbor. Apply the patch to the current uv environment:
REPO_ROOT="$(git rev-parse --show-toplevel)"
HARBOR_SITE="$(
cd "$REPO_ROOT"
uv run --no-sync python - <<'PY'
import sysconfig
print(sysconfig.get_paths()["purelib"])
PY
)"
cd "$HARBOR_SITE"
patch -p1 < "$REPO_ROOT/benchmark/patches/harbor-agent-patches.diff"
cd "$REPO_ROOT"
Reapply this after recreating the virtualenv, reinstalling Harbor, or running a forced dependency reinstall.
The generated dataset is local build output and is not committed. This command downloads and exports
openthoughts-tblite@2.0, prebakes pinned agent versions into each task image, injects the
benchmark proxy, and writes switchyard_dataset_manifest.json:
uv run --no-sync python benchmark/prepare_harbor_dataset.py --overwrite
Default output:
benchmark/datasets/openthoughts-tblite-closed-book
To reuse an already exported Harbor dataset instead of downloading again:
uv run --no-sync python benchmark/prepare_harbor_dataset.py \
--source-dir /path/to/exported/openthoughts-tblite \
--overwrite
The pinned versions live in benchmark/agent-versions.env. To prepare a different Harbor dataset,
see Benchmark Datasets.
Terminal-Bench 2.0 is supported through the same generated local proxy dataset path. The TB2 export keeps model/tool egress on the closed-book path while allowlisting the package and data sources required by the official Oracle solutions.
uv run --no-sync python benchmark/prepare_harbor_dataset.py \
--source-dataset terminal-bench/terminal-bench-2 \
--output-dir benchmark/datasets/terminal-bench-2-closed-book \
--overwrite
Terminal-Bench 2.1 (the verified iteration of 2.0) is supported the same way and shares the 2.0 Oracle allowlist:
uv run --no-sync python benchmark/prepare_harbor_dataset.py \
--source-dataset terminal-bench/terminal-bench-2-1 \
--output-dir benchmark/datasets/terminal-bench-2-1-closed-book \
--overwrite
SWE-Bench Pro is supported with the Harbor dataset cais/swebenchpro. The generated dataset uses
the same pinned-agent and closed-book proxy path without opening dataset-specific agent egress.
uv run --no-sync python benchmark/prepare_harbor_dataset.py \
--source-dataset cais/swebenchpro \
--output-dir benchmark/datasets/swebenchpro-closed-book \
--overwrite
Run Without Switchyard
Omit --server-config to fully disable Switchyard. The runner still creates the benchmark
Docker network for the generated proxy sidecar, but Harbor sends model calls straight to
${UPSTREAM_BASE_URL:-https://openrouter.ai/api/v1} using OPENROUTER_API_KEY by default:
bash benchmark/run-baseline.sh \
--harbor-path benchmark/datasets/openthoughts-tblite-closed-book \
--model openai/gpt-5.5 \
--agent codex \
--reasoning-effort xhigh \
--n-tasks 1 \
--n-concurrent 1 \
--max-retries 0
For another OpenAI-compatible upstream, pass --upstream-base-url and
--upstream-api-key-env. Claude Code direct runs require an Anthropic-compatible upstream because
Switchyard translation is disabled.
Run With Switchyard Routing
Pass --server-config to start switchyard-server and route Harbor traffic through it. This smoke
test uses benchmark/server-configs/tb-lite-llm-classifier-opus-kimi-gemini.toml, a Rust
task-classifier configuration for coding-agent tasks:
bash benchmark/run-baseline.sh \
--harbor-path benchmark/datasets/openthoughts-tblite-closed-book \
--server-config benchmark/server-configs/tb-lite-llm-classifier-opus-kimi-gemini.toml \
--model switchyard \
--agent codex \
--reasoning-effort xhigh \
--n-tasks 1 \
--n-concurrent 1 \
--max-retries 0
Use the route id from the TOML as --model. In this config, the Gemini classifier selects the
target tier for the task, then Switchyard routes to one of:
- strong:
anthropic/claude-opus-4.7 - weak:
moonshotai/kimi-k2.7-code
Classifier model: google/gemini-3.5-flash.
To smoke-test a single-model Switchyard path instead, use one of:
benchmark/server-configs/tb-lite-single-gpt-5-5.toml
benchmark/server-configs/tb-lite-single-opus-4-7.toml
By default, the runner starts in the background and prints the PID, log path, and kill command.
Book Modes
Both book modes use the same generated --harbor-path dataset, prebaked agent images, and proxy
sidecar topology. Switchyard is Dockerized only when --server-config is provided.
Closed-book mode is the default:
bash benchmark/run-baseline.sh \
--harbor-path benchmark/datasets/openthoughts-tblite-closed-book \
--server-config benchmark/server-configs/tb-lite-llm-classifier-opus-kimi-gemini.toml \
--model switchyard \
--agent codex \
--n-tasks 1
In closed-book mode, the proxy allows Switchyard/model traffic, blocks public cheat sources such as
raw.githubusercontent.com, strips hosted web/search/code tools from model API payloads, and adds
agent-specific web-disable settings where supported.
Open-book mode keeps the same proxy path but broadens egress:
bash benchmark/run-baseline.sh \
--book-mode open \
--harbor-path benchmark/datasets/openthoughts-tblite-closed-book \
--server-config benchmark/server-configs/tb-lite-llm-classifier-opus-kimi-gemini.toml \
--model switchyard \
--agent codex \
--n-tasks 1
Use open-book mode only when the evaluation intentionally allows internet access. The manifest records the mode, the local dataset digest, a snapshot of the server config, proxy metadata, upstream base URL for direct runs, and agent version pins in both modes.
Run A Full TB Lite Pass
After the smoke test succeeds, remove --n-tasks 1, raise concurrency to match your host and
provider quota, and let the runner use the background wrapper.
Direct upstream:
bash benchmark/run-baseline.sh \
--harbor-path benchmark/datasets/openthoughts-tblite-closed-book \
--model openai/gpt-5.5 \
--agent codex \
--reasoning-effort xhigh \
--n-concurrent 8 \
--max-retries 2
Switchyard LLM-classifier routing:
bash benchmark/run-baseline.sh \
--harbor-path benchmark/datasets/openthoughts-tblite-closed-book \
--server-config benchmark/server-configs/tb-lite-llm-classifier-opus-kimi-gemini.toml \
--model switchyard \
--agent codex \
--reasoning-effort xhigh \
--n-concurrent 8 \
--max-retries 2
Tune --n-concurrent for your machine and provider quota. Use --task-id, --task-list-file, or
--n-tasks for subsets.
Inspect A Run
Run directories are created under benchmark/tb_runs/. The most useful artifacts are:
run_manifest.json
server.log
harbor.log
server_metrics_final.prom
routing_stats_final.json
jobs/<job-name>/result.json
jobs/<job-name>/<task-id>/agent/trajectory.json
The manifest records the command, git state, Harbor patch provenance, local dataset digest, copied server config, direct-upstream metadata when Switchyard is disabled, book-mode settings, agent version pins, log paths, and final Harbor status.
server_metrics_final.prom is the final /metrics snapshot. routing_stats_final.json is the
final aggregate /v1/stats snapshot, including model and tier calls, errors, tokens, and latency.
Neither artifact provides task or trial attribution. The runner writes them only after Harbor exits
and while the Rust server is still reachable; otherwise the manifest records them as missing.
routing_requests.jsonl and routing_stats_by_task.json are not produced by the Rust server.
Docker Image Notes
Baseline runs build switchyard-baseline:local from
the repository-root Dockerfile.
The default is to rebuild before each run so the container matches the current checkout.
To reuse an already built image:
SWITCHYARD_DOCKER_BUILD=0 bash benchmark/run-baseline.sh ...
Only reuse the image when you know it already contains the current Rust switchyard-server binary.
Troubleshooting
If the runner reports that the current Harbor patch is not applied cleanly, recreate or reinstall the uv environment and rerun the patch command from this README.
If port 4000 is busy, pass a different port:
bash benchmark/run-baseline.sh ... --port 4001
If the Docker reachability preflight fails, check Docker/Compose first. The preflight proves the
task container can reach Switchyard through the benchmark Docker network.
For local debugging only, it can be bypassed with SWITCHYARD_CLOSED_BOOK_PREFLIGHT=0.