Skip to content

About

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.

Topics

Resources

Contributing

Security policy

Stars

25 stars

Watchers

1 watching

Forks

Latest commit

 

History

292 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

polyrouter

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.

CI License: AGPL-3.0 ghcr.io/izzoa/polyrouter Node 24.x

Quickstart · Self-hosting · Connect an agent · Routing · Roadmap · Changelog · Contributing

polyrouter dashboard — the overview: KPI tiles for requests, spend, tokens and success rate; a requests-over-time chart; spend by model; and the live request log


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):

    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:

    { 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:

    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 boot

    Upgrade: bump the tag, then docker compose pull && docker compose up -d. Going public (APP_URL, reverse proxy, Apprise): see Self-hosting.

  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:

    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).

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
  • 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

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

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
Loading

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/.

Docs

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.

Development

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.

Compliance note

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.

License

AGPL-3.0. If you offer a modified polyrouter as a network service, you must offer its users your modified source.

About

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.

Topics

Resources

Contributing

Security policy

Stars

25 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages