Self-hosted · OpenAI-compatible · Alpha
TinyRouter
An LLM gateway small enough to read before you hand it your API keys.
One OpenAI-compatible endpoint in front of OpenAI, Anthropic, Gemini, and your own models, with fallbacks you write down in YAML rather than ones a router guesses at. No accounts, no database, no dashboard in the gateway.
curl -fsSL https://tinyrouter.dev/install.sh | sh
Picks the build for your system, checks it against the release's SHA256SUMS, and
unpacks it here. Read it first ·
Quick start
- ~3,000lines of TypeScript
- 2runtime dependencies
- 1YAML file
- 0databases to run
Fallbacks you can read
Give an alias a list of targets. TinyRouter tries them in exactly the order you wrote, and nothing is inferred. Serve your own hardware first and reach for a hosted model only when it cannot answer:
routes:
assistant:
- local/qwen3
- anthropic/claude-sonnet-4-6
- local/
qwen3503 · fall back - anthropic/
claude-sonnet-4-6200 · served
What it promises
Fallback in the order you wrote
A target is retried or passed over only for the statuses you list (429 and 5xx by default) or a connection that fails. Never on a heuristic.
No switching mid-stream
Once a provider starts answering, it finishes the answer. Replaying the request elsewhere could duplicate output or tool calls.
No quietly weakened requests
A target that cannot honor part of a request, such as an image format, a strict JSON schema, a reasoning-effort level, or several choices, is skipped before any call. Routing moves on.
Nothing you send is logged
Logs record targets, status codes, and latency. Never prompts, completions, provider keys, or the bearer tokens your clients send.
The whole routing contract fits on one page.
Quick start
-
Install
curl -fsSL https://tinyrouter.dev/install.sh | shThe script picks the archive for Linux or macOS on x64 or arm64, checks it against that release's
SHA256SUMS, and unpackstinyrouterandtinyrouter.example.yamlinto the current directory. It needs no root. Pin a release with| sh -s -- 0.9.0. It is about a hundred lines, so read it before you pipe it into a shell. The executable embeds the Bun runtime, so nothing else needs installing, exceptapk add libstdc++on Alpine.Or download by hand
curl -fsSL https://github.com/honzabit/tinyrouter/releases/latest/download/tinyrouter-linux-x64.tar.gz | tar -xzSwap
linux-x64forlinux-arm64,darwin-x64, ordarwin-arm64, and check the download againstSHA256SUMS. On macOS, extract withtarrather than Finder, which marks the executable as damaged. On Alpine, uselinux-x64-muslorlinux-arm64-musl, afterapk add libstdc++. Releases up to 0.9.0 have no musl build; use the container below for those. -
Configure
The archive includes an example that matches the executable beside it. Keep the providers you use, add a route like the one above, and export the keys it references:
cp tinyrouter.example.yaml tinyrouter.yaml export TINYROUTER_API_KEY=local-secret export ANTHROPIC_API_KEY=sk-ant-... -
Run
./tinyrouterIt reads
tinyrouter.yamland listens on127.0.0.1:8080until you setserver.host. Use--checkto validate a configuration without starting. -
Call the alias like a model
curl http://localhost:8080/v1/chat/completions \ -H 'Authorization: Bearer local-secret' \ -H 'Content-Type: application/json' \ -d '{"model":"assistant","messages":[{"role":"user","content":"Hello"}]}'Any client that takes an OpenAI base URL works unchanged: point it at
http://localhost:8080/v1.
Or run the multi-architecture container:
docker run --rm -p 8080:8080 --env-file .env \
-v "$PWD/tinyrouter.yaml:/etc/tinyrouter/tinyrouter.yaml:ro" \
ghcr.io/honzabit/tinyrouter:latest
Providers
Clients always see one shape, OpenAI's /v1/chat/completions and
/v1/models, streaming or not. Behind it:
| Type | What TinyRouter does |
|---|---|
openai | Passes Chat Completions through unchanged |
openai-compatible |
The same for any OpenAI-shaped server, such as Ollama or vLLM. No API key required. |
anthropic |
Translates messages, tools, images, and reasoning effort, and normalizes JSON and streams |
gemini |
Translates messages, tools, images, JSON mode, and reasoning effort, and normalizes JSON and streams |
Also in the box
Request filters
Block API keys and private keys outright. Redact emails, phone numbers, and card numbers before a request leaves. Scope a filter to cloud providers and your local model still sees everything.
Circuit breaking
Opt in, and a provider that keeps failing is demoted to a last resort for a cooldown instead of costing every request its full timeout.
Stall detection
timeout_ms bounds every gap in a response, not just the wait for headers, so a
provider that goes quiet halfway through is cut loose.
Prometheus metrics
Requests, attempts, tokens per model, and how every response body ended. Built in, with no exporter to run.
Named API keys
Give each caller its own key and revoke one by deleting a line. The name appears in the request log, never in metrics.
Closed by default
Listens on loopback until you say otherwise, and can put its observability endpoints behind the same key.
See what it is doing
A single static HTML file in ui/, also attached to every release as
tinyrouter-ui.html, reads the gateway's own endpoints. It shows which providers are
healthy, which the circuit breaker has demoted, and where your tokens went:
| provider | circuit | models | 2xx | errors | bad bodies |
|---|---|---|---|---|---|
| anthropic | cooling | claude-sonnet-4-6 | 0 | 3 | 0 |
| local | healthy | qwen3 | 184 | 0 | 0 |
Bad bodies is the column no status code can give you: a provider that answers
200 and then hangs part-way through the stream. The status line went out long before
anyone knew.
Download it and open it locally rather than using a copy hosted here. It is a page that reads your gateway, and a page served from someone else's domain is a page whose JavaScript you have to trust with it. That is the whole argument for this project, so it would be odd to make an exception.
When to use it
- You want to audit the thing that holds your keys. Two dependencies, no build magic, one file of configuration.
- You refuse to run infrastructure for a proxy. One process. No database, no Redis, no admin UI.
- You run local models with cloud fallback. Serve your own hardware first, fail over only when it cannot answer.
- You need failover you can reason about. The routing contract fits on a page and never switches providers mid-stream.
- Clients you don't control need one endpoint. Anything that accepts an OpenAI base URL gets aliases and failover with no code changes.
Use something else when
| You need | Better fit |
|---|---|
| Budgets, virtual keys, per-user spend tracking, an admin UI | LiteLLM |
| Guardrails, semantic caching, prompt management | Portkey |
| A hosted gateway with nothing to operate | OpenRouter, Cloudflare AI Gateway |
| A hundred providers out of the box | LiteLLM or a hosted gateway |
| Provider switching inside one app whose code you control | An SDK such as the AI SDK. You may not need a proxy at all. |
Those are good projects. TinyRouter exists for the case where the feature you want most is being able to understand the whole thing.
Documentation
Reference lives in the repository, versioned alongside the code, so a clone or a source archive describes the commit you actually have rather than whatever is newest.
Configuration
Every key, circuit breaking, request filters, finding model IDs
Routing contract
What happens per request, and what each adapter translates
Operations
Endpoints, metrics, the status page, shutdown, TLS
Development
Build, test, smoke against live providers, releasing
Changelog
What changed in each release, and which changes broke something
Status page
Serving the page and letting it read your gateway