An open-source LLM gateway that rides on well-maintained engines instead of rebuilding them, and gives them one API, one UI and one configuration.
- One OpenAI-compatible endpoint for chat, responses, messages, embeddings, speech-to-text and decision models.
- Subscription accounts (Antigravity, Codex, Grok and more) next to API-key providers, pooled.
- Aliases with nested fallback chains: free and subscription capacity first, paid providers when a member answers 429 or 402.
- Keys per consumer, with allowlists on aliases, usage and logs.
- A small web UI, a management API, and
remora.yamlimport/export. - One image, one port, one volume.
You need Docker, curl, jq and openssl. CI runs the shell blocks of this section, in order, on every pull
request and before every release, so they work as written.
Start remora with an admin password you choose, and wait until it is healthy. The first boot can take up to two minutes on a slow host.
export REMORA_ADMIN_PASSWORD="$(openssl rand -hex 16)"
docker run -d --name remora -p 8080:8080 -v remora-data:/data -e REMORA_ADMIN_PASSWORD ghcr.io/47vigen/remora:latest
for _ in $(seq 60); do curl -fsS http://localhost:8080/healthz >/dev/null 2>&1 && break; sleep 2; done
curl -fsS http://localhost:8080/healthzOpen http://localhost:8080 and sign in with that password (echo "$REMORA_ADMIN_PASSWORD" shows it). Then:
- Providers → Add: an OpenRouter provider with your OpenRouter key.
- Optionally, Providers → a subscription provider → Add account: sign in with the provider in your browser, then paste the final address back into remora. Read Subscription accounts and terms of service first.
- Keys → Create: a key for your client. Its value is shown once; copy it then.
The same from a shell, with the management API: sign in (the session cookie goes to a temporary file, removed at the end), add the provider, and create a key that may call any OpenRouter model.
export UPSTREAM_URL=https://openrouter.ai/api UPSTREAM_KEY=sk-or-your-key
COOKIES="$(mktemp)"
jq -n --arg password "$REMORA_ADMIN_PASSWORD" '{password: $password}' |
curl -fsS -c "$COOKIES" -H 'Content-Type: application/json' -d @- http://localhost:8080/api/auth/login
jq -n --arg url "$UPSTREAM_URL" --arg key "$UPSTREAM_KEY" \
'{name: "openrouter", kind: "builtin", type: "openrouter", base_url: $url, keys: [{name: "main", value: $key}]}' |
curl -fsS -b "$COOKIES" -H 'Content-Type: application/json' -d @- http://localhost:8080/api/providers >/dev/null
REMORA_KEY="$(curl -fsS -b "$COOKIES" -H 'Content-Type: application/json' \
-d '{"name":"my-app","allow":["openrouter/*"]}' http://localhost:8080/api/keys | jq -r .value)"
export REMORA_KEY
rm -f "$COOKIES"Call a model with the new key. Models are named provider/model. The engines load a new provider within a few
seconds, so the call is retried a few times.
curl -fsS --retry 10 --retry-delay 2 --retry-all-errors http://localhost:8080/v1/chat/completions \
-H "Authorization: Bearer $REMORA_KEY" -H 'Content-Type: application/json' \
-d '{"model":"openrouter/openai/gpt-4o-mini","messages":[{"role":"user","content":"Say hi"}]}' |
jq -r '.choices[0].message.content'To run remora with Docker Compose instead, use deploy/compose.yaml.
remora reads its settings from the environment. Everything else (providers, accounts, aliases, keys) lives in its
store and is managed in the UI, through the management API, or with remora.yaml.
| Variable | Default | What it does |
|---|---|---|
REMORA_LISTEN |
:8080, in the image 127.0.0.1:8081 |
The address of remora's listener: /v1, /api, /mcp and /healthz. In the image, nginx owns port 8080, serves the UI and proxies those paths here. |
REMORA_DATA |
/data |
The data directory. Mount a volume here. |
REMORA_ADMIN_PASSWORD |
unset | The admin password, used only while no admin exists. After the first boot the stored password wins; change it in Settings. When unset at first boot, a password is generated and written to the log once. |
REMORA_LOG_LEVEL |
info |
debug, info, warn or error. |
REMORA_ENGINE_DIR |
/usr/local/lib/remora/engines |
Where the engine binaries are. The image sets this up; change it only for a custom build. |
REMORA_MASTER_KEY |
unset | The key that encrypts stored provider keys: base64, 32 bytes. When unset, $REMORA_DATA/master.key is generated at first boot. Back it up with the volume: without it, stored provider keys cannot be decrypted. |
REMORA_PORT_ROUTER |
18080 |
The loopback port of the routing engine. |
REMORA_PORT_ACCOUNTS |
18317 |
The loopback port of the subscription engine. |
REMORA_PORT_SPEECH |
18090 |
The loopback port of the speech-to-text adapter. |
REMORA_SONIOX_BASE_URL |
https://api.soniox.com |
The Soniox API the speech-to-text adapter calls. |
REMORA_SONIOX_POLL_INTERVAL |
1s |
How often the adapter polls a running Soniox transcription. |
REMORA_SONIOX_DEADLINE |
30m |
How long the adapter waits for one transcription before it gives up. |
REMORA_SONIOX_SWEEP_INTERVAL |
10m |
How often the adapter deletes leftover Soniox files and transcriptions. |
REMORA_COOLING_429_COUNT |
3 |
How many 429 answers within the window cool an alias member. |
REMORA_COOLING_429_WINDOW |
60s |
The window for REMORA_COOLING_429_COUNT. |
REMORA_COOLING_DEFAULT |
5m |
How long a member cools when the upstream gave no retry hint. |
REMORA_COOLING_POLL |
1s |
How often remora reads the request log for failures. |
REMORA_COOLING_SUBSCRIPTION_DEFAULT |
30s |
How long a subscription account member cools when the upstream gave no retry hint. A retry hint always wins. |
REMORA_QUOTA_INTERVAL |
15m |
How often each subscription account's quota is checked. At least 5m; 0 turns background checks off, and the Refresh buttons still work. |
REMORA_QUOTA_BASE_URL |
unset | Sends every quota check here instead of to the providers. For tests only: the checks carry the accounts' tokens. |
REMORA_LOG_CONTENT |
false |
Keep request and response bodies in the request log. Off by default: the log keeps 7 days of metadata and errors only. |
REMORA_PPROF_ADDR |
unset | A loopback host:port, such as 127.0.0.1:6060, that serves Go profiling (net/http/pprof) on its own listener. Off when unset; any other host is refused. |
The internal ports are bound to 127.0.0.1 inside the container and are never published. Change them only when
they collide with something else in the container.
The remora export, remora apply and remora import commands talk to a running remora:
| Variable | Default | What it does |
|---|---|---|
REMORA_URL |
http://127.0.0.1 on the port of REMORA_LISTEN |
The remora to talk to. |
REMORA_TOKEN |
unset | An API token (Settings → API tokens). Without one, REMORA_ADMIN_PASSWORD signs in. |
REMORA_PROVIDER_<PROVIDER>_<KEY> |
none | A provider key's value. remora.yaml never holds secrets, only env: references with these names; apply reads them from the environment. |
REMORA_KEY_<NAME> |
none | A client key's value, the same way. |
In the last two names, letters are upper-cased and every other character that is not a letter or digit becomes
_: the key main of provider openrouter is REMORA_PROVIDER_OPENROUTER_MAIN.
/data/remora.db remora's store (SQLite), the source of truth
/data/master.key the generated master key, when REMORA_MASTER_KEY is unset
/data/<engine>/ one directory per engine: its configuration, request logs and subscription account credentials
Back up the whole volume, including master.key.
one port (default 8080), nginx in front
client ──► remora ──┬── /v1/* inference proxy ──► routing engine (127.0.0.1) ──┬── API-key providers (OpenRouter, ...)
│ see Credits ├── subscription engine (127.0.0.1) ──► Antigravity, Codex, Grok, ...
│ │ see Credits
│ └── speech-to-text adapter (127.0.0.1, inside remora) ──► Soniox
├── /api/* management API (OpenAPI)
└── / web UI (static files, brotli and gzip built ahead)
remora is one image with one exposed port. Inside it, remora supervises the engines as child processes bound to loopback ports, writes their configuration from its own store, and restarts them when they crash. Clients and admins only ever talk to remora: one key format, one error format, one API, one UI.
Using consumer subscriptions through a proxy may conflict with the terms of service of the companies that offer them. Read those terms and decide for yourself; remora does not change them.
The Quota page shows, for every Antigravity, Codex and Claude account, how much of each quota window is left and
when it resets. Other subscription providers show their status and cooldowns only. remora checks one account at a
time, at least 2 seconds apart, through each account's own proxy, and never by sending a prompt. Codex and Claude
accounts that served traffic recently reuse the quota headers of that traffic instead of being checked. At the
default REMORA_QUOTA_INTERVAL, about 20 Antigravity accounts cost about 80 upstream calls an hour. "Unknown" means
the provider did not report that number. A manual refresh of one account runs at most once a minute.
Codex models served through a subscription do not accept temperature, top_p or max_tokens; those
parameters are dropped, and remora says so: the x-remora-dropped-params response header lists every top-level
request key the Codex backend did not receive as sent. It is absent when nothing was dropped or when any other
provider served the request.
remora announces every request rewrite it knows of; none is silent. When a Codex member (x-remora-served-by
starts with codex/) serves a request, the header is a comma-separated, sorted list. It is set on streaming and
non-streaming answers alike, but not on the Responses WebSocket route, where it would have to be sent per message.
/v1/chat/completions: every key exceptmodel,messages,stream,stream_options,reasoning_effort,response_format,text,tool_choice,tools,service_tierandparallel_tool_calls. Sotemperature,top_p,top_k,max_tokens,max_completion_tokens,stop,user,n,seedand the rest are all reported. Alsoservice_tierunless it ispriority,fastorultrafast(any case), andparallel_tool_callsunless it istrue, because Codex always runs tool calls in parallel./v1/responses:max_output_tokens,max_completion_tokens,temperature,top_p,truncation,prompt_cache_options,prompt_cache_retention,userandcontext_management;service_tierunless it ispriority,fastorultrafast(any case);parallel_tool_callsunless it istrue; andstoreunless it isfalse./v1/messages:temperature,top_pandmax_tokens.- Any other endpoint: nothing.
The list is checked against the engine on every engine update by contract tests. What remora does and does not do,
each row with the test that proves it, is in docs/parity.md.
A subscription model in an alias may carry an explicit thinking level as a suffix: (minimal), (low),
(medium), (high), or a token budget such as (1024), where (0) turns thinking off. For example, an alias
flash-fast with the member antigravity/gemini-3-flash(low) answers faster than one without the suffix.
- The suffix applies only to subscription models; the alias API refuses it on any other provider.
- For that member it overrides the client's
reasoning_effort. A model without thinking ignores it. - No existing alias changes, and there is no default level: a faster tier is a separate, named alias.
Six strategies decide which member of an alias, and which account of a subscription provider, a request tries first. "Live" means not cooling and not disabled. "Busy" means requests started in the last 60 seconds, counted in memory (a restart resets it). Ties go to the declared order.
| strategy | first pick | fallback after the first pick |
|---|---|---|
priority |
the first live member in order / the highest priority tier, rotating inside it | the rest, in order |
fill-first |
the first live member in order, staying on it until it cools | the rest, in order |
round-robin |
a rotation with one cursor per alias / per provider | the other live members, rotated from the pick |
random |
uniform over the live members | the other live members, rotated from the pick |
p2c |
two distinct live members at random; the less busy one wins | the other live members, rotated from the pick |
least-used |
the least busy live member | the other live members, rotated from the pick |
- Aliases. Set
strategyon the alias (form, API orremora.yaml); omitted meanspriority. For an alias,priorityandfill-firstbehave the same: cooling already moves a request past a member that stopped answering. A WebSocket session on/v1/responsesalways uses the declared order, because its headers are fixed before any model is known. With a spreading strategy, the turns of one conversation may land on different members. - Accounts. Each subscription provider has one strategy, default
round-robin, set on its page in the console, throughPUT /api/accounts/strategies/{provider}, or inremora.yamlasaccount_strategies: {codex: p2c}. Each account has a priority number, default 0, editable on the same page:priorityhonours the numbers and takes turns inside equal ones;fill-firstuses one account at a time, highest priority first, then the oldest account. The other four treat every account alike. A conversation that already has an account keeps it while that account is live; the strategy decides first turns and moves. - Upgrading. Aliases stored with
round-robinbefore this version picked a member at random; they were migrated torandom, which keeps that behaviour.round-robinnow rotates.
remora serves its management tools over the Model Context Protocol, so Claude Code or any MCP client can operate it:
at /mcp on the same listener (stateless Streamable HTTP), and over stdio with remora mcp. Every tool calls the
management API, with the same authentication, validation and errors.
The only credential is an API token from Settings → API tokens, sent as Authorization: Bearer. The console's
session cookie is not accepted on /mcp.
claude mcp add --transport http remora https://<host>/mcp --header "Authorization: Bearer rmt_..."
claude mcp add --transport stdio --env REMORA_URL=https://<host> --env REMORA_TOKEN=rmt_... remora -- remora mcp| tool | kind | covers |
|---|---|---|
remora_overview |
read | health, version, counts, accounts out of the pool, cooling, quota per provider |
remora_inspect |
read | list or show providers, accounts, aliases (with the compiled chain), keys (with usage), API tokens, cooling, quota, account strategies, logins |
remora_logs |
read | search the request log, or one entry; bodies cut to 4096 bytes |
remora_stats |
read | totals over a range, grouped by key, alias, provider or model |
remora_alias_compile |
read | preview the chain an alias definition compiles to |
remora_provider_test |
read | test one provider; sends one real request |
remora_config_export |
read | the current remora.yaml |
remora_config_plan |
read | the changes a whole or partial remora.yaml would make, and its plan_hash |
remora_config_apply |
write, destructive | apply a planned document; refuses with the fresh plan if the configuration changed since |
remora_delete |
write, destructive | delete a provider, alias, client key or account |
remora_account |
write | enable, disable, set priority, reset cooldown, refresh quota, sign in, import a credential |
Providers, aliases, keys and both strategy kinds change only through plan then apply. prune is off by default
here, so a partial document adds or updates exactly what it names and deletes nothing; a full replace says
prune: true, and its delete rows are what you confirm. Claude Code's permission rules match
mcp__remora__<tool>, so the read tools can be allowed and the writes kept on ask.
Every release image is signed with cosign (keyless, by the release workflow) and carries an SBOM and build provenance. Verify a digest with:
cosign verify ghcr.io/47vigen/remora@<digest> \
--certificate-identity-regexp '^https://github\.com/47vigen/remora/\.github/workflows/release\.yml@refs/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.comThe design, the plans, the research and the maintainer notes are indexed in docs/README.md.
remora composes two engines, and never forks them for long:
- Bifrost by Maxim AI (Apache-2.0) is the routing engine: inference routing and same-request fallback across providers, API-key providers, embeddings, transcription transport, decision models, and the request logs and analytics behind remora's logs and stats.
- CLIProxyAPI (MIT) is the subscription engine: OAuth sign-in for subscription accounts, account pooling, token refresh, and per-account cooldown and quota.
Their licenses and notices are in THIRD_PARTY_NOTICES.md. Every change remora carries
on top of an engine, with its upstream pull request, is in docs/patches.md.