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.
Needs Docker with Compose v2. About 5 minutes (first build is the slow part).
-
Start it. Pick one:
A. Installer (builds from source):
curl -fsSL https://raw.githubusercontent.com/izzoa/polyrouter/main/install.sh | shPrefer 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:
{ 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 .envdocker-compose.yml: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:
docker compose up -d # migrations run on bootUpgrade: bump the tag, then
docker compose pull && docker compose up -d. Going public (APP_URL, reverse proxy, Apprise): see Self-hosting. -
Open
http://localhost:3001and sign up. The first account becomes the admin. -
Add a provider (Providers) and create an agent key (Agents → New, shown once,
poly_…). -
Point your client at it:
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 (OpenAI SDK, Anthropic SDK, OpenClaw, Hermes).
- Two protocols, one key. OpenAI
/v1/chat/completions+/v1/modelsand 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 - 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. 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
- 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.
flowchart LR
A["Agents<br/>(any OpenAI / Anthropic SDK)"] -- "/v1 + poly_ key" --> P
subgraph S["polyrouter — one container"]
P["Inference proxy<br/>route · fallback · budget"] --- T["Protocol<br/>translation"]
D["Dashboard SPA + API"]
end
T --> O["OpenAI-compatible<br/>providers"]
T --> C["Anthropic-compatible<br/>providers"]
P -.->|"atomic counters · breakers"| R[("Redis")]
P -->|"RequestLog + price snapshots"| PG[("PostgreSQL")]
D --> PG
Monorepo (Turborepo + npm workspaces): packages/shared, packages/control-plane
(NestJS API + /v1 endpoints), packages/data-plane (routing, translation, adapters),
packages/frontend (SolidJS). Code wiki: openwiki/.
| Need | Read |
|---|---|
Run it: Docker, prebuilt image, .env, upgrades |
Self-hosting |
| Wire up an SDK or terminal agent | Connect an agent |
| Routing precedence, workload classes, auto layers | Routing |
| Budget progress semantics | Budgets API |
| Release history | CHANGELOG |
docs/self-hosting.md also covers the reverse proxy / CDN setup, the optional semantic
embedder, Apprise, subscription (OAuth) providers, and users & registration.
Needs Node.js 24.x (.nvmrc), npm 10–11, Docker.
npm ci
docker compose -f docker-compose.dev.yml up -d # PostgreSQL 16 + Redis 7
npm run dev # API :3001, dashboard :3000| Command | What it does |
|---|---|
npm run build |
Production build (Turborepo) |
npm test -w packages/<pkg> |
Unit tests for one package |
npm run test:e2e -w packages/control-plane |
E2E (needs dev compose up) |
npm run db:generate / npm run db:migrate |
Drizzle migrations (run on boot) |
npm run lint / npm run format |
ESLint / Prettier |
For a seeded dev admin, boot with SEED_DATA=true (loopback, non-production, self-hosted
only): admin@polyrouter.local / changeme-dev-admin. Change it immediately.
Work is spec-driven (OpenSpec). Read CLAUDE.md for the stack and
invariants, and STYLESEED.md for UI rules.
Using flat-rate consumer subscriptions (ChatGPT Plus, Claude Max) programmatically likely violates those providers' ToS. polyrouter supports the provider kind but flags the risk. The coding plans (OpenCode Go, MiniMax M Plan, Kimi Code, Z.ai GLM Coding Plan) are connected with an API key, but that key carries its plan's own terms: Z.ai forbids proxy and aggregate use, and Kimi forbids automation and altering its client identity. polyrouter states each plan's risk before you connect, and when a MiniMax, Kimi Code or Z.ai plan runs out of allowance it pauses that provider until the reset time, skips it in routing and falls back, without tripping the circuit breaker; OpenCode Go keeps the generic error mapping (see Plan limits and pauses). Pay-per-token API keys and local models don't carry subscription terms risk.
AGPL-3.0. If you offer a modified polyrouter as a network service, you must offer its users your modified source.