This page is a showcase. LLM Control Room runs on your own computer: clone the repository and run uv run llm-control-room, or double-click run.bat. Every screenshot below is the real app, captured after the input in its caption was typed or loaded.
A self-hosted control plane for LLM apps that runs on your own computer, with no GPU, no account and no model required. It is for teams and solo builders who run LLM apps and want one place that gates, routes, prices and watches every call.
Apps call one OpenAI-compatible endpoint. The control room authenticates the tenant, redacts secrets and personal data, blocks prompt injection, routes each request to the cheapest model that is good enough, caches, falls back when a provider fails, records cost and latency without storing prompts, runs prompt and model releases as canaries with automatic rollback, runs agents under hard limits and runs generated code under named hardening profiles. Built-in demo models and a traffic simulator let every page work offline; real providers (Ollama, OpenAI-compatible, Anthropic) plug in through environment variables.
What it does
- Gateway: one OpenAI-compatible
/v1/chat/completionsendpoint with per-tenant API keys. - Guardrails: redacts keys, emails, card numbers and custom terms before a call leaves; blocks prompt injection and per-tenant blocked terms.
- Router: sends each request to the cheapest model good enough for its difficulty, with savings, a confusion matrix and a cost-against-success frontier.
- Cache and fallback: repeated prompts come from the cache; a failing provider falls back along the tenant's chain.
- Observability: cost per tenant, feature and model, latency percentiles, grounding and prompt drift, alerts and CSV export, without storing prompts.
- Releases: canary, A/B and shadow traffic for prompt and model versions, with SLOs and an auto-rollback that holds during a provider outage.
- Agent runs: agents under hard limits on cost, steps, time and loops, with an approval gate before risky actions.
- Sandbox: runs generated code under named hardening profiles, with an attack suite that shows what each profile stops.
- Playground and batch: one prompt or a file of prompts, with route, cost, cache, redactions and blocks per prompt.
- Tenants and keys: budgets, rate limits, model access, block and redact terms, import from a file.
In detail:
| Gateway | POST /v1/chat/completions (plus /v1/models, streaming replay). Per-tenant API keys (stored as SHA-256), spend budget over a rolling window that each call is held to (never overspent, also by a burst of concurrent requests), requests-per-minute limit, allowed-model list, response cache, fallback chain, secret redaction always (Unicode-normalised, look-alike letters, spaced-out or split across messages, one level of base64/percent/hex decoding), PII redaction per tenant (including obfuscated addresses such as name [at] example [dot] com), per-tenant literal block/redact terms, prompt-injection block (also the common phrasings in twelve other languages and HTML-entity, escape, ROT13 and leetspeak spellings), GET /v1/usage for a tenant's own spend. Order: guard, rate limit, route, budget, cache, call, output guard, record. |
| Router | Difficulty from cheap, explainable signals (length, code, reasoning words, multi-part asks, retrieved context, easy cues). Picks the lowest expected cost among models whose quality for that difficulty reaches the tenant's threshold. Shows each decision, savings against a baseline model over the calls that reached a model (cache hits are not credited to the router), an estimate-vs-label confusion matrix and a cost/success frontier. |
| Observability | Cost per tenant, feature, model and per successful answer (spend includes the shadow calls a tenant's requests caused, as its budget does); p50/p95/p99 latency excluding cache hits; grounding rate for RAG calls; prompt drift by PSI over prompt shape (length, question type); alerts with a minimum sample and a cooldown. |
| Releases | Versions of a model and system prompt, called like a model name. Sticky hash assignment, canary, A/B (two-proportion z-test), shadow traffic whose failures cannot reach the caller, per-version SLO windows, auto-rollback. |
| Agent runs | Hard limits on steps, tool calls, cost, wall time and repeated actions; risk-tiered tools with an approval gate; a live run log; every reasoning step is a billed gateway call. |
| Sandbox | Run Python under subprocess (unsafe baseline), restricted (scrubbed env, audit hook, timeout, output cap, 1 GB memory cap, 64 MB workdir cap) or hardened (Docker, when available), and a twelve-attack suite judged by the harness's own evidence. |
| Playground | Send a prompt as any tenant; see route, candidate costs, cache hit, redactions, fallbacks, grounding. |
Gallery
Every screenshot is the real UI in headless Microsoft Edge (Chromium; 1440x900, dark mode), taken after typing or loading the input shown and waiting for the real output. Recreate them with uv run --with playwright python scripts/gallery.py; captions are also in docs/gallery/CAPTIONS.md.
Overview: Try-it




Playground


Tenants




Router

Observability

Releases


Agent runs


Sandbox

Simulator

About and phone


Inputs
Every place that takes content accepts a file (file picker or drag and drop onto the box) and pasted text, shows what the input looks like, and has a "load example" button. Files are read in the browser and sent to the local server; nothing uploaded is stored except what you choose to save.
| Feature | Accepts |
|---|---|
| Playground, one prompt | typed or pasted prompt; prompt, system prompt and context from .txt .md .json (up to 200 KB) |
| Playground, batch | .jsonl .csv .json .txt or pasted, up to 500 prompts and 500 KB, run as a chosen tenant (or route only); progress bar, results table, CSV download |
| Tenants, import | .json or .csv (up to 50 tenants, 500 KB) or pasted, preview then import, first key shown once; current config downloads as JSON or CSV |
| Tenants, block / redact terms | comma-separated field, or a term list from .txt .csv .json (up to 100 KB), added to or replacing the list |
| Sandbox | typed or pasted code, or a .py / .txt file (up to 200 KB) |
| Agent goal, release version prompt | typed or pasted, or a .txt / .md file (up to 20 KB) |
The About and guide page (nav entry, and the "?" by the logo) covers what it is, what it does, step-by-step use with the expected input and output of each feature, privacy, the roadmap and the maker.
Tech stack
- Backend: Python 3.11+, FastAPI, Uvicorn, SQLite
- Gateway: OpenAI-compatible API, per-tenant keys, budgets and rate limits, response cache, fallback chains
- Providers: built-in demo models, Ollama, OpenAI-compatible APIs, Anthropic
- Routing and releases: difficulty-based routing, canary/A-B/shadow assignment by session hash, SLO checks with two-proportion tests
- Observability: latency percentiles, grounding scores, PSI drift over prompt shape (no prompts stored)
- Sandbox: subprocess profiles with an audit hook, optional Docker hardening
- Frontend: vanilla JavaScript, HTML and CSS with SVG charts, no build step; light and dark themes
- Performance: measured by
scripts/bench_obs.py(one process, in-process client): with 300,000 calls in the window the Observability page builds in 2.4 s and the Router page in 1.1 s, and the gateway keeps serving about 52 chat requests a second (p95 26 ms) while the Observability page is requested back to back. - Tooling: uv, pytest, ruff, Playwright for the UI audit and gallery
Run it
uv run llm-control-room # starts on http://127.0.0.1:8790, opens the browser, simulates a first day of traffic
or double-click run.bat. Options: --port, --no-browser, --no-seed, --db FILE, --host. Data lives in ~/.llm-control-room/control-room.sqlite3 (set LCR_HOME to move it).
The admin API (/api, and the UI that drives it) needs a token. If LCR_ADMIN_TOKEN is not set, the first start generates one into admin-token next to the database; the launcher opens the browser at /#token=..., the page keeps it and clears the address bar. Opening the bare URL later shows a sign-in box that says where that file is. Only the OpenAI-compatible /v1 endpoints (tenant keys) and /api/health are open. The server answers only to localhost/127.0.0.1 Host headers (extend with LCR_ALLOWED_HOSTS=a.example,b.example, or *), refuses cross-origin and non-JSON writes, and, when started with a non-loopback --host, creates no demo keys and revokes existing ones. A demo key or demo tenant you revoke or delete stays that way across restarts. Python 3.11+ and uv. Nothing leaves the machine.
Call it from an app (demo keys lcr-demo-acme, lcr-demo-globex, lcr-demo-initech; create real ones under Tenants and keys):
curl http://127.0.0.1:8790/v1/chat/completions -H "Authorization: Bearer lcr-demo-acme" \
-H "Content-Type: application/json" -d '{"model":"auto","messages":[{"role":"user","content":"Hello"}]}'
model can be auto, a model id, or a release name. Extensions: header X-LCR-Feature, and "lcr": {"feature", "context", "session", "use_cache"} in the body. Response headers carry x-lcr-model, x-lcr-cache, x-lcr-cost-usd.
Optional real providers (never required; set before launching):
LCR_OLLAMA_MODELS=qwen2.5:0.5b # LCR_OLLAMA_URL, default http://127.0.0.1:11434
LCR_OPENAI_MODELS=gpt-4o-mini:0.15:0.6 # LCR_OPENAI_BASE_URL, LCR_OPENAI_API_KEY
LCR_ANTHROPIC_MODELS=claude-haiku-4-5:1:5 # ANTHROPIC_API_KEY
LCR_ADMIN_TOKEN=... # require X-Admin-Token on /api (the UI prompts for it)
A model spec is id[:usd_in_per_1M[:usd_out_per_1M[:q_easy:q_medium:q_hard]]]. Real models join the router's pool next to the mock ones; restrict a tenant's allowed_models to keep them apart.