9Router is an open-source proxy you run on your own machine. Your coding tool talks to one local endpoint, and 9Router decides which model provider answers. It ships with a long list of providers, and it also takes any OpenAI-compatible API as a custom one. That is the part this guide covers: adding an open model as a provider, then checking that real coding tools can work through it.
The worked example is our own API, ParalonCloud, with qwen3.8-27b. Everything below was run on 9Router 0.5.99 on 11 October 2026, and the numbers are from that run.
1. Install and start 9Router
npm install -g 9router
9router --host 127.0.0.1
The dashboard is at http://localhost:20128/dashboard and the API at http://localhost:20128/v1.
Two things to do before anything else:
- Bind it to localhost. Started with no flags, 9Router listens on
0.0.0.0and prints a warning that it is reachable from the network.--host 127.0.0.1keeps it on your machine. - Change the dashboard password. A fresh install accepts the default one,
123456.
2. Add the API as a custom provider
In the dashboard, open Providers and add an OpenAI Compatible provider. The form asks for four things:
| Field | Value |
|---|---|
| Name | ParalonCloud |
| Prefix | paralon |
| API type | chat |
| Base URL | https://paraloncloud.com/v1 |
Then add a connection to it with your API key. Keys are made on the API keys page.
The prefix becomes the first part of every model name. 9Router reads the provider's /v1/models and lists what it finds:
curl -s http://localhost:20128/v1/models | grep -o '"id":"paralon/[^"]*"'
"id":"paralon/multilingual-e5-small"
"id":"paralon/qwen3.8-27b"
"id":"paralon/openjev-4b"
"id":"paralon/qwen3-3b"
For a coding tool the one that matters is paralon/qwen3.8-27b: it has tool calling and a 131,072-token context window. qwen3-3b is a small model with a 16,384-token window, multilingual-e5-small is for embeddings, and openjev-4b is a classifier, not a chat model.
3. Make a 9Router key
Requests to http://localhost:20128/v1 need a key of 9Router's own, separate from the provider's. Create one in the dashboard under API keys. Without it every request answers 401 Invalid API key.
4. Point a coding tool at it
Claude Code
Claude Code speaks the Anthropic format. 9Router translates it, so the tool only needs a different base URL and model name. (Since 13 October 2026 our API answers that format itself, so Claude Code can also skip the router: Claude Code on an open model, no router.)
export ANTHROPIC_BASE_URL=http://localhost:20128
export ANTHROPIC_AUTH_TOKEN=sk-your-9router-key
export ANTHROPIC_MODEL=paralon/qwen3.8-27b
export ANTHROPIC_SMALL_FAST_MODEL=paralon/qwen3.8-27b
claude
opencode
opencode speaks the OpenAI format. Put this in opencode.json in the project:
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"ninerouter": {
"npm": "@ai-sdk/openai-compatible",
"name": "9Router",
"options": {
"baseURL": "http://localhost:20128/v1",
"apiKey": "sk-your-9router-key"
},
"models": {
"paralon/qwen3.8-27b": {
"name": "Qwen3.8 27B",
"limit": { "context": 131072, "output": 8192 }
}
}
}
},
"model": "ninerouter/paralon/qwen3.8-27b"
}
The limit block is there on purpose. See "Three things to watch" below.
What we measured
The task was the same for both tools: a folder with a slugify.js that has a bug and three tests, two of them failing. The prompt asked the tool to run the tests, find the bug, fix it, and run the tests again. That needs the whole loop: a shell command, a file read, an edit, and another shell command.
| Claude Code 2.1.296 | opencode 1.19.0 | |
|---|---|---|
| Result | fixed, 3 of 3 tests pass | fixed, 3 of 3 tests pass |
| Wall time | 20 s | 14 s |
| Requests to the model | 6 | 6 |
| Prompt size per request | 15,780 to 17,731 tokens | 2,644 to 11,043 tokens |
| Time to first token, first large prompt | 8.9 s | 4.3 s |
| Time to first token, later requests | 0.34 to 0.72 s | 0.32 to 0.64 s |
Both tools made the same fix: one regular expression changed to collapse runs of separators, and a second one added to strip a dash at either end.
The gap between the first request and the rest is the prompt cache. A coding tool sends its whole system prompt and tool list on every turn. The first time, the model reads all of it. After that the worker already holds those tokens, and 9Router's log showed 15,744 to 17,472 of Claude Code's prompt tokens read from cache on each later request. That is why a 17,000-token request can start answering in under a second.
Before running the tools we also sent requests by hand through 9Router, to check the pieces one at a time:
- streaming: first byte in under 200 ms, the stream ends with
data: [DONE]and carries token usage - a streamed tool call with two tools in one turn, then the tool results sent back: both calls had the right arguments and the final answer used both results
- the Anthropic format (
/v1/messages) with a tool:stop_reasonwastool_use, with the right arguments - embeddings with
paralon/multilingual-e5-small
Three things to watch
Always send stream. A request to 9Router with no stream field came back with the content type text/event-stream and a body that is the JSON answer followed by data: [DONE], which a strict JSON parser rejects. With "stream": false the answer is plain JSON, and with "stream": true it is a normal stream. The same request sent straight to our API returns plain JSON in every case, so this is on the router's side. Coding tools stream by default and are not affected; your own scripts might be.
The context window is assumed, not read. For a custom provider 9Router lists every model with a 200,000-token window, whatever the provider serves. Claude Code reported the same 200,000 for paralon/qwen3.8-27b, whose real window is 131,072. On a short task it makes no difference. On a long session, a tool that believes it has more room than it does will be refused by the API before it thinks to compact. Where the tool lets you state the limit, as opencode does in the config above, state it.
Ignore the cost a tool prints. Claude Code ended its run with a dollar figure. It has no price list for a model it does not know, so that figure is not what the provider charged. The real numbers are in the provider's own usage page: for us, Inference usage. Claude Code also prints unrecognized_model on stderr for the same reason, and carries on.
What we did not test
- A session long enough to approach the context limit.
- 9Router's fallback from one provider to the next when the first one fails or runs out of quota. It is the product's main feature, and this guide only exercised a single provider.
- Models other than
qwen3.8-27bin a tool loop.
Troubleshooting
| Symptom | Cause |
|---|---|
401 Invalid API key from localhost:20128 | the request carries the provider's key; it needs a 9Router key (step 3) |
The provider's models are missing from /v1/models | no connection was added to the custom provider, or its key is wrong |
404 model not found | the model name needs the prefix: paralon/qwen3.8-27b, not qwen3.8-27b |
| A script fails to parse the JSON answer | the request has no stream field; add "stream": false |



