Run your own router
The router holds model credentials so no agent has to. Agents ask for a route name; the router resolves it to a provider and decides whether that caller may use it.
Run your own when you already have provider accounts and want everything in one place.
Install
Section titled “Install”hexaeight-activate install-routerhexaeight-activate restart routerAdd providers
Section titled “Add providers”hexaeight-activate upstreamsSupported: anthropic, openai, azure, bedrock, openrouter, ollama. Each carries its own
key and model list, written to upstreams.yaml beside the binary. That one file holds the
upstreams and the router’s own settings — there is no second config file.
These files hold your provider keys in plain text. Treat them the way you would any secret: restrict file permissions, exclude them from version control, and keep them out of backups that travel.
Confirm what each will actually serve:
hexaeight-activate modelsHow a route name works
Section titled “How a route name works”An engine sends both names in one field, separated by |:
"model": "claude-ant-aws|<the provider model id>"The router matches the left half against upstreams.yaml to choose a provider and key, and puts
the right half on the wire.
Two consequences worth understanding:
- a route name never has to be a real model id — it is yours to name
- a provider never sees your routing names
A route name is a glob you choose. It is matched against the left half of what an engine sends, so it never has to look like a real model id, and a provider never sees it.
A useful convention is *-<dialect>-<tag> — the dialect being the API the caller speaks (ant,
oai, res) and the tag one provider and one key. Two regions of the same provider are two tags,
because they are two keys. But a plain prefix works just as well: *-ant-aws and glm5br* are both
valid, and a real router usually has some of each.
List what yours are called:
hexaeight-activate upstreams --listIts policy — the base set
Section titled “Its policy — the base set”The router enforces rules of the same shape as an agent’s: a sender, a destination, a direction and an effect. It has its own store, sealed under the router’s key, which the agent CLI cannot write.
A working base policy looks like this:
sender dest dir meaning------------------------ ------------ --------- --------------------------------------------[email protected] * inbound the owner may relay through any agent[email protected] * outbound the owner may use any routemy-agent * inbound this agent may relaymy-agent * outbound ...using any routesecond-agent * inbound a second agent may relaysecond-agent *-ant-aws outbound ...but only these two routessecond-agent *-oai-aws outbound<subject-hash> my-agent inbound a person, relaying through that agent<subject-hash> * outbound ...may use any routeTwo things to take from it:
inboundis “may relay”;outboundis “may use this route”. Both are needed. An agent with only an inbound row connects and can reach no model.- Restrict by route where it matters.
second-agentabove is deliberately limited to two routes.*is convenient for your own agent and wrong for anything you did not write.
A person appears as the hash of their address, not the address itself — the same subject used for vouching on the agent side.
Confirming it is on
Section titled “Confirming it is on”The router prints the whole policy at startup:
Authorization: Casbin (encrypted policy at …/router-policy.he, bootstrap=False)[policy] 9 rule(s):[policy] sender='[email protected]' dest='*' realm='default' dir='inbound' eft='allow'…Callers: opaque permitted - an agent may decline to name its callersRead three things: bootstrap=False (it is enforcing), the rule count, and the
Callers line, which tells you whether requireIdentifiedCaller is in effect.
bootstrap=True, or any rule whose sender is *, means the router is open to anyone who can
reach it. That is where a fresh install starts. Add your real rules and restart before exposing
it.
It authorises twice per call: may this caller relay through this agent, and may this caller use this
route and model. Policy matches the whole route|model string, with globs:
claude-ant-aws|* that route, any model on it*|moonshotai.kimi-k2.5 that model, by whichever route reaches itGlobs do not cross
/. A model id containing a slash needs a star per level —kimi-oai-or|*/*, notkimi-oai-or|*.
The router reads its policy once at startup. It does not reload on change — after editing, restart the router or the change is not in force.
Writing the rules
Section titled “Writing the rules”--init-policy is how rules are written. There is no --add-rule: the store is sealed under the
router’s own key, so the router itself is the only thing that can change it.
cd my-routercp -p router-policy.he "router-policy.he.bak_$(date +%Y%m%d_%H%M%S)"set -a; . ./env-file; set +a./hexaeight-router-linux-x64 --init-policyIt shows the rules you already have, then asks five things:
| it asks | answer with | notes |
|---|---|---|
| Do you have an external authorization service? | N for most people | say yes only if entitlements live in your own system |
| Who may use this router? | [email protected],my-agent | comma-separated. *@example.com admits a whole domain |
| Which models may they use? | blank, or *-ant-aws,glm5br* | blank means every model this router serves |
| Which agents may vouch for them? | blank, or an agent name | blank means any agent |
| Write this policy? | y | nothing is written before this answer |
Typing * as the sender makes it demand the literal word YES — because that is bootstrap, and
leaving bootstrap is the whole reason the command exists.
What it does to bootstrap
Section titled “What it does to bootstrap”A fresh router starts wide open, with two rules:
p, *, *, default, inbound, allow # anyone may reach this routerp, *, *, default, outbound, allow # ...and use any model on itWhen you write your first real policy, those two are removed. The command says so before it does it. After that the router is default-deny, and every caller needs a rule.
It is additive, and it does not reload
Section titled “It is additive, and it does not reload”Running it again adds to what is there rather than replacing it, so you can admit one more caller later without retyping the rest.
The router reads its policy once, at startup. After writing, restart it:
hexaeight-activate restart routerThen check the boot line says what you expect:
Authorization: Casbin (encrypted policy at …/router-policy.he, bootstrap=False)[policy] 9 rule(s):Reading the policy back
Section titled “Reading the policy back”router-policy.he is encrypted and there is no decrypt tool. You read it two ways, and both print
it in clear: the router prints every rule at startup, and --init-policy shows the current
rules before it asks you anything — so you can run it, read them, and answer N to change nothing.
Accounting
Section titled “Accounting”Because every call carries a proven identity, usage is attributable per caller rather than per key. That is the practical reason to run a router even with a single provider: an API key tells you what was spent; a router tells you who spent it.
There is no billing screen, and none is needed. The router appends a line per call to
usage.jsonl beside the binary, and every session on the agent side leaves its own JSONL trail
of what was asked and what it cost.
wc -l usage.jsonl # calls servedtail -1 usage.jsonl # the most recent, with its token countsIt is a plain text record, one JSON object per line, so totalling it by caller, by model or by day
is whatever tool you already use — jq, a spreadsheet, or your own script. Nothing is aggregated
behind an interface you cannot query.
Who may decline to be named
Section titled “Who may decline to be named”An agent relaying on behalf of a person may send :opaque: instead of that person’s identity. The
router then attributes the call to the relaying agent.
On your own router you usually do not want that — you run both sides, and per-caller accounting is the point:
requireIdentifiedCaller: trueThat setting refuses opaque callers outright. It is the opposite of what an external router wants — see Forward to an external router.