Setup SWE-Duel on your local server.
Clone the repository and install it into a virtual environment.
git clone https://github.com/sweduel/SWE-Duel.git cd SWE-Duel python3.12 -m venv venv ./venv/bin/pip install -e ".[dev]" # or: make install source venv/bin/activate # puts the swe-duel-* commands on PATH
export SWE_DUEL_OPENROUTER_API_KEY="sk-or-..."
| Path | Purpose |
|---|---|
config/models.yaml | Model registry for specifying OpenRouter model_id, temperature, the selectable reasoning-effort and provider menus, and per-provider rate limits. |
config/repos/*.yaml | Each file contains repository URL, commit hash, Docker image name, and test commands for the respective repository. |
config/arena.yaml | Agent time/step budgets, and match configurations (match.turns_per_player, match.max_workers), arena-wide agent_model.max_tokens, and paths.output_dir. |
repos/ | Clones of the target repositories (created in step 03). |
data/ | All run artifacts: challenge_bank/, defenses/, matches/, tournaments/, workspaces/, logs/. Relocate with --output-dir, SWE_DUEL_OUTPUT_DIR, or paths.output_dir. |
rankings/ | Exported standings, rankings_<tournament_id>.json (step 08). |
Clone every repository listed in config/repos/ at its pinned commit, then build the
swe-duel-base, swe-duel-mock, and swe-duel-<repo> images. A full build takes a while; use --only to start with a subset.
swe-duel setup repos # → ./repos/<name> at pinned commits swe-duel setup docker # all images (add --force to rebuild) # or start small: swe-duel setup repos --only flask jinja chi swe-duel setup docker --only flask jinja chi
swe-duel doctor command test-runs the arena's real code paths (install, config,
Docker daemon, images, clones, an offline test run, the deterministic scorer, and a replay of sealed
gate-regression fixtures) to ensure that the environment is ready to start generating tasks and running tournaments.
swe-duel doctor # host + Docker stages, no API spend swe-duel doctor --all-repos # replay every repo's sealed gate fixture (slower) swe-duel doctor --live # + one tiny OpenRouter call per configured model swe-duel doctor --live --smoke-agent glm-5.3-flash # + one real agent session
Each of the swe-duel doctor ... command fails fast on a missing Docker daemon,
a missing swe-duel-<repo> image, or a missing clone, and suggest the exact
swe-duel setup … command that fixes it.
A participant is the 4-tuple (model, harness, reasoning effort, provider). Thus, the same model under a different harness, at a different effort, or served by a different OpenRouter provider counts as a distinct participant.
config/models.yamlmodels: deepseek-v4-flash-0731: # nickname displayed on the CLI model_id: "deepseek/deepseek-v4-flash-0731" temperature: 0.2 reasoning_efforts: ["max", "high", "low"] # will become selectable on CLI providers: ["deepseek", "wafer/fast", "fireworks"] # will become selectable on CLI provider_rate_limits: # requests/minute, shared by all workers "deepseek": 60 "wafer/fast": 60 "fireworks": 40 "default": 6 # auto-route / unlisted providers
No per-token prices are statically configured in the config/models.yaml.
SWE-Duel tracks cost by reading from the per-response metadata for accurate OpenRouter cost reports.
You may add additional OpenRouter providers, and probe their rate limits with real API calls:
swe-duel-probe-rate-limits --models deepseek-v4-flash-0731 --providers fireworks
| Harness ID | Runtime | Effort Level Support | Provider Pin Support |
|---|---|---|---|
mini-swe-agent | mini-swe-agent, docker exec into the repo image | any string in the model's menu | yes |
openhands | OpenHands SDK agent-server | any string in the model's menu | yes |
codex | Codex CLI (codex exec) | minimal · low · medium · high · xhigh | no |
claude-code | Claude Code CLI (claude -p) | no | no |
In order for participant to enter a tournament or run a match against another participant,
it should generate Red tasks (interchangeably, challenges) for every repository involved in the
match or tournament.
Admitted tasks are stored under data/challenge_bank/.
# interactive wizard (needs a TTY)
swe-duel-generate \
--repos jwt helmet java-html-sanitizer cjson flask jinja expressjs java-jwt libexpat chi sqlalchemy simdjson jjwt node-jsonwebtoken csrf \
--target-per-repo 1 --target-per-repo argument maps to the tournament's --turns_per_player argument
(match.turns_per_player in arena.yaml, default 3). Each match draws that many
challenges per repository per side, in deterministic bank order.1..target). Re-running skips every slot that already has a
record (successfully admitted, or failed after challenge_bank.max_generation_attempts attempts), so the
command is resume-safe and never re-spends on a finished/exhuasted slot.challenge_bank.max_generation_attempts failed attempts will not manifest as error during a match or tournament.
Rather, in a match, the opponent is awarded an auto-win that Blue turn.The round-robin tournament schedules a match between every two participants in the tournament.
A single match spans all selected repositories: the two participants alternate Red and Blue for
turns_per_player challenges per repository per side. Blue defenses are cached in
data/defenses/ per (challenge, Blue participant), so later tournaments reuse the cached
records to save costs.
# interactive wizard (needs a TTY)
swe-duel-tournament-rr \
--repos jwt helmet java-html-sanitizer cjson flask jinja expressjs java-jwt libexpat chi sqlalchemy simdjson jjwt node-jsonwebtoken csrf \
--turns-per-player 1On start the console UI lists existing round_robin_state_*.json files for the same
repository set and turns-per-player configuration, so you can resume a tournament or start a new one.
Commands for navigating the console UI:
| Command | Effect |
|---|---|
status (s) | Tournament overview, including the Tournament ID you need in step 08. |
pairings (p) | Preview the next round's pairings with cached-defense reuse counts. |
next (n) | Run the next scheduled round under the live progress view. |
all (a) | Run every remaining match in one parallel batch. |
standings (ls) | Cumulative points + Elo leaderboard. |
history (h) / history N | Past rounds with outcomes / the Elo table after round N. |
report (r) | Regenerate report figures. |
quit (q) | Exit (a final report is produced on the way out). |
State persists to data/tournaments/round_robin_state_<tournament_id>.json and match records
to data/matches/, so an interrupted tournament resumes exactly where it stopped. After resuming,
the console also offers Add a new participant: newcomers are onboarded with ceil(log2 N)+1 matchups
against the incumbents via the active sampling algorithm, instead of a matching against the entire incumbent pool.
swe-duel-tournament --repos … runs the same REPL but
re-pairs every round by score (state in swiss_state_*.json), and re-uses match/defense records from previous tournament(s) to save costs.
Once the matches of the tournament are played, export the tournament's standings from ./data/.
ls data/tournaments/ # round_robin_state_<tournament_id>.json swe-duel-rankings <tournament_id> # Will output to ./rankings/rankings_<tournament_id>.json (override with --rankings-dir)
The export is self-contained:
rankings: pinned identity (model, harness, effort, provider), points,
matches played, match-level Bradley–Terry and Elo with ranks, TrueSkill μ/σ, and 1,000-draw match-level
bootstrap uncertainty (Elo/BT confidence intervals and bootstrap rank distributions).head_to_head: W/L/D per pair, with match ids.matches: the raw claimed match list (endpoints, outcome, timestamp, repositories).repos + targets_per_repo: the exact arena configurations the tournament ran.swe-duel-tournament-update <submission.zip> --tournament <tournament_id>.Provider returned error 429. Lower the provider's entry in
provider_rate_limits (or probe it with swe-duel-probe-rate-limits), or reduce
--max-workers/--match-workers: the configured rate is a budget shared by all workers.
Retry chatter is mirrored to data/logs/model_retries.log.swe-duel- and reaped on exit; force-clean
stragglers with swe-duel-kill-containers.