Run your whole SubRouter account from the terminal.
Every documented account-management endpoint, one binary, no browser required. The same core is also a Python and a TypeScript package.
- API keys โ All four kinds: smart routing, a pinned provider, shared pools, official channels. Pin a model to a provider, set a quota, a price ceiling or an IP allowlist.
- Directories โ The model directory and the provider directory, with search, sort and filters. Every entry opens on its own.
- Usage โ Every log field the platform returns, with the throughput the console shows, and the totals of any window.
- The rest โ Top-ups, referrals, async task records, provider subscriptions, shared plans, the merchant and station consoles.
- Output โ Bordered tables for people, JSON and YAML for scripts. The payload goes to stdout, the warnings to stderr.
- Speed โ Every response is cached in SQLite. A week of logs takes about a minute once, then under a second.
- From a program โ The same core as a Python and a TypeScript package.
- At a glance
- The problem it solves
- Install
- Quick start
- The four kinds of key
- Searching and sorting
- Single listings
- Command map
- Endpoint coverage
- Output formats
- Caching
- Credentials
- Python and TypeScript
- Safety
- Development
SubRouter has a capable web console, but everything in it is a click. If you want to rotate a key, find the cheapest route for a model, or pull yesterday's spend into a spreadsheet, you are back to the browser and the mouse.
subrouterctl speaks to the same account management API the console uses,
using your system access token. The browser is needed exactly once, when
you authorize the CLI. After that, everything is scriptable.
$ subrouterctl token list --kind fixed
โโโโโโโโโโฌโโโโโโโโโโโโโฌโโโโโโโโฌโโโโโโโโโโโฌโโโโโโโโโโโโฌโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโ
โ ID โ NAME โ KIND โ PROVIDER โ PROVIDERS โ EXPIRES โ KEY โ STATUS โ
โโโโโโโโโโผโโโโโโโโโโโโโผโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโค
โ 392958 โ demo-fixed โ fixed โ demo โ โ -1 โ sk-Qk7******Rt2x โ enabled โ
โ 392939 โ acme-fixed โ fixed โ acme โ โ -1 โ sk-Nm3******Hs6v โ enabled โ
โโโโโโโโโโดโโโโโโโโโโโโโดโโโโโโโโดโโโโโโโโโโโดโโโโโโโโโโโโดโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโgit clone https://github.com/Apothic-AI/subrouterctl
cd subrouterctl
make install # -> $GOBIN/subrouterctlOr build in place:
make build # -> ./subrouterctlBuilding needs Go 1.27 or newer. The result is a single file you can copy anywhere. The Python and TypeScript packages sit on the same source.
# 1. authorize the CLI (one time, opens a URL you confirm in the browser)
subrouterctl auth login
# 2. see where you stand
subrouterctl account info
# 3. create a key that always routes through one provider
subrouterctl token create --type fixed --name "demo only" --provider demo
# 4. the key is masked in the table; --show-secrets prints it in full
subrouterctl token create --type smart --name "router" --show-secretsThe credentials land in ~/.subrouter/credentials.json, so every later
command works without an argument.
SubRouter's console hides the most important idea behind four buttons.
subrouterctl names them explicitly, and token create --type picks one.
--type |
Routing group | What it does |
|---|---|---|
smart |
subrouter |
Every subscribed source. Cheapest healthy route wins, auto failover. |
official |
subrouter |
Platform official channels only. |
shared |
subrouter |
Subscribed shared account pools only. |
fixed |
provider:<s> |
One pinned provider. No cross-provider failover. |
# smart routing across two providers, price first
subrouterctl token create --type smart --name "router" \
--providers demo,acme --route-preference price_first,stability_first
# pinned to a single provider
subrouterctl token create --type fixed --name "demo only" --provider demo
# shared account pools only, with a discount ceiling
subrouterctl token create --type shared --name "pools" --shared-max-discount 2500
# official channels only
subrouterctl token create --type official --name "official"Pin a single model to a specific provider with --model-route:
subrouterctl token create --type smart --name "pinned" \
--model-route "gpt-5.4=demo" --model-route "claude-sonnet-4-6=acme"--route-preference takes an ordered list. The first criterion that separates
two candidates wins.
stability_first authenticity_first provider_credit_first first_token_first price_first
--sort-mode picks how candidates of equal standing are ordered:
token_price_first (default) or per_call_price_first.
token create and token update take the same flags. On an update, a flag
that is not given keeps the value of the stored key.
| Flag | Meaning |
|---|---|
--type |
smart, official, shared or fixed. |
--name |
The name of the key. |
--provider |
The provider slug a fixed key is pinned to. |
--providers |
Comma separated slugs a smart key may use. |
--models |
Comma separated model whitelist. Empty means every model. |
--model-route |
model=provider1,provider2. Repeatable. |
--route-preference |
The ordered routing criteria, comma separated. |
--sort-mode |
token_price_first or per_call_price_first. |
--load-balance |
Spread the traffic over the candidates. |
--include-cache-hit |
Weigh the cache hit rate. On by default. |
--include-provider-self |
Allow routes through provider operated channels. |
--include-official-channels |
Allow routes through platform official channels. |
--include-shared-subscriptions |
Allow routes through subscribed shared pools. |
--official-max-discount |
The highest discount factor of an official channel. |
--shared-max-discount |
The highest discount factor of a shared plan. |
--official-channel |
Pin the key to one official channel id. |
--unlimited |
Give the key unlimited quota. On by default. |
--quota-usd |
A quota in dollars. |
--quota-units |
A quota in raw platform units. |
--expire |
never, 30d, 12h or a unix timestamp. |
--allow-ips |
An IP or CIDR allowlist, one per line. |
--cross-group-retry |
Retry the next group when the current one fails. |
--group |
Override the routing group. |
--key-group |
Attach the key to a key group id. |
Every directory, log feed and task list takes the same three flags.
| Flag | Meaning |
|---|---|
-q, --search |
Free text search. Matched against names, slugs and categories. |
--sort |
Sort field, for example price, rating, used, time. |
--order |
asc (default) or desc. |
--limit |
Maximum rows to print. 0 prints everything. |
--page |
Page number for the server-side paged endpoints. |
--page-size |
Rows per page. 50 by default. |
# cheapest routes for gpt models, most expensive first
subrouterctl model list --search gpt --sort price --order desc
# best rated providers
subrouterctl provider list --sort rating --order desc
# the keys that burned the most quota
subrouterctl token list --sort used --order desc
# logs for one model, most expensive call first
subrouterctl usage logs --model deepseek --sort cost --order desc--sort is applied locally, so the CLI fetches every matching page first.
Search is pushed to the server where the endpoint supports it.
The usage log carries the time to first token and the request duration, and
subrouterctl shows the generation throughput the same way the SubRouter
console does in its Time/first word column: the completion tokens divided
by the total request duration.
$ subrouterctl usage logs --limit 3
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโฌโโโโโโโโฌโโโโโโโโโฌโโโโโโโโโโฌโโโโโโโโโโฌโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ TIME โ KEY โ MODEL โ GROUP โ COST โ TOKENS โ TIME(s) โ TTFT(s) โ TPS โ REQUEST โ
โโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโผโโโโโโโโผโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ 2026-11-13 08:36 โ demo-fixed โ deepseek-v4.1-flash-1017 โ provider:demo โ $0.00 โ 275744 โ 49.0 โ 35.9 โ 47.6 โ 20261113133557142351560... โ
โโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโดโโโโโโโโดโโโโโโโโโดโโโโโโโโโโดโโโโโโโโโโดโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ--sort tps orders by that figure and --sort ttft by the time to first
token.
subrouterctl usage logs --sort tps --order desc
subrouterctl usage logs --sort ttft --order ascOne log in full, including every field the platform returned and the whole
performance payload it sends in the other field:
$ subrouterctl usage log 20261113133557142351560demo000
Log 20261113133557142351560demo000
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Id 1042
Created 2026-11-13 08:36:46
Key demo-fixed
Model deepseek-v4.1-flash-1017
Group provider:demo
IP 203.0.113.7
Prompt tokens 273410
Completion tokens 2334
Duration 49.0s
Time to first token 35.9s
Throughput 47.6 t/s
Streaming yes
Cost $0.00
Upstream request id 00000000-0000-4000-8000-000000000000
performance payload
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโ
โ FIELD โ VALUE โ
โโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโค
โ frt โ 35949 โ
โ cache_tokens โ 266368 โ
โ provider_slug โ demo โ
โ site_input_price โ 0.016911764705882355 โ
โโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโThe JSON and YAML output carries every field the API returned, so nothing is lost between the platform and a script.
Every directory entry can be opened on its own. A model is addressed by its numeric id, a provider by its slug.
$ subrouterctl model show 27613
Model glm-5.3-prime
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Id 27613
Provider Globe Labs
Provider slug globe
Upstream model glm-5.3-prime
Category chat
Billing mode ratio
Input price 0.4800 CNY
Output price 1.5086 CNY
Official input 1.4000
Cache read 0.0960
Context length 1000000
Average latency 1432 ms
Last call 2026-11-13 09:30:24
Endpoints {"openai":{"path":"/v1/chat/completions","method":"POST"}, ...}
Description GLM-5.3 high speed inference version$ subrouterctl provider show demo
Provider Demo
โโโโโโโโโโโโโโโโโโโโโโโโโโ
Id 101
Slug demo
Status active
Rating 5.00
Uptime 99.62%
Models 12 listed, 12 public
Credit score 500
Max RPM 100000
Subscribers 666
Description Domestic Model-Openai
โโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโฌโโโโโโโโโฌโโโโโโโโโ
โ MODEL โ PROVIDER โ IN/M โ OUT/M โ
โโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโค
โ deepseek-v4.1-flash โ Demo โ 0.1150 โ 0.4600 โ
โโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโดโโโโโโโโโดโโโโโโโโโprovider models <slug> prints only the catalogue, and it accepts the same
search and sort flags:
subrouterctl provider models demo --search deepseek --sort input --order desc--from and --to accept a date, a relative age or a unix timestamp.
subrouterctl usage logs --from 2026-11-01 --to 2026-11-07
subrouterctl usage stats --from 24h --to now
subrouterctl usage data --from 7d --to nowThe platform refuses windows longer than 31 days.
The range of a log listing is cached, so a window that was fetched before comes back from disk the next time. Only the parts of the window that are missing are fetched from the platform.
subrouterctl
โโโ auth login ยท status
โโโ account info ยท update ยท groups ยท models ยท subrouter-models
โ topup-info ยท topups ยท redeem ยท balance
โ notifications ยท aff ยท status
โโโ usage logs ยท log ยท stats ยท data ยท export
โโโ token list ยท show ยท create ยท update ยท delete ยท delete-batch ยท types
โโโ model list ยท show ยท routing ยท pricing ยท options
โโโ provider list ยท show ยท models ยท subs ยท subscribe ยท unsubscribe
โ subscribe-all ยท unsubscribe-all ยท sub-config ยท shared
โโโ shared list ยท subscribe ยท cancel
โโโ subscription list ยท plans ยท preference ยท pay
โโโ task list ยท show
โโโ merchant self ยท models ยท model-create ยท model-update ยท model-batch
โ price-preview ยท channels ยท channel-test ยท earnings ยท payouts ยท register
โโโ station self ยท dashboard ยท models ยท global-models ยท packages
โ customers ยท key-groups ยท shared ยท official-channels
โ logs ยท earnings
โโโ bridge the machine interface, on stdin and stdout
merchant and station cover the provider (ๅๅฎถ) and distributor (ๅ็ซ็ซ้ฟ)
roles. A regular account receives 403 ๆ ๆ่ฟ่กๆญคๆไฝ from them, which is the
expected answer.
Every endpoint in the Account Management API reference has a command. All 35 of them.
| Documented endpoint | Command |
|---|---|
GET /api/status |
account status |
GET /api/user/self |
account info |
PUT /api/user/self |
account update |
GET /api/user/topup/info |
account topup-info |
GET /api/user/topup/self |
account topups |
GET /api/log/self |
usage logs |
GET /api/log/self (by request id) |
usage log <request-id> |
GET /api/log/self/stat |
usage stats |
GET /api/data/self |
usage data |
GET /api/subscription/plans |
subscription plans |
GET /api/subscription/self |
subscription list |
PUT /api/subscription/self/preference |
subscription preference |
POST /api/subscription/{stripe,creem,epay}/pay |
subscription pay |
GET /api/marketplace/providers |
provider list |
GET /api/marketplace/subscriptions |
provider subs |
POST /api/marketplace/subscribe |
provider subscribe |
POST /api/marketplace/subscribe-all |
provider subscribe-all |
DELETE /api/marketplace/subscribe/{provider_id} |
provider unsubscribe |
DELETE /api/marketplace/subscriptions |
provider unsubscribe-all |
GET /api/marketplace/subscription-config |
provider sub-config |
PUT /api/marketplace/subscription-config |
provider sub-config --auto-subscribe |
GET /api/shared-subscriptions/plans |
shared list |
POST /api/shared-subscriptions/plans/{id}/subscribe |
shared subscribe |
DELETE /api/shared-subscriptions/plans/{id}/subscribe |
shared cancel |
GET /api/marketplace/models |
model list |
GET /api/marketplace/models/{id} |
model show |
GET /api/marketplace/providers/{slug} |
provider show |
GET /api/user/self/groups |
account groups |
GET /api/user/models |
account models |
GET /api/user/self/subrouter/models |
account subrouter-models |
GET /api/token/ |
token list |
POST /api/token/ |
token create |
PUT /api/token/ |
token update |
GET /api/token/{id} |
token show |
DELETE /api/token/{id} |
token delete |
POST /api/token/batch |
token delete-batch |
Beyond the reference, the CLI also reaches the async task records
(GET /api/task/self), the referral and top-up endpoints, the shared-provider
routes, the marketplace pricing sheet, the log CSV export and the full
merchant and station surfaces.
subrouterctl account info # bordered table
subrouterctl -o json account info # machine readable
subrouterctl -o yaml token list --limit 3 # machine readable$ subrouterctl -o json account info
{
"id": 10001,
"username": "demo-user",
"display_name": "demo-user",
"email": "",
"role": 1,
"group": "default",
"quota": 25000000,
"used_quota": 125000000,
"request_count": 13724,
"status": 1,
"aff_code": "DEMO",
"aff_quota": 0,
"inviter_id": 0,
"deleted_at": null,
"created_time": 0
}Every list command honours --output, and the JSON and YAML payloads use the
same field names as the platform's own API.
The payload goes to the standard output and everything else, the warnings and the errors, goes to the error stream. A JSON payload therefore stays machine readable even when the command had something to say.
Every response is written through to a local SQLite database the moment it arrives, so the cache is always warm. Where a response is read from is a separate decision:
| Situation | Where the data comes from |
|---|---|
| A normal run | The network, always |
--cache-ttl 5m and the entry is that fresh |
The cache |
| The network is down | The cache, with a warning |
| Usage logs | The cache, see below |
| Flag | Meaning |
|---|---|
--cache-ttl |
Serve cached responses younger than this. 0 reads live data. |
--cache-dir |
Where the cache lives. Default ~/.cache/subrouterctl. |
--cache-clear |
Remove every cached entry and exit. |
Nothing is ever served stale without either --cache-ttl or a dead network,
and the offline fallback always says so:
$ subrouterctl --base-url http://127.0.0.1:9 account status
! served GET /api/status from the local cache because the network is unavailable
Site
โโโโโโโโ
Name SubRouter
โฆA log listing names a time range, so the cache keeps the rows together with the range of time they cover. A range that is already cached is drawn from disk, and only the parts that are missing are fetched:
subrouterctl usage logs --from 7d # fetches the range it does not have
subrouterctl usage logs --from 7d # instant: the range is coveredA range that reaches into the present always keeps a fresh tail, so new calls still show up. When a range cannot be fetched, the cached rows are still shown and the gap is reported:
$ subrouterctl usage logs --from 7d
! part of the requested range could not be fetched: GET /api/log/self: โฆWide ranges are fetched in parallel chunks, so a week of logs takes about a minute the first time and under a second afterwards.
The cache lives in ~/.cache/subrouterctl/cache.db, a SQLite database with
mode 0600. Payloads are compressed, so a week of logs takes tens of
megabytes instead of hundreds.
The platform hands out the full relay key of every key on the account, so a cached response would be a file full of live credentials. The cache is scrubbed on the way in: every secret value it holds is replaced by a placeholder of the same length, and the cached copy still decodes. The command itself is unaffected and still hands you the real key.
$ subrouterctl token list --limit 1 # prints the key, masked
$ subrouterctl -o json token list --limit 1 # prints the key in full--cache-clear empties the cache, which is the way to drop the keys that a
version of the tool before this one may have left on disk.
The public directories are slow to fetch. The model directory holds around 2100 models spread over 43 pages, and a sorted listing needs all of them. Those pages are requested in parallel: the full 2100 model sort went from about 50 seconds to under 10, and a warm cache brings it down to a second.
subrouterctl auth login runs the device authorization flow and writes
~/.subrouter/credentials.json with mode 0600. It never prints the secrets
it stores.
Every value can also come from the environment, which is what you want in CI.
export SUBROUTER_ACCESS_TOKEN=โฆ
export SUBROUTER_USER_ID=10001
export SUBROUTER_BASE_URL=https://subrouter.aiEvery global flag works on every command.
| Flag | Environment variable | Meaning |
|---|---|---|
--base-url |
SUBROUTER_BASE_URL |
Site to talk to |
--token |
SUBROUTER_ACCESS_TOKEN |
System access token |
--user-id |
SUBROUTER_USER_ID |
Numeric account id |
--api-key |
SUBROUTER_API_KEY |
sk- relay key |
-o, --output |
โ | table, json or yaml |
--quota-per-unit |
SUBROUTER_QUOTA_PER_UNIT |
Quota units per 1 USD (500000) |
--show-secrets |
โ | Print secrets in full |
--cache-ttl |
โ | Serve cached responses younger than this |
--cache-dir |
SUBROUTER_CACHE_DIR |
Where the cache lives |
--cache-clear |
โ | Empty the cache and exit |
--no-color |
โ | Plain output |
--timeout |
โ | Request timeout in seconds |
-y, --yes |
โ | Skip confirmation prompts |
The platform has two distinct token types and mixing them up is the single most common mistake:
| Credential | Header | Use |
|---|---|---|
sk-โฆ relay key |
Authorization: Bearer sk-โฆ |
Model calls, /v1/* |
| System access token + user id | Authorization: Bearer โฆ and New-Api-User: <id> |
Account management, /api/* |
subrouterctl picks the right one for you and refuses to send the wrong pair.
The same account, from a program. The two packages in packages/
call the Go library this tool is built on, so the cache, the retries and the
credential handling are the ones of the tool rather than a second copy of
them.
| Python | TypeScript | |
|---|---|---|
| Package | packages/python |
packages/typescript |
| Transport | The Go library in process, over a C ABI | A child process, over a pipe |
| Needs | The shared library, or the binary as a fallback | The binary |
| Extra dependencies | None | None |
| Calls | Synchronous | async |
from subrouterctl import SubRouterClient
client = SubRouterClient()
print(client.account.info()["username"])
for log in client.usage.logs(from_="24h", model="gpt-5", limit=10):
print(log["created_at"], log["model_name"], log["quota"])import { SubRouterClient } from "subrouterctl";
const client = new SubRouterClient();
const info = await client.account.info();
console.log(info.username);
for (const log of await client.usage.logs({ from: "24h", model: "gpt-5", limit: 10 })) {
console.log(log.created_at, log.model_name, log.quota);
}Neither package shells out to the CLI and neither one re-implements the API. They speak one small JSON protocol to the Go library, and the tool itself can serve that protocol:
$ echo '{"method":"account.info"}' | subrouterctl bridge
{"ok":true,"result":{"id":10001,"username":"demo-user","display_name":"demo-user",...}}The protocol is documented in packages/README.md, the
Python API in packages/python and the TypeScript API in
packages/typescript.
make bindings # the shared library the Python binding loads
make test-bindings # the Python and TypeScript tests- Secrets are masked in every table by default.
--show-secretsreveals them. - The on-disk cache never holds a secret. The values it scrubs are replaced by a placeholder, so a cached response cannot be read back as a live key.
- Deleting a key, unsubscribing a provider and changing billing settings ask
for confirmation.
-yskips the prompt in scripts. - Quota limits accept either USD (
--quota-usd 10) or raw platform units (--quota-units 5000000). - Payment, top-up and payout stay in the browser console.
subscription payonly creates a checkout session and prints the URL.
make build # build the binary
make test # unit tests
make smoke # live smoke test against your account
make lint # gofmt + go vet
make help # list every make target
make bindings # the shared library the Python binding loads
make test-bindings # the Python and TypeScript testsThe smoke test creates and removes a few throwaway API keys, so it needs a real system access token. It restores the account to its original state: it sweeps the keys an earlier interrupted run left behind, and if it cannot remove its own keys at the end it says so and fails, rather than leaving live credentials behind.
passed 67, failed 0
SubRouter is a trademark of its respective owner. This tool is an independent client and is not affiliated with the platform.
{ "key": "sk-โฆ", // relay key: model calls "access_token": "โฆ", // management credential "user_id": 10001, // numeric account id "base_url": "https://subrouter.ai" }