Skip to main content

Robin

v0.1 — early, and honest about it: see What it is not.

Round-robin your LLM subscriptions. One local OpenAI-compatible endpoint in front of every plan you hold — so the 5-hour and weekly windows you already pay for all get used, instead of one plan burning out while the others expire idle.

Born for DeepSeek refugees: instead of hunting for one plan with enough capacity, find several whose capacity adds up — and let them add up.

your client  ──►  Robin  ──┬──►  ark/deepseek-v4-flash         (plan A: own window)
(Claude Code, opencode,    │     opencodego/deepseek-v4-flash  (plan B: own window)
 dsh — anything that       │     …add as many plans as you hold
 speaks /v1/chat/          └──►  deepseek/deepseek-v4-flash     (pay-as-you-go, last)
 completions)

What's actually different

Sticky routing is not new and neither is load balancing. LiteLLM's router spreads proactively (weighted shuffle, lowest-TPM, latency) and can pin by API key or session_id; Portkey has sticky load balancing on a configured hash_fields; Kong does consistent hashing on a header. The key-pool rotators (dsh-api-key-pool and friends) are the ones that move only on failure.

Two things are still unserved:

  • Those gateways spread by tokens, requests or dollars — never by a subscription window. A 5-hour bucket that refills when the provider says so, and is worth nothing if it expires unspent, is a different unit from TPM. If you hold three plans, that unit is the whole reason you hold them.
  • Every sticky implementation above needs the caller to hand over an identity — an API key, a session_id, a header. Robin derives one from the request itself, so an unmodified OpenAI client gets stickiness with no cooperation at all.

(Closest prior art, and worth your time if it fits: claude-relay-service and ccflare do window-aware account pooling for Claude subscriptions specifically — claude-relay-service derives its session hash much the way Robin does. Robin's difference is that it is local, single-process, and works for any OpenAI-compatible plan.)

Robin moves on conversation boundaries, and gets two things right:

  • Prefix stickiness. Provider prefix caches are per-provider, and agent workloads replay the whole transcript every turn (measured on one real workload — AItelier's, where this routing layer came from: 26:1 prefill:decode at an 89.4% cache hit rate). Rotating per call converts cached input into full-price input and costs more than a second plan saves. Robin keys on the conversation prefix: the same conversation stays on the same endpoint, a NEW one starts on the next plan. Quota spreads; caches survive.
  • Spent-window parking. A plan whose window is spent is parked until the provider's own stated reset instant, keyed on provider/model — so later requests skip it instead of re-paying the same doomed call. Burst throttling and an exhausted plan are both HTTP 429; only the prose says which.

Pay-as-you-go endpoints are declared as fallback and never rotate forward: they are what turns "everything stops until the window resets" into "the next request goes elsewhere", and money should be the last resort, not a peer.

What it is not

No cost tracking, no request logging, no persistence across restarts, one process. If you want an observability plane or a team gateway, LiteLLM and Portkey are the grown-ups. Robin is one job: spend the windows you already bought.

OpenAI protocol only, in and out. Robin speaks /v1/chat/completions and forwards to /v1/chat/completions. It does not accept Anthropic-format (/v1/messages) requests, and it cannot route to an endpoint that only serves that shape.

In practice that is a per-reseller limit, not a per-model one: OpenCode Go, for instance, serves DeepSeek/GLM/Kimi over the OpenAI shape but puts Qwen-Max and MiniMax behind /v1/messages — while the same models are OpenAI-compatible straight from the vendor (Alibaba's DashScope compatible-mode/v1), which is just another provider entry here. So the gap is "that plan's copy of that model", not the model. Adding the Anthropic shape is additive (a provider gains an api: field) rather than a rewrite; it is on the list if people want it.

For pooling Claude subscriptions specifically, claude-relay-service and ccflare already do that job well.

Nothing here is DeepSeek-specific — any OpenAI-compatible base URL works; the shipped examples just happen to be what the author holds.

Configuration

Robin reads the same two tables AItelier uses, so an existing deployment can point a client at Robin with no migration:

// llm_providers.json — a provider is (base_url, key NAME), NOT a vendor.
// Hold two plans with the same vendor? Register it twice under different names.
{
  "ark":        {"base_url": "https://ark.example/api/v3", "api_key_env": "ARK_API_KEY"},
  "opencodego": {"base_url": "https://opencode.ai/zen/go/v1", "api_key_env": "GO_API_KEY"}
}
// model_routes.json — a MODEL is an ordered list of ENDPOINTS.
{
  "flash": {"rotate":   ["ark/deepseek-v4-flash", "opencodego/deepseek-v4-flash"],
            "fallback": ["deepseek/deepseek-v4-flash"]}
}

Keys are read from files (~/.robin-secrets/<NAME>, or $ROBIN_SECRETS_DIR), never from the config and never from the environment — so a config is safe to share, and a subprocess that inherits the environment does not receive your keys. See .env.example. A key name with no file means "I do not hold this plan": Robin skips that endpoint rather than burning a call that cannot succeed.

Run it

pip install -e .

cp llm_providers.example.json llm_providers.json   # who you can call
cp model_routes.example.json  model_routes.json    # which plans serve what

mkdir -p ~/.robin-secrets && chmod 700 ~/.robin-secrets
( umask 077; printf '%s' "<your-key>" > ~/.robin-secrets/ARK_API_KEY )

robin --check      # says which endpoints are usable, and why the rest are not
robin              # http://127.0.0.1:8080/v1
pip install robin-router && robin --init && robin --check

Point any client that speaks /v1/chat/completions at http://127.0.0.1:8080/v1 and ask for a route name (flash, pro) as the model. Streaming works. /v1/completions and /v1/embeddings do not exist yet.

endpoint what it's for
GET /health routes and providers loaded
GET /stats rotation cursors, live conversations, and what is parked — the answer to "why is everything landing on the expensive endpoint"
GET /v1/models the route names, for clients that populate a picker
POST /reload re-read both tables without dropping conversations or parks; a broken edit is refused and the old config keeps serving
POST /unpark release a park (?endpoint=…, or all) — parking is inferred from provider prose and can be wrong
x-robin-endpoint header, robin.served_by in the body which plan actually served this turn

Key files need no reload: they are read per call, so writing a new plan's key takes effect on the next request.

Settings are environment variables (ROBIN_HOST, ROBIN_PORT, ROBIN_SECRETS_DIR, ROBIN_PROVIDERS, ROBIN_ROUTES, ROBIN_API_KEY_FILE) — see .env.example for what each does. Robin does not read a .env file; export them, or put them in whatever unit file starts it.

Robin listens on loopback by default and holds every key you own. Put auth in front of it (or set ROBIN_API_KEY_FILE) before binding it to anything else.

Licence

MIT.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

robin_router-0.1.0.tar.gz (39.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

robin_router-0.1.0-py3-none-any.whl (29.8 kB view details)

Uploaded Python 3

File details

Details for the file robin_router-0.1.0.tar.gz.

File metadata

  • Download URL: robin_router-0.1.0.tar.gz
  • Upload date:
  • Size: 39.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.3

File hashes

Hashes for robin_router-0.1.0.tar.gz
Algorithm Hash digest
SHA256 02d2fb36f3ed8515558997600e3694fd218ebd67515fd6b42f699022f242b33e
MD5 3d1123511b0f6a038736d5c09873c59c
BLAKE2b-256 47263e5e3a4484f68b1e4d1e07ed4b784ab487290624db52e30a3b084b3a7989

See more details on using hashes here.

File details

Details for the file robin_router-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: robin_router-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 29.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.3

File hashes

Hashes for robin_router-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7c9dad94177579db7c763c740cba5b489021bf96d45310063ab99904368e10ad
MD5 ebb84702b197d8e9586dfa4c5ab4d36b
BLAKE2b-256 3e7e0bec661a0df40f7534fc6c336153cfca93204abf34cb6eb30f768bbddc91

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page