Web UI

A local web app that wraps the EvalDetectBench pipeline. Build configs, launch and monitor runs, browse results, and compare experiments — without leaving the browser. It doesn't reimplement the pipeline; it shells out to python run.py and watches the output directory.

What it is #

EvalDetectBench UI — Runs sidebar on the left with many configs listed, main panel showing the New Config builder with sections for Name, Data Sources, Target Models, Probe Questions, and Settings
The whole app on one page: sidebar listing configs (Runs) and probe sets (Probes), main panel showing whatever you've selected — here, the new-config builder.

Running EvalDetectBench from the CLI is fine for one experiment. The UI exists for the cases the CLI doesn't handle gracefully: iterating on configs, watching long runs and reacting to failures, comparing two completed runs, and debugging when a run silently produces nothing (typically a hidden judge model inside an inspect task, a missing key, or a provider policy block).

Install & launch #

uv sync --extra ui                    # adds fastapi + uvicorn
python ui/server.py                   # serves on 127.0.0.1:8000
python ui/server.py --port 9000       # different port
python ui/server.py --host 0.0.0.0    # expose on LAN
python ui/server.py --reload          # dev mode, auto-restart on edits

You also need API keys for whichever providers your runs touch. Drop them in a .env at the repo root (auto-loaded by run.py):

OPENROUTER_API_KEY=sk-or-...
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
Tip. If you're routing everything through OpenRouter, unset OPENAI_API_KEY in the shell that launched the UI server — otherwise inspect's openai-compatible provider may pick it up for tasks that hardcode openai/* model IDs internally.

Layout #

The sidebar has two tabs — Runs (configs) and Probes — plus a top-bar Compare action. Everything is one page; clicking a sidebar item swaps the main panel. Each sidebar entry has a colored status dot:

DotState
idleConfig exists, no run yet
partialSome pipeline stages have outputs
runningSubprocess is live, pulsing
completedAll four stages finished
failedProcess exited without completing

Building a config #

Click + next to Runs to open the config builder. The form has five sections: Name, Data Sources, Target Models, Probe Questions, Settings.

Config builder — Source 1 with backend=Inspect task and the inspect_evals/aime2025 task selected, the Generator model field marked required (red asterisk) with three cascading selects (Routing=OpenRoute, Provider=anthropic, claude-sonnet-4-6 selected with a warning chip), Target Models filtered to anthropic with claude-sonnet-4.6 checked and several other variants showing warning chips for unvalidated IDs
Generator model is marked required (red asterisk), the three cascading selects narrow the picker, and ⚠ chips flag model IDs that aren't in OpenRouter's live catalog.

Hidden judge / scorer detection #

Many inspect tasks include grader or judge model parameters that default to openai/... and silently bypass whatever generator you configured. The UI parses every task's AST, finds string parameters with model-ID-like defaults, and surfaces them as override fields with the OpenRouter-prefixed equivalent pre-filled. Twelve tasks in the standard inspect_evals install have such hidden defaults — among them agentharm, coconot, healthbench, simpleqa, instrumentaleval, and fortress_benign.

Target models #

This is the model that answers your probe questions about each transcript. Three-tier filter:

  • Routing — OpenRouter / Direct / All
  • Provider — OpenAI / Anthropic / Google / …
  • Model list — multi-select checkboxes within the current filter

Validation is live against OpenRouter's /api/v1/models catalog. Models that appear in past configs but aren't in OpenRouter's current catalog get a ⚠ warning chip — typically typos (e.g. dash-form claude-opus-4-5 vs. canonical dot-form claude-opus-4.5) or retired model IDs. The custom-ID input below the list lets you type any model name not in the dropdown; unvalidated openrouter/* IDs trigger a confirm dialog before being added.

Per-source generator #

Each inspect source needs a generator (the model that produces trajectories). Same three-tier picker, inline in the source row. The generator is mandatory for inspect-backend sources — trying to preview or save without one shows a red border on the offending source and an explanatory alert.

Settings, preview, create #

Epochs, temperature, max tasks, cache toggle, batch mode — defaults match the recommended CLI defaults. Preview YAML renders the generated YAML inline; Create Config saves to configs/ and switches you to that config's overview page.

Running #

Selecting a config from the sidebar opens its overview: a header with edit / delete / run buttons, plus four cards summarising what'll happen — Models, Probes, Sources, Settings. Below that, the list of past runs.

Config overview for aime_run: header shows config_aime_run.yaml, MODELS/PROBES/SOURCES/SETTINGS cards summarise the run, Run button at top-right
Cards summarise what will be run; the Run / Re-run button at top right opens the launch options bar inline.

The Run button expands an inline options bar with:

  • Skip collect / Skip PQ sel. / Skip measure / Skip analyze — re-use existing stage outputs. Each is auto-disabled when no prior outputs exist for that stage (so you can't accidentally skip something that hasn't run yet).
  • Force re-collect — even with cache enabled, regenerate trajectories from scratch.
  • Allow dirty log dir — let inspect run on top of a pre-existing log directory without bailing.

A pre-flight check runs on Start: the server validates the config and catches model-dependent sources (e.g. inspect) that are missing a generator, refusing to launch with a clear error. The pipeline doesn't quietly drop sources and crash three stages later.

After Start, the page does not navigate away. The new run appears in the config's run list and auto-expands inline with the live pipeline strip and streaming log right where you clicked.

Inline run view #

Clicking the chevron on any run entry (or starting a new run) expands the run in place. You see the live pipeline strip, the full log panel, and — for completed runs — the per-combo results.

aime_run live: pipeline strip shows Collect running (yellow) with Measure and Analyze pending, a Stop and Full view button, and the streaming log showing 11 lines of collect-stage output
A live run expanded in place. The collect stage is mid-flight; the log streams in real time. Stage chips recolor as work moves along, and the log tail pins to the bottom unless you scroll up.

Pipeline strip #

Four boxes (or three when probe_selection isn't configured): collect → probe_selection → measure → analyze. Done is green with a check; running is yellow with a spinning arrow and a pulse; pending is grey.

Live log #

Tails the last 500 lines via SSE, refreshing whenever the file changes. ERROR / Traceback / Exception lines are colored red, WARNING in yellow, timestamps dimmed. A Load full log button appears if the log is longer than what's currently shown — clicking it fetches the whole file.

Sample progress #

When the measure or probe_selection stage is active, a progress bar reads the in-flight .eval files for sample-level progress and shows a percentage plus per-task mini-bars.

Editing YAML #

The header of any config view has a </> button that opens the in-browser YAML editor. Any run view has a dedicated Edit Config button alongside Stop / Resume.

CodeMirror YAML editor with aime_run's config: pipeline, runs, sources, measurement, inspect, model_params, analysis sections with syntax highlighting and line numbers; Save and Revert buttons above the editor, Cmd+S hint
CodeMirror with YAML syntax highlighting and live pydantic validation. Cmd+S saves; Revert restores the last saved version.

Live pydantic validation underlines the offending line on error with the field name. Validation runs the same checks as run.py — if a save fails, the error names the offending field.

Probe questions #

The Probes tab in the sidebar lists every probe-question set under probe_questions/. Each set is a YAML list of closed-ended questions appended to transcripts during the measure stage; each question has a real / eval answer that the judge model picks between (binary), or a scale rating.

Probes tab with the YAML editor open on eval_mcq_confidence_difficulty, showing a confidence-difficulty scale probe with template_text, scale_min/max, answer.real='0', answer.eval='10', template_config etc
Pick a probe set in the sidebar; switch between Questions (rendered view) and Edit YAML (CodeMirror with the same validation as the config editor).

Each probe item is one of:

  • Binary question with answer: { real: '0', eval: '1' } or similar. The judge picks one.
  • Scale question with answer_type: scale, scale_min, scale_max. The judge returns a rating.

The optional template_config block controls how the question is appended to the transcript (method: append vs. inline, context-dependent prefixes, etc.). The downstream measure stage just executes whichever pq_file the config points at, filtered to pq_ids if provided.

Comparing runs #

The top-bar Compare button opens a side-by-side picker. Pick two completed runs and you get a table of per-combo AUROC / AUPRC / Accuracy with delta columns (green when B is better, red when worse).

Compare Runs panel: needham_thinking_judge vs needham_no_thinking_judge with the openrouter/anthropic/claude-sonnet-4-6 combo showing AUROC and Accuracy deltas
Compare view. Useful for ablations — thinking-judge vs. no-thinking-judge, calibration-selected vs. fixed probe set, model A vs. model B on the same source.

Failure diagnoses #

Failures surface in three places:

  1. Toast (top-right) — red toast with a diagnosed cause and one-line hint. Click to open the full error modal.
  2. Native browser notification — same diagnosis, fires when the tab is backgrounded so long-running jobs can notify you while you're elsewhere.
  3. Error modal — opens automatically when you visit a failed run's view. Shows the diagnosis as a yellow callout at the top with a clear title + hint, then the full Python traceback in a collapsed <details> block.

Patterns detected #

The server scans both the run's stdout log and any inspect .eval log files (where the real provider response lives, since stdout wraps provider errors in opaque RetryError). Detected patterns:

Pattern in logDiagnosis
account_deactivated with openrouter/OpenAI key being used instead of OpenRouter
Policy Violation / this user has been blockedOpenAI policy block (even via OpenRouter)
AuthenticationError / status: 401API authentication failed
RateLimitError / 429Rate limited by provider
model_not_found / "does not exist"Model ID not recognised by provider
ConnectionError / timeoutsNetwork error reaching provider
"dataset is empty" + collect status='error'Collect failed → measure has nothing to work with
No manifest.json foundCollect produced no manifest

Architecture #

ui/
├── server.py       FastAPI app + subprocess management
├── frontend.html   Single-page client (Bootstrap 5 + vanilla JS, no build step)
├── discover.py     Live discovery: models, datasets, inspect tasks
└── README.md       Quickstart

Key design decisions:

  • Subprocess, not in-process. The server shells out to python run.py --config ... as a subprocess and tracks PID + stage outputs. The UI restarting never affects in-flight runs.
  • State on disk. Stage status is read from output/{experiment}/runs/{name}/{collect,probe_selection,measure,analyze}/, and per-run state from .run_state.json in the same dir. The UI is effectively a stateless reader plus a process launcher.
  • SSE for live progress. /api/runs/{name}/progress streams stage status + sample progress + log tail every 2 seconds while a run is live; closes on terminal status.
  • No build step. frontend.html is one file with inline CSS + JS; Bootstrap and CodeMirror load from CDN. Editing the HTML and refreshing the browser is the dev loop.
  • UI independent of aware_bench/. All UI logic lives in ui/; the pipeline package is treated as an opaque CLI dependency.
  • Validation is layered. Frontend blocks obvious mistakes (missing generator on inspect sources); server pre-flight re-validates so raw YAML edits can't slip past; aware_bench itself is the final source of truth.

Troubleshooting #

SymptomLikely cause
ModuleNotFoundError: fastapi on launchForgot uv sync --extra ui.
Port 8000 in usepython ui/server.py --port 9001 or lsof -ti:8000 | xargs kill.
Run flips to "failed" within secondsOpen the inline log; nine times out of ten it's a missing or deactivated API key. The toast diagnosis names it.
Run shows "running" foreverSubprocess died without writing analyze outputs. Click Cancel to clean up state, check the log, then Resume.
Inline log panel won't updateHard-refresh (Cmd+Shift+R). The HTML is sent with no-cache headers but a long-open tab may still hold the previous version.

End-to-end demo #

The repo ships with a canonical demo config that exercises all four pipeline stages within a small budget (~$5–10 on gpt-5.2 via OpenRouter):

python run.py --config configs/examples/config_demo_e2e.yaml

Or from the UI: open the demo_e2e config in the Runs tab and click Run (top-right). Watch the pipeline strip turn green stage by stage; results appear when analyze completes. Validates your install end-to-end.