Skip to content
47vigenPublic

About

Open-source LLM gateway that rides on maintained engines: one API, one UI, one config.

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

 

History

71 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

remora

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.

What it does

  • 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.yaml import/export.
  • One image, one port, one volume.

Quick start

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

Open http://localhost:8080 and sign in with that password (echo "$REMORA_ADMIN_PASSWORD" shows it). Then:

  1. Providers → Add: an OpenRouter provider with your OpenRouter key.
  2. 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.
  3. 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.

Configuration

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.

The data directory

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

Architecture

                         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.

Subscription accounts and terms of service

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.

Account quota

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.

Dropped parameters

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 except model, messages, stream, stream_options, reasoning_effort, response_format, text, tool_choice, tools, service_tier and parallel_tool_calls. So temperature, top_p, top_k, max_tokens, max_completion_tokens, stop, user, n, seed and the rest are all reported. Also service_tier unless it is priority, fast or ultrafast (any case), and parallel_tool_calls unless it is true, 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, user and context_management; service_tier unless it is priority, fast or ultrafast (any case); parallel_tool_calls unless it is true; and store unless it is false.
  • /v1/messages: temperature, top_p and max_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.

Thinking level

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.

Routing strategies

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 strategy on the alias (form, API or remora.yaml); omitted means priority. For an alias, priority and fill-first behave the same: cooling already moves a request past a member that stopped answering. A WebSocket session on /v1/responses always 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, through PUT /api/accounts/strategies/{provider}, or in remora.yaml as account_strategies: {codex: p2c}. Each account has a priority number, default 0, editable on the same page: priority honours the numbers and takes turns inside equal ones; fill-first uses 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-robin before this version picked a member at random; they were migrated to random, which keeps that behaviour. round-robin now rotates.

MCP

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.

Verifying an image

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

Documentation

The design, the plans, the research and the maintainer notes are indexed in docs/README.md.

Credits

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.

License

Apache-2.0.

About

Open-source LLM gateway that rides on maintained engines: one API, one UI, one config.

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages