Your keys. Your quota. Your infrastructure.
A self-hostable LLM gateway that speaks OpenAI, Responses and Anthropic over one endpoint, routes across many providers with key rotation and automatic fallback, and runs on your own Cloudflare account — or your own hardware. TierMux maintains the catalog; inference stays entirely on your infrastructure.
- prompts · responses · tool calls
- provider keys · issued API keys
- routing state · usage counters
One endpoint, every protocol
Whatever shape your client speaks, the same URL answers it — and the two translations happen inside the gateway, so a provider that only talks chat-completions still serves Claude Code and Codex CLI.
/v1/messages/count_tokens too.auto's current order — the picker shows what the gateway believes is best first.auto is refused here. The catalog carries no embedding model yet, so today this needs one of your own.
Base URL with or without /v1 — both work, and the bare paths
(/chat/completions, /messages, /responses, /models) are served too.
Which base URL your client wants →
Deploy it
Two targets, one source tree, one schema. The database schema is applied by the app itself on first request — there is no migration command, ever.
The Deploy button forks the repo into your GitHub, provisions D1, deploys, and wires up Workers Builds. Every later push to your fork auto-deploys.
Needs a browser and a Cloudflare account. Nothing installed locally.
Checks your machine, then either runs the gateway right here or deploys it to Cloudflare for you — create D1, upload the script, enable the workers.dev URL — with real upload progress.
Mac: start.command · Windows: start.bat
docker compose up -d # :8788/panel npm install && npm start # Node 22.5+ # built-in node:sqlite — no # compile step, no Redis, no # Postgres. Config: PORT and # DATA_DIR, nothing else.
The packaged release ships a pre-built Worker bundle — deploying needs no wrangler and no build step on the far end.
State survives docker compose down and every image update.
First run
- Open
/panel. With no admin yet you land on/panel/install. - Set an admin username and password. That is the whole form — you are never asked where the catalog lives, because that is not yours to manage: it loads on its own.
- Install locks.
/panel/installstops working, so nobody can claim the account after you. - Add a provider key, create an API key.
/v1/*refuses to serve until at least one API key exists. No open mode, no default password.
Connect a client
Pick what you run and follow the steps. The two SDK families do not take the same base URL, and that is the most common way to end up with a working gateway that looks broken.
Claude Code
https://YOUR-DEPLOYMENT — no /v1- Claude Code is configured by environment, not by a settings page. Put these in your shell profile (
~/.zshrc), or run them in the terminal you launchclaudefrom.Environment shellexport ANTHROPIC_BASE_URL=https://YOUR-DEPLOYMENT export ANTHROPIC_AUTH_TOKEN=tmx_your_key_here export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 export CLAUDE_CODE_ATTRIBUTION_HEADER=0
- The base URL has no
/v1. Claude Code appends/v1/messagesitself, so a URL ending in/v1sends every call to/v1/v1/messagesand 404s — which it then reports as “authentication failed”. - Start it with
claude. Model discovery reads/v1/modelsfrom the gateway, so/modellists what you actually have. - Verify it. This lists the models your gateway will actually serve — a 200 with a non-empty list means the URL, the key and the catalog are all good.Verify curl
curl -s -H "Authorization: Bearer tmx_your_key_here" \ "https://YOUR-DEPLOYMENT/v1/models"
Codex CLI
https://YOUR-DEPLOYMENT/v1- Codex is configured by file. Put this in
~/.codex/config.toml— the top two lines must come before the table header, or TOML folds them inside it.~/.codex/config.toml tomlmodel = "auto" model_provider = "tiermux" [model_providers.tiermux] name = "TierMux Gateway" base_url = "https://YOUR-DEPLOYMENT/v1" env_key = "TIERMUX_API_KEY" wire_api = "responses" requires_openai_auth = false
env_keynames an environment variable, not the key itself. Export it in the shell you start Codex from.Environment shellexport TIERMUX_API_KEY=tmx_your_key_here
wire_api = "responses"is not optional. Codex removed its chat-completions wire protocol, so it talks to/v1/responsesand nothing else — the gateway serves that and translates it down to whatever your services actually speak.- Start it with
codex. It takes its model from the config rather than from/v1/models, soautoabove lets the gateway choose; put a model id there instead to pin one. - Verify it. This lists the models your gateway will actually serve — a 200 with a non-empty list means the URL, the key and the catalog are all good.Verify curl
curl -s -H "Authorization: Bearer tmx_your_key_here" \ "https://YOUR-DEPLOYMENT/v1/models"
Cline
https://YOUR-DEPLOYMENT/v1- Open the ⚙ settings icon in the Cline panel.
- Set API Provider to OpenAI Compatible. This is the field that decides everything else — any other provider ignores the base URL below and calls its own servers.
- Fill in the rest:Base URLhttps://YOUR-DEPLOYMENT/v1API Keytmx_your_key_hereModelauto
- The remaining boxes — Max Output Tokens, Context Window, Image Support, prices — are Cline’s own bookkeeping and the gateway ignores them. Leave them alone unless Cline complains. Cline’s own docs ↗
- Verify it. This lists the models your gateway will actually serve — a 200 with a non-empty list means the URL, the key and the catalog are all good.Verify curl
curl -s -H "Authorization: Bearer tmx_your_key_here" \ "https://YOUR-DEPLOYMENT/v1/models"
Roo Code
https://YOUR-DEPLOYMENT/v1- Open the ⚙ settings icon in the Roo Code panel.
- Set API Provider to OpenAI Compatible. This is the field that decides everything else — any other provider ignores the base URL below and calls its own servers.
- Fill in the rest:Base URLhttps://YOUR-DEPLOYMENT/v1API Keytmx_your_key_hereModelauto
- The remaining boxes — Max Output Tokens, Context Window, Image Support, prices — are Roo Code’s own bookkeeping and the gateway ignores them. Leave them alone unless Roo Code complains. Roo Code’s own docs ↗
- Verify it. This lists the models your gateway will actually serve — a 200 with a non-empty list means the URL, the key and the catalog are all good.Verify curl
curl -s -H "Authorization: Bearer tmx_your_key_here" \ "https://YOUR-DEPLOYMENT/v1/models"
Kilo Code
https://YOUR-DEPLOYMENT/v1- Open the settings ⚙ gear and go to the Providers tab.
- Scroll to the bottom of the provider list and choose Custom provider.
- Fill the dialog in with exactly these:Provider IDtiermuxDisplay nameTierMux GatewayProvider APIOpenAI CompatibleBase URLhttps://YOUR-DEPLOYMENT/v1API keytmx_your_key_here
- Click Submit. The gateway’s models appear in the picker — pick one, or type
autoand let the gateway choose. - Verify it. This lists the models your gateway will actually serve — a 200 with a non-empty list means the URL, the key and the catalog are all good.Verify curl
curl -s -H "Authorization: Bearer tmx_your_key_here" \ "https://YOUR-DEPLOYMENT/v1/models"
Zed
https://YOUR-DEPLOYMENT/v1- Choose the OpenAI-compatible provider type. The gateway is not Zed-specific: any client that can be pointed at an OpenAI-shaped URL works.
- Give it these three, whatever the client calls them — Base URL may appear as “API endpoint”, “Custom URL” or “Provider API”:Base URLhttps://YOUR-DEPLOYMENT/v1API Keytmx_your_key_hereModelauto
- Zed reaches the gateway through its OpenAI-compatible language-model provider. The exact field names move between Zed releases, so take them from Zed’s own settings screen rather than from here — the three values above are all it needs.
- Verify it. This lists the models your gateway will actually serve — a 200 with a non-empty list means the URL, the key and the catalog are all good.Verify curl
curl -s -H "Authorization: Bearer tmx_your_key_here" \ "https://YOUR-DEPLOYMENT/v1/models"
OpenCode
https://YOUR-DEPLOYMENT/v1- Choose the OpenAI-compatible provider type. The gateway is not OpenCode-specific: any client that can be pointed at an OpenAI-shaped URL works.
- Give it these three, whatever the client calls them — Base URL may appear as “API endpoint”, “Custom URL” or “Provider API”:Base URLhttps://YOUR-DEPLOYMENT/v1API Keytmx_your_key_hereModelauto
- OpenCode reads an OpenAI-compatible provider from its config file. The block’s exact shape moves between releases, so copy it from OpenCode’s own docs and drop these three values in.
- Verify it. This lists the models your gateway will actually serve — a 200 with a non-empty list means the URL, the key and the catalog are all good.Verify curl
curl -s -H "Authorization: Bearer tmx_your_key_here" \ "https://YOUR-DEPLOYMENT/v1/models"
OpenAI SDK
https://YOUR-DEPLOYMENT/v1- Point the official SDK at the gateway. Nothing else changes — same methods, same shapes.Python openai
from openai import OpenAI client = OpenAI( base_url="https://YOUR-DEPLOYMENT/v1", api_key="tmx_your_key_here", ) r = client.chat.completions.create( model="auto", messages=[{"role": "user", "content": "hi"}], ) print(r.choices[0].message.content) - Node is the same shape.Node openai
import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://YOUR-DEPLOYMENT/v1", apiKey: "tmx_your_key_here", }); - Streaming, tool calls and
/v1/modelsall work as they do against OpenAI. - Verify it. This lists the models your gateway will actually serve — a 200 with a non-empty list means the URL, the key and the catalog are all good.Verify curl
curl -s -H "Authorization: Bearer tmx_your_key_here" \ "https://YOUR-DEPLOYMENT/v1/models"
DeepSeek agent harness
https://YOUR-DEPLOYMENT/v1- Choose the OpenAI-compatible provider type. The gateway is not DeepSeek-specific: any client that can be pointed at an OpenAI-shaped URL works.
- Give it these three, whatever the client calls them — Base URL may appear as “API endpoint”, “Custom URL” or “Provider API”:Base URLhttps://YOUR-DEPLOYMENT/v1API Keytmx_your_key_hereModelauto
- It speaks OpenAI-shaped chat with tool loops, so it is served directly with no translation. Unknown model names fall through to
autoinstead of dead-ending on a 404 — which is what stops a harness looking broken on its very first request. - Verify it. This lists the models your gateway will actually serve — a 200 with a non-empty list means the URL, the key and the catalog are all good.Verify curl
curl -s -H "Authorization: Bearer tmx_your_key_here" \ "https://YOUR-DEPLOYMENT/v1/models"
Aider
https://YOUR-DEPLOYMENT/v1- Aider takes the endpoint on the command line or from the environment. Either works; the flags win over the variables.Terminal aider
export OPENAI_API_BASE=https://YOUR-DEPLOYMENT/v1 export OPENAI_API_KEY=tmx_your_key_here aider --model openai/auto
- The
openai/prefix tells Aider to route through its OpenAI-compatible path.autoafter it is the gateway’s own model name, so the gateway picks the model rather than Aider. - Pin a specific one instead by putting a model id from your Models tab after
openai/. - Verify it. This lists the models your gateway will actually serve — a 200 with a non-empty list means the URL, the key and the catalog are all good.Verify curl
curl -s -H "Authorization: Bearer tmx_your_key_here" \ "https://YOUR-DEPLOYMENT/v1/models"
Goose
https://YOUR-DEPLOYMENT/v1- Run
goose configureand choose OpenAI as the provider — that is the option that lets you override the host. Any other provider ignores the URL and calls its own servers. - Give it these three, whatever the client calls them — Base URL also appears as “API endpoint”, “Custom URL” or “Provider API”:Base URLhttps://YOUR-DEPLOYMENT/v1API Keytmx_your_key_hereModelauto
- Goose leans on tool calling, so leave the model on
auto: the gateway keeps tool-declaring requests on tool-capable models automatically. - Verify it. This lists the models your gateway will actually serve — a 200 with a non-empty list means the URL, the key and the catalog are all good.Verify curl
curl -s -H "Authorization: Bearer tmx_your_key_here" \ "https://YOUR-DEPLOYMENT/v1/models"
OpenHands
https://YOUR-DEPLOYMENT/v1- In Settings choose the Advanced / custom model option, which is what exposes a base URL field.
- Give it these three, whatever the client calls them — Base URL also appears as “API endpoint”, “Custom URL” or “Provider API”:Base URLhttps://YOUR-DEPLOYMENT/v1API Keytmx_your_key_hereModelauto
- Prefix the model with
openai/if OpenHands asks for a provider-qualified name — the gateway answers on the OpenAI-compatible path. - Verify it. This lists the models your gateway will actually serve — a 200 with a non-empty list means the URL, the key and the catalog are all good.Verify curl
curl -s -H "Authorization: Bearer tmx_your_key_here" \ "https://YOUR-DEPLOYMENT/v1/models"
Continue
https://YOUR-DEPLOYMENT/v1- Add a model block to
config.yaml(orconfig.jsonon older versions) with provider openai, which is the one that honours a customapiBase. - Give it these three, whatever the client calls them — Base URL also appears as “API endpoint”, “Custom URL” or “Provider API”:Base URLhttps://YOUR-DEPLOYMENT/v1API Keytmx_your_key_hereModelauto
- Continue sends the model name through untouched, so
autoreaches the gateway and the gateway chooses. - Verify it. This lists the models your gateway will actually serve — a 200 with a non-empty list means the URL, the key and the catalog are all good.Verify curl
curl -s -H "Authorization: Bearer tmx_your_key_here" \ "https://YOUR-DEPLOYMENT/v1/models"
Open WebUI
https://YOUR-DEPLOYMENT/v1- Settings → Connections → add an OpenAI API connection. That is the connection type that takes a custom URL.
- Give it these three, whatever the client calls them — Base URL also appears as “API endpoint”, “Custom URL” or “Provider API”:Base URLhttps://YOUR-DEPLOYMENT/v1API Keytmx_your_key_hereModelauto
- Open WebUI fetches
/v1/modelsto fill its picker, so everything your gateway can serve shows up there — listed inauto’s current order. - Verify it. This lists the models your gateway will actually serve — a 200 with a non-empty list means the URL, the key and the catalog are all good.Verify curl
curl -s -H "Authorization: Bearer tmx_your_key_here" \ "https://YOUR-DEPLOYMENT/v1/models"
Pi
https://YOUR-DEPLOYMENT/v1- Pi registers providers in
~/.pi/agent/models.json.apimust beopenai-completions— that is the transport the gateway answers on.~/.pi/agent/models.json json{ "tiermux": { "baseUrl": "https://YOUR-DEPLOYMENT/v1", "api": "openai-completions", "apiKey": "$TIERMUX_API_KEY", "models": [ { "id": "auto", "name": "TierMux auto", "reasoning": false, "input": ["text"], "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }, "contextWindow": 128000, "maxTokens": 4096 } ] } } apiKeyinterpolates$ENV_VAR, so the key itself never sits in the file. Export it in the shell you start Pi from.Environment shellexport TIERMUX_API_KEY=tmx_your_key_here
- The
autoentry hands the choice to the gateway. Add more entries with model ids from your Models tab to pin specific ones; thecostblock is Pi’s own bookkeeping and the gateway ignores it. - Verify it. This lists the models your gateway will actually serve — a 200 with a non-empty list means the URL, the key and the catalog are all good.Verify curl
curl -s -H "Authorization: Bearer tmx_your_key_here" \ "https://YOUR-DEPLOYMENT/v1/models"
Hermes Agent
https://YOUR-DEPLOYMENT/v1- Hermes reads
~/.hermes/config.yaml. Add the gateway underproviders:with theopenai_chattransport.~/.hermes/config.yaml yamlproviders: tiermux: api: https://YOUR-DEPLOYMENT/v1 api_key: "${TIERMUX_API_KEY}" transport: openai_chat api_keytakes a${VAR}reference, so export the key rather than writing it into the file.Environment shellexport TIERMUX_API_KEY=tmx_your_key_here
- Leave model discovery on — Hermes probes
/models, which the gateway serves inauto’s current order, so everything you can reach shows up. Then pick the provider:Terminal hermeshermes chat --model auto --provider tiermux # or from inside a session, and persist it: /model auto --provider tiermux --global
- Verify it. This lists the models your gateway will actually serve — a 200 with a non-empty list means the URL, the key and the catalog are all good.Verify curl
curl -s -H "Authorization: Bearer tmx_your_key_here" \ "https://YOUR-DEPLOYMENT/v1/models"
curl
all three shapes- The OpenAI shape:POST /v1/chat/completions curl
curl -X POST "https://YOUR-DEPLOYMENT/v1/chat/completions" \ -H "Authorization: Bearer tmx_your_key_here" \ -H "content-type: application/json" \ -d '{"model":"auto","messages":[{"role":"user","content":"hi"}]}' - The Anthropic shape — note the base URL has no
/v1of its own:POST /v1/messages curlcurl -X POST "https://YOUR-DEPLOYMENT/v1/messages" \ -H "Authorization: Bearer tmx_your_key_here" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{"model":"auto","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}' - The Responses shape, which is what Codex CLI and the Agents SDK speak:POST /v1/responses curl
curl -X POST "https://YOUR-DEPLOYMENT/v1/responses" \ -H "Authorization: Bearer tmx_your_key_here" \ -H "content-type: application/json" \ -d '{"model":"auto","input":"hi"}' - Verify it. This lists the models your gateway will actually serve — a 200 with a non-empty list means the URL, the key and the catalog are all good.Verify curl
curl -s -H "Authorization: Bearer tmx_your_key_here" \ "https://YOUR-DEPLOYMENT/v1/models"
Every deployment shows these same steps with your own URL and a real key already filled in, under Connect in the panel.
How a model is chosen
A fixed, deterministic order — and a substitution is always announced, never silent.
- Exact alias — yours, panel-configured, resolves before model names
- Exact
model_id - Normalized key —
deepseek/deepseek-v4:freeanddeepseek-v4collide, which is what makes cross-provider fallback possible - Wildcard match —
deepseek/*style patterns - Configured fallback alias
auto— unknown model names land here instead of dead-ending on a 404. The substitution is never hidden:x-tiermux-modeland the response body both name what actually ran
Before any of it: requests declaring tools only reach tool-capable models, image-carrying requests only vision-capable ones, and prompts are steered away from models whose context window cannot hold them.
The auto model
Ranks the catalog on what the gateway can observe: the shared catalog's prior (quality tier, free, tools, recommended, reasoning, price, context) combined with this install's own measurements (circuit state, per-model health, remaining budget, observed latency). The tier is the catalog's own band — frontier, strong, mid, small — the same tag the VS Code extension routes on, and a request carrying tools is never led by a small or unjudged model. The request itself selects a profile:
A band name is a profile too — ask for a class of model without learning a single model id:
When an upstream fails
Each result gets a distinct, deterministic response — and the difference between "retry this" and "this is your fault" is decided once, here, not per client.
| Upstream result | Verdict | What happens |
|---|---|---|
| 400 · 413 · 415 · 422 | Caller's fault | Returned verbatim, never retried. Asking three providers produces the same rejection three times. |
| 401 · 403 | Our key is dead | That key parked 15 minutes, request fails over. |
| 402 · 404 · 405 · 501 | Provider cannot serve it | Fail over — the model–provider pair is remembered and skipped next time. |
| 408 · 409 · 425 | Transient | Fail over. |
| 429 | Rate limit | That key parked and the next takes over — rate limiting is a property of the credential, not the provider's health. Honours Retry-After (60 s default); a "daily quota gone" body parks the key until midnight UTC. Never opens the circuit. |
| 5xx · 529 | Provider sick | Counts toward two circuits — the provider's (5 consecutive) and that one model's (3). One broken model never takes its provider offline. |
| Stream dies before first token | Not a commitment | Falls over. HTTP 200 and headers are not commitment. |
| Stream dies after first token | Never falls over | The failure is reported inside the stream — switching mid-answer would duplicate generation and re-run tool calls. |
Recovery is deterministic as well: circuit windows escalate from one minute to thirty, half-open admits exactly one probe, spent rpm/rpd budgets are skipped before they cost a 429, and all health state survives a restart — a Workers isolate recycle does not cost five wasted requests re-learning which provider is sick.
The panel
One admin, six tabs, no YAML. /panel is where provider keys go in, API keys come out, and the routing decisions the gateway has already made are visible after the fact.
Services · health and observed latency
Add a provider key, name it, and switch it off without deleting it. Several keys per provider, rotated in request order.
Circuit state and 429 parking are shown per key, not per provider.
Issue an API key — shown once, only its hash is stored — then copy a ready-made config block for Claude Code, Codex CLI, Cline or the OpenAI SDK.
Your own deployment URL is already filled in.
Aliases, the fallback model, hedging, the catalog source (read-only), and Backup: one JSON file out, one JSON file back in.
The admin account is never part of the export.
Who owns what
TierMux owns the one thing that goes stale in every self-hosted gateway — the provider and model list. You own everything that touches your traffic.
TierMux catalog
- verified providers & models
- capability flags, limits, prices
- public config, no key
~5 min poll
Your gateway
- your provider keys, rotated
- your aliases, your fallbacks
- your issued API keys
- your circuits, your usage data
3 protocols
Your clients
- OpenAI SDK · Codex CLI
- Claude Code
- Cline · Kilo Code
- Zed · anything OpenAI-shaped
Adding a provider to the catalog is a data-only change upstream — your gateway picks it up on its next config poll. No code change, no redeploy, no restart.
What is logged, what is not
Not a promise pinned to a config flag. There is no flag.
In the log
- request id
- model
- selected provider
- key index
- attempt
- status
- latency
- error code
Never, anywhere
- prompts
- responses
- tool payloads
- headers
- provider keys
- issued API keys
- heartbeats
- usage reports
At launch
The public repo lands with a one-click Deploy-to-Cloudflare button. Until then, this catalog already serves the exact configuration payload the gateway consumes — a gateway deployed against it on day one requires no additional setup. Launch updates will be published first on the announcements feed.