Patch every LLM call through one board.

SwitchBoard sits between your application and Groq, Google and Anthropic. It answers repeat questions from a semantic cache instead of the provider, rotates across your API keys the moment one hits a rate limit, and reports every hop to Prometheus. The endpoint is OpenAI-compatible — change the base URL, keep your client.

git clone https://github.com/sankalp-happy/switchboard.git && cd switchboard && ./setup.sh
Patch field · request routing
Request in POST /v1/chat/completions Semantic cache redis 7 · gemini embeddings Router key selection · failover Providers groq · google · anthropic X-CACHE: HIT X-CACHE: MISS 429 → ROTATE KEY
> "Say hi in five words."    X-Cache: MISS · X-Provider: groq · X-Latency-Ms: 424.0
> "Say hi in five words."    X-Cache: HIT · X-Semantic-Similarity: 1.0000
> "Greet me using five words." X-Cache: HIT · X-Semantic-Similarity: 0.9144
The problem 3 faults

Three things break when you call a provider directly.

None of them are exotic. They show up the first week you put a prototype in front of real traffic, and every one of them is a gateway's job — not your application's.

Fault 01

Keys run dry mid-traffic

Provider keys carry hard per-minute token and request ceilings. One 429 and the call fails — while a second key sits idle in your .env. SwitchBoard marks the exhausted key, retries the next one, and only surfaces an error once every key is genuinely spent.

Fault 02

You pay twice for one question

People ask the same thing in different words all day long. Hash-based caches miss every rephrasing. SwitchBoard embeds the prompt and compares it by cosine similarity, so "greet me using five words" hits the entry stored for "say hi in five words."

Fault 03

No view of what's happening

A provider SDK hands back a response, not a hit rate, a p95, or a count of key rotations. SwitchBoard exposes all of it on /metrics and ships a Grafana dashboard that is provisioned for you at first boot.

What's on the board 8 modules

A small, legible gateway — not a platform.

Roughly two thousand lines of Python you can read in an afternoon. Every part of it is listed here, and every claim on this page maps to a file in the repository.

Drop-in OpenAI-compatible surface

POST /v1/chat/completions accepts and returns the shapes your client already speaks. Point the SDK at SwitchBoard and delete nothing else.

python
from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:8000/v1",
    api_key="not-needed",   # SwitchBoard holds the real keys
)
gateway/main.pyopenai sdk compatible

Semantic caching

Prompts are embedded with gemini-embedding-001 and compared by cosine similarity. Anything at or above 0.90 is served from Redis with a one-hour TTL — no provider call, no tokens spent.

cache/redis_client.pythreshold 0.90 · ttl 3600s

Rotation & failover

Register as many keys per provider as you like. The router picks the one with the most remaining quota, then walks the rest on 429, 401 or 5xx. A sweeper revives expired keys every five seconds.

routing/router.pysweeper 5s

Keys encrypted at rest

Provider credentials are sealed with Fernet before they touch SQLite and are only ever returned masked — gsk_…4f2a. The ENCRYPTION_KEY stays in your environment.

core/key_manager.pyfernet · sqlite

Per-request routing

Set "provider" in the request body to pin a single call to groq, google or anthropic. Omit it and SWITCHBOARD_PROVIDER decides.

providers/3 adapters

Self-hosted by default

There is no SwitchBoard service to sign up for. The whole stack — gateway, Redis, Prometheus, Grafana, admin UI — runs from one Compose file on hardware you control. Prompts leave your host only when they reach the provider you chose.

docker-compose.yml6 services

Instrumented, not guessed at

Seven purpose-built metric families plus automatic FastAPI instrumentation, scraped every 15 seconds and rendered by a Grafana dashboard that provisions itself.

Cache
cache_hits_total
Latency
provider_latency_seconds
Rotation
key_switches_total
Tokens
tokens_processed_total
core/metrics.pyprefixed switchboard_

Admin API & control panel

Add, disable and delete keys without a restart, and read live quota state per key. A static control panel ships in the stack on port 3000, proxied to the gateway by nginx.

POST
/admin/keys
GET
/admin/providers
GET
/admin/keys/usage
GET
/admin/stats
gateway/admin.py · vis/port 3000
Architecture 6 steps

One request, end to end.

Everything inside the dashed boundary is a single FastAPI process. Redis, Prometheus and Grafana are sibling containers; the providers are the only hop that leaves your network.

SWITCHBOARD GATEWAY · FASTAPI · UVICORN CLIENT openai sdk · curl REDIS 7 embeddings + responses ttl 3600s SEMANTIC CACHE cosine ≥ 0.90 gemini-embedding-001 ROUTER picks best key retries on 429/401/5xx KEY MANAGER sqlite · fernet encrypted rate-limit ledger PROVIDER ADAPTER httpx · unified schema reads x-ratelimit-* PROVIDERS groq google anthropic leaves your network PROMETHEUS scrape 15s GRAFANA provisioned Cache hit → return · X-Cache: HIT /metrics 1 2 3 4 5 6 SWITCHBOARD GATEWAY CLIENT openai sdk · curl SEMANTIC CACHE cosine ≥ 0.90 · gemini REDIS 7 ttl 3600s ROUTER retries on 429/401/5xx KEY MANAGER sqlite · fernet PROVIDER ADAPTER httpx · x-ratelimit-* PROVIDERS groq · google · anthropic leaves your network PROMETHEUS scrape 15s GRAFANA provisioned dashboard Cache hit /metrics
  1. Request arrives FastAPI validates the body against ChatCompletionRequest — model, messages, temperature, optional provider.
  2. Cache lookup Messages are joined and embedded, then compared against every stored embedding in Redis by cosine similarity.
  3. Hit or miss At ≥ 0.90 the stored response returns immediately with X-Cache: HIT. Otherwise the router takes over.
  4. Key selection The key manager returns the enabled key with the most remaining quota, decrypting it from SQLite only at the moment of use.
  5. Provider call The adapter sends the request over HTTPX, normalises the reply, and hands back the x-ratelimit-* headers.
  6. Settle up Quotas and per-key usage are written back, metrics increment, the response is cached, and the client gets X-Provider and X-Latency-Ms.
On failure — a 429 marks the key exhausted and moves to the next; a 401 disables the key permanently; a 5xx just moves on. Only when every enabled key for that provider has been tried does the caller see a 502. A background sweeper restores keys whose rate-limit window has expired every five seconds.
Telemetry 9 panels

The dashboard is already built.

grafana/dashboards/switchboard.json is provisioned at container start against a Prometheus datasource that is also provisioned. Nothing to click through on first boot — these nine panels are simply there.

PanelTypeQuery
Cache Hit Ratetimeseriesrate(switchboard_cache_hits_total[5m]) / (rate(hits[5m]) + rate(misses[5m]))
Total Request Throughputtimeseriesrate(http_requests_total{handler="/v1/chat/completions"}[5m])
Total Requestsstathttp_requests_total{handler="/v1/chat/completions"}
Cache Hitsstatswitchboard_cache_hits_total
Provider Latencytimeserieshistogram_quantile(0.50 / 0.95 / 0.99, rate(switchboard_provider_latency_seconds_bucket[5m]))
Key Switchesstatincrease(switchboard_key_switches_total[1h])
Active Keysstatswitchboard_active_keys
Tokens Processedtimeseriesrate(switchboard_tokens_processed_total[5m])
Cache Hits vs Missestimeseriesrate(switchboard_cache_hits_total[5m]) · rate(switchboard_cache_misses_total[5m])

Grafana ships with anonymous viewer access enabled and an admin login of admin / switchboard — change both before exposing it.

Install 6 services

Clone, run one script, done.

setup.sh checks your Docker install, generates the Fernet encryption key, prompts for whichever provider keys you want to seed, writes .env, brings the stack up, and waits until every service reports healthy. Re-running it is safe — existing values are reused, never overwritten.

bash
git clone https://github.com/sankalp-happy/switchboard.git
cd switchboard
./setup.sh

Docker and Docker Compose v2 are the only prerequisites. Provider keys are optional at install time — add them later from the control panel. Without GOOGLE_API_KEY the gateway still serves traffic; it just runs with the semantic cache disabled.

Gateway API:8000
Swagger docs:8000/docs
Control panel:3000
Redis:6379
Prometheus:9090
Grafana:3001

If a port is already taken, setup remaps the service and records the new port in .env rather than killing whatever holds it.