One endpoint for every model.
A self-hostable LLM router / gateway: OpenAI- and Anthropic-compatible, explicit-first
routing with fallbacks, spend limits, and metadata-only cost tracking.
No markup, no third-party proxy — your keys, your box.
Quickstart · Self-hosting · Connect an agent · Routing · Roadmap · Changelog · Contributing
--- polyrouter sits between your AI agents and your LLM providers. Agents use **one** endpoint and **one** key. polyrouter picks the model, falls back when a provider fails, enforces budgets, and records what each request cost. It stores **metadata only** — never prompt or response bodies unless you opt in. ## Quickstart Needs Docker with Compose v2. About 5 minutes (first build is the slow part). 1. Start it. Pick one: **A. Installer** (builds from source): ```bash curl -fsSL https://raw.githubusercontent.com/izzoa/polyrouter/main/install.sh | sh ``` Prefer to read it first? Clone the repo and run `./install.sh`. **B. Prebuilt image** (no build). In an empty folder, generate secrets, save the compose file, boot: ```bash { for k in BETTER_AUTH_SECRET API_KEY_HMAC_SECRET PROVIDER_CREDENTIAL_KEY \ NOTIFY_CREDENTIALS_SECRET POSTGRES_PASSWORD; do echo "$k=$(openssl rand -hex 32)" done; } > .env && chmod 600 .env ``` `docker-compose.yml`: ```yaml name: polyrouter-selfhost services: app: image: ghcr.io/izzoa/polyrouter:0.31.0 # or :latest restart: unless-stopped ports: - '127.0.0.1:3001:3001' # loopback only until you claim the instance depends_on: postgres: { condition: service_healthy } redis: { condition: service_healthy } stop_grace_period: 45s # drains in-flight streams env_file: .env environment: NODE_ENV: production MODE: selfhosted BIND_ADDRESS: 0.0.0.0 PORT: '3001' DATABASE_URL: postgresql://polyrouter:${POSTGRES_PASSWORD}@postgres:5432/polyrouter REDIS_URL: redis://redis:6379 BETTER_AUTH_URL: http://localhost:3001 BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:?set in .env} API_KEY_HMAC_SECRET: ${API_KEY_HMAC_SECRET:?set in .env} PROVIDER_CREDENTIAL_KEY: ${PROVIDER_CREDENTIAL_KEY:?set in .env} NOTIFY_CREDENTIALS_SECRET: ${NOTIFY_CREDENTIALS_SECRET:?set in .env} postgres: image: postgres:16-alpine restart: unless-stopped environment: POSTGRES_USER: polyrouter POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set in .env} POSTGRES_DB: polyrouter volumes: ['polyrouter-pg:/var/lib/postgresql/data'] healthcheck: test: ['CMD-SHELL', 'pg_isready -U polyrouter -d polyrouter'] interval: 5s timeout: 3s retries: 12 redis: image: redis:7-alpine restart: unless-stopped volumes: ['polyrouter-redis:/data'] healthcheck: test: ['CMD', 'redis-cli', 'ping'] interval: 5s timeout: 3s retries: 12 volumes: polyrouter-pg: polyrouter-redis: ``` ```bash docker compose up -d # migrations run on boot ``` Upgrade: bump the tag, then `docker compose pull && docker compose up -d`. Going public (`APP_URL`, reverse proxy, Apprise): see [Self-hosting](./docs/self-hosting.md). 2. Open `http://localhost:3001` and sign up. **The first account becomes the admin.** 3. Add a provider (**Providers**) and create an agent key (**Agents → New**, shown once, `poly_…`). 4. Point your client at it: ```bash curl http://localhost:3001/v1/chat/completions \ -H "Authorization: Bearer poly_your_key" \ -H "Content-Type: application/json" \ -d '{"model":"auto","messages":[{"role":"user","content":"hi"}]}' ``` Next: [connect a real agent](./docs/connect-an-agent.md) (OpenAI SDK, Anthropic SDK, OpenClaw, Hermes). ## What you get - **Two protocols, one key.** OpenAI `/v1/chat/completions` + `/v1/models` and Anthropic `/v1/messages`, streaming or not. Cross-protocol requests are translated. - **Explicit-first routing.** A named model always works. Optional extras: tier headers, fallback chains, and `model: "auto"` smart layers. Every smart layer degrades to the default — routing never fails a request. → [Routing](./docs/routing.md) - **No silent model swaps.** Fallback happens before the first token. After that the model is committed; a failure ends the stream with a clear error. - **Immutable cost records.** Each request stores its price snapshot. Later price updates never rewrite history. Estimates are flagged. - **Budgets that block.** Day/week/month, global or per-agent, enforced with atomic Redis counters across instances. - **Async alerts.** SMTP and [Apprise](https://github.com/caronc/apprise). A failing channel never blocks a request. - **Dashboard.** Costs, per-agent usage, provider health, routing config, and a decision inspector showing why each request went where it did. - **Plan usage on subscription cards.** A Claude, ChatGPT, OpenCode Go, MiniMax, Kimi Code or Z.ai card can show one neutral line of how much of the plan is used, when each window resets, and when the plan is billing extra usage. It reads what replies already carry, so it costs nothing; key plans add a **Refresh usage** button you press yourself, at most once every five minutes. It is display only: no alert, no routing change, no background polling. Each usage endpoint stays off until it has been checked against the live provider. → [Plan usage](./docs/self-hosting.md#plan-usage-on-the-card) ## Security - Metadata-only by default; credentials encrypted at rest. - Agent keys are HMAC-SHA256 (fast); dashboard passwords use a slow hash. Separate planes. - Every user-supplied URL is SSRF-checked (loopback allowed only for local models in self-host mode). - Every entity is tenant-scoped through one central guard. - First signup wins, then invite-only. Disabling a user revokes their sessions and keys. ## Architecture ```mermaid flowchart LR A["Agents