How to Use FreeRouter with the Pi Coding Agent
Pi is a terminal coding agent from Earendil Works, and its model setup covers more ground than most. /login handles subscriptions and API keys, around thirty hosted providers pick their key up from an environment variable, OpenRouter, Vercel AI Gateway, and Cloudflare AI Gateway are built in, and models.json takes any endpoint that speaks an API Pi already implements. If you have an OpenRouter key, exporting OPENROUTER_API_KEY and opening /model gets you a working agent in about a minute, and plenty of people never touch it again. That's a reasonable setup.
What it commits you to is a per-gateway, per-machine arrangement. Each gateway is its own provider in Pi, with its own credential in auth.json or the shell, its own spelling of model ids, and its own rows in the model picker. Choosing a gateway means choosing a model from that provider's list. Spend and latency for each one live in that gateway's dashboard, and every laptop or CI runner that runs Pi carries its own copy of the provider keys.
The constraint you hit first in an agent is a failed request partway through a run. A Pi task is a long chain of tool-calling turns, and every turn is a fresh request to the same provider. Pi handles transient failures sensibly: by default it retries up to three times with exponential backoff starting at two seconds (the retry.* keys in settings). Those retries go back to the same endpoint, though. When the gateway itself is rate-limiting your account or having a slow hour, the run waits on it or stops, and moving to another gateway is a manual /model switch.
FreeRouter plugs into the same models.json mechanism as any other compatible endpoint. A provider entry with "api": "openai-completions", a base URL of https://api.freerouter.com/v1, and an fr_live_ key is the entire Pi side. Pi keeps sending the chat completions it already sends, and the key's routing rule decides which of your gateways serves each one, including moving to the next gateway when the first fails. It's the usual two changes, base URL and key, and because a gateway you add later is a dashboard row, there's no penalty for starting with just one.
Below: how the pieces connect, why it's worth doing even when OpenRouter is the only gateway you use today, the setup with a working models.json, and the edges you should know about before relying on it.
What it looks like when Pi talks to FreeRouter
Pi sees one provider, freerouter, with whatever models you list under it. Each request goes to a FreeRouter key that carries the API shape, the routing rule, and any model remaps. Behind the key sit your gateway accounts, each with your own provider key. OpenRouter can be the first one, and a second gateway is a row you add when you want failover. Pi's config stays the same through all of it.
flowchart LR
Pi["Pi: provider freerouter"] -->|"POST /v1/chat/completions"| Key["fr_live_ key: openai shape, rule, remaps"]
Key --> OR["OpenRouter (your key)"]
Key -.->|"a row you add later"| GW2["Second gateway (your key)"]
OR --> M["z-ai/glm-5.3"]
GW2 --> M2["z-ai/glm-5.3"]
Pi talks to one key. The dotted path is a dashboard edit that models.json never sees.
Why put a router between Pi and OpenRouter
Pi already speaks OpenRouter natively, so the fair question is what an extra hop buys you. For a coding agent it comes down to four things: a run that survives one gateway having a bad hour, model ids that don't depend on which gateway serves them, fewer provider keys sitting on developer machines, and a per-request record of what happened.
1. OpenRouter stays, as one target among several
Nothing here asks you to leave OpenRouter. Store its key as a provider in the FreeRouter dashboard (BYOK, so you keep the account, the credits, and the bill), route 100% of the key to it, and Pi behaves the way it did with OPENROUTER_API_KEY. What changes is where OpenRouter sits: it becomes a row in a routing rule, and your Pi config points at the key in front of it.
It's worth being precise about the overlap. OpenRouter already falls back between the inference providers behind it, and that keeps working through FreeRouter. What it can't do is fall back to a different gateway when OpenRouter itself is what's failing, or when your account is the thing being rate-limited. That second layer is what FreeRouter adds.
2. Failover happens before Pi sees an error
A priority rule tries its targets in order and moves to the next one on connection errors, 5xx, 429, a model the target doesn't list, or no response headers inside the first-byte budget (4 seconds by default). The move happens inside the same request, so Pi's agent loop receives an ordinary streamed completion and the run carries on. A 400 that would fail everywhere comes straight back, since retrying a malformed body around the list helps nobody.
Pi's own retry settings still apply on top and become the last resort for anything the rule couldn't absorb. When a failover does happen, X-FreeRouter-Failover lists each skipped attempt as gateway:reason, X-FreeRouter-Provider names the gateway that answered, and the same attempt path is on the request's row in Logs.
3. Model ids stop depending on the gateway
Gateways spell the same model differently. OpenRouter lists z-ai/glm-5.3, Fireworks writes accounts/fireworks/models/glm-5p3, and Sail wants zai-org/GLM-5.3 with that exact casing. Configured directly in Pi, those would be three models under three providers. Through FreeRouter you send the canonical id and each target receives its own spelling (see model IDs across gateways), so the id in models.json survives a change of gateway and failover can land on a second gateway for the same model.
When a new coding model ships on only one of your gateways, a model remap points an id Pi already sends at that destination. Each remap carries a percent (1–100), rolled per request, so percent: 20 sends a fifth of that model's traffic to the new destination while the rest follows the rule. Compare the two in Logs, then take it to 100 or delete it. Pi never needs to learn the new name.
4. One key on each machine, provider keys in the workspace
When Pi talks to gateways directly, every machine that runs it holds the raw provider keys, in auth.json or in the environment. With FreeRouter, each machine holds a single fr_live_ key, and the provider keys stay in the workspace, encrypted at rest. Rotating your OpenRouter key becomes one dashboard edit, and nobody has to update their shell. A key can also carry an IP allowlist, which is useful for the key your CI runners use.
The same key gives you one place to look afterward. Every proxied request logs which gateway served it, the attempt path, and a Router, Upstream, and Duration split, across all of your gateways at once. Pi's /session still tells you what a session cost; the Logs tab tells you where each of its requests went and how long each hop took.
Setting it up
Plan on fifteen minutes, most of it in the dashboard. You need an API key for at least one gateway (your OpenRouter key is fine) and Pi installed.
- Sign up. Create an account with Google or email plus an OTP. You land in a workspace.
- Add your gateway key as a provider. Providers → add the gateway you already use and paste its key. Your existing
OPENROUTER_API_KEYsetup in Pi keeps working, so you can run both paths side by side while you compare. - Create a FreeRouter key with the
openaishape. API Keys → new key. The defaultopenaishape serves/v1/chat/completions, which is what Pi'sopenai-completionsAPI calls. Leave MCP tools and Companion Ads off on this key (more on that under limits). - Point the routing rule at that one provider. One target, 100%. That's a legitimate rule to start with; failover arrives when you add a second target.
- Confirm the model ids. List what your key can serve and copy ids from that output, since a gateway's marketing page doesn't always use the request id:
curl https://api.freerouter.com/v1/models \ -H "Authorization: Bearer $FREEROUTER_API_KEY" | head -c 2000
- Add the provider to
models.json. The file and each field are below. - Pick the model in Pi and send a prompt. Then open Logs and check that
X-FreeRouter-Providernames your gateway and that the Router vs Upstream split looks sane. The dashboard Playground and Test button won't tell you this, because they call providers from the dashboard rather than throughapi.freerouter.comand are never logged.
Wiring Pi with models.json
Before touching Pi, send the kind of request Pi will send. Pi streams, so this one does too:
export FREEROUTER_API_KEY=fr_live_…
curl -i https://api.freerouter.com/v1/chat/completions \
-H "Authorization: Bearer $FREEROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "z-ai/glm-5.3",
"stream": true,
"messages": [{ "role": "user", "content": "Reply with the word ready." }]
}'You should get ordinary data: chunks ending in [DONE]. The -i is there for the headers: X-FreeRouter-Provider (who served it), X-FreeRouter-Attempts (how many targets were tried, 1 for now), and X-FreeRouter-Request-Id (quote this if you ever ask us about a request).
Then the Pi side. User-level model config lives at ~/.pi/agent/models.json (or under PI_CODING_AGENT_DIR if you've moved the agent directory). Add a freerouter provider:
{
"providers": {
"freerouter": {
"name": "FreeRouter",
"baseUrl": "https://api.freerouter.com/v1",
"api": "openai-completions",
"apiKey": "$FREEROUTER_API_KEY",
"models": [
{ "id": "z-ai/glm-5.3", "name": "GLM 5.3 via FreeRouter" },
{ "id": "anthropic/claude-opus-5.5", "name": "Claude Opus 5.5 via FreeRouter", "input": ["text", "image"] }
]
}
}
}Field by field: baseUrl is the FreeRouter base, and Pi appends /chat/completions itself. api picks Pi's OpenAI Chat Completions implementation, which matches the key's openai shape. apiKey uses Pi's $NAME interpolation, so the key comes from your environment and nothing secret goes in the file; a leading ! runs a command instead, if you keep keys in a secret manager. Each id is the string FreeRouter routes on, so it has to be one from step 5. name is just what the picker shows.
Two optional fields are worth setting once things work. Pi assumes a 128,000-token contextWindow and a 16,384-token maxTokens for custom models, and it uses the context window to decide when to compact, so put the model's real numbers in if they differ. And set "reasoning": true on models that think, which makes /thinking offer levels for them.
Opening /model reloads the file. Pi only lists models whose provider has usable credentials, so FREEROUTER_API_KEY has to be exported in the shell that starts Pi. To check from the command line, or start straight into a model:
pi --list-models freerouter pi --model freerouter/z-ai/glm-5.3
In the picker, Ctrl+S on the model saves it as the default for new sessions.
flowchart TD
A["Sign up, land in workspace"] --> B["Providers: add OpenRouter key"]
B --> C["API Keys: new fr_live_ key, openai shape"]
C --> D["Rule: 100 percent to OpenRouter"]
D --> E["GET /v1/models, copy ids"]
E --> F["~/.pi/agent/models.json: freerouter provider"]
F --> G["/model or pi --model freerouter/..."]
G --> H["Prompt, then check Logs"]
Eight steps, one file on the Pi side. Everything that changes later happens between C and D.
What the setup buys later
The first change most people make is a second gateway. Add its key under Providers and put it under OpenRouter in a priority rule, and failover starts working for every model on the key, Pi included, with ids translated to the second gateway's spelling. Nothing in models.json changes. If a new model is faster on that second gateway, add a remap from the id Pi already sends at percent: 20, watch the split in Logs, and take it to 100 or delete it.
This also sits comfortably next to Pi's virtual models. A virtual model chooses which model handles a request (a small one for quick questions, a large one for hard problems) and is written as an extension that runs inside Pi. FreeRouter decides which gateway serves whatever model was chosen. Point the virtual model's targets at freerouter/… models and each layer does its own job. If the case for a router in front of a single gateway still feels thin, the one-gateway post makes it at length.
Limits and failure modes
- One target means no failover. With a single gateway behind the key there's nothing to fall over to, and Pi's own retries are doing all the work. Add a second target before you need it.
- Failover happens before the first byte. FreeRouter can move to another gateway while it's waiting on headers. Once a gateway has started streaming a response, a failure partway through reaches Pi, and Pi's retry handles it from there.
- Pi treats FreeRouter as a stock OpenAI endpoint. Pi tunes its request format per provider, keyed on the provider name or base URL. It doesn't recognize
api.freerouter.com, so it sends standard OpenAI Chat Completions, and the OpenRouter-specific tuning it applies on its built-inopenrouterprovider (its reasoning format, Anthropic cache markers onanthropic/models) doesn't carry over automatically. Theopenaishape forwards the body as-is, so if a model rejects a field you'll see the 400 in Logs; fix it with thecompatsettings on that model, and, as Pi's docs say, only set the flags you've confirmed are needed. - Keep FreeRouter's MCP tools off on the Pi key. Pi brings its own tools and its own MCP support. FreeRouter never executes tools Pi sends, but with MCP enabled on the key it would run a second tool loop server-side inside the same request, which makes a transcript hard to reason about. Companion Ads and Keyterms are built for apps with end users and have no job to do in an agent session.
- Billing stays with your providers. FreeRouter is BYOK: inference is charged to your gateway accounts, and routing itself is free during the preview. It doesn't make tokens cheaper.
- The hop costs something. Overhead is sub-1 ms on a simple route and 20–30 ms with remaps, translation, or failover drain. Next to a coding model's generation time that's small, but you don't have to take our word for it: every Logs row shows the Router vs Upstream split.
- FreeRouter doesn't pick your model. If the id in
models.jsonis wrong for the task, routing just delivers the wrong answer more reliably. Choosing models stays with you, or with a Pi virtual model if you write one.
Next steps
Create a key, add the freerouter provider to models.json, and run one task in Pi while the Logs tab is open. Once the provider column looks right, add a second gateway to the rule, since that's the point where the router starts paying for itself.