LLM Control Room

A self-hosted control plane that gates, routes, prices and watches every LLM call

Source on GitHubSee it working (20 screenshots)

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

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.

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

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.