Guides & Tutorials7 min read

Add an Open Model to 9Router as a Custom Provider: Setup and What We Measured

9Router puts one local endpoint in front of your coding tools and many model providers. Here is how to add an OpenAI-compatible API to it as a custom provider, with ParalonCloud and Qwen3.8 27B as the worked example, and what happened when Claude Code and opencode fixed a bug through it.

The Paralon capybara at a railway switch, routing glowing tracks toward three graphics cards

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.0 and prints a warning that it is reachable from the network. --host 127.0.0.1 keeps 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:

FieldValue
NameParalonCloud
Prefixparalon
API typechat
Base URLhttps://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.296opencode 1.19.0
Resultfixed, 3 of 3 tests passfixed, 3 of 3 tests pass
Wall time20 s14 s
Requests to the model66
Prompt size per request15,780 to 17,731 tokens2,644 to 11,043 tokens
Time to first token, first large prompt8.9 s4.3 s
Time to first token, later requests0.34 to 0.72 s0.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_reason was tool_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-27b in a tool loop.

Troubleshooting

SymptomCause
401 Invalid API key from localhost:20128the request carries the provider's key; it needs a 9Router key (step 3)
The provider's models are missing from /v1/modelsno connection was added to the custom provider, or its key is wrong
404 model not foundthe model name needs the prefix: paralon/qwen3.8-27b, not qwen3.8-27b
A script fails to parse the JSON answerthe request has no stream field; add "stream": false

On the network

A GPU with JupyterLab on it, in about a minute

Billed for the minutes it runs, on a machine we benchmarked before listing it. Or call open models through an OpenAI-compatible API.

Keep reading

Related articles