OpenCode plugin provider for 9Router — FREE AI Router & Token Saver. 40+ providers, 100+ models.
Registers 9Router as a custom provider in OpenCode with auto-discovery of models.
Dual V1 + V2 support. This package ships a single entrypoint that works on both OpenCode V1 (via the V1
serverhook, OpenCode1.18.29+) and OpenCode V2 (via the V2Plugin.define({ id, setup })API). The discovery core is shared; only the plugin entrypoint differs. No separate package is needed.
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@vheins/opencode-9router@latest"]
}- Add the plugin to
opencode.json - Restart OpenCode
/models→ select a 9Router model
{
"$schema": "https://opencode.ai/config.json",
"plugins": ["@vheins/opencode-9router@latest"]
}Or with plugin options (object form):
{
"$schema": "https://opencode.ai/config.json",
"plugins": [
{
"package": "@vheins/opencode-9router@latest",
"options": {
"baseURL": "http://localhost:20128",
"apiKey": "{env:ROUTER_API_KEY}",
"cache": true,
"cacheTTL": 10800000,
"discoveryTimeout": 30000
}
}
]
}The plugin will auto-discover models from http://localhost:20128 (default).
- Auto-discover models — Models from 9Router are automatically detected on startup
- Smart caching — Discovery results cached for 3 hours (configurable) for instant subsequent loads
- Stale fallback — If the backend is unreachable, returns cached models instead of failing
- Configurable timeout — Adjustable discovery timeout (default 30s) for slow backends
- Dynamic model list — All models from 9Router are available, including custom combos
- Combo multimodal defaults — Combo models (
owned_by: "combo") that expose no capability info are forced to accept text/image/audio input with tool calling, reasoning, 256K context, 128K output, andlow/medium/high/xhigh/max/minimal/thinkingvariants - OpenAI-compatible — Uses
@ai-sdk/openai-compatible(V1) /@opencode/ai/providers/openai-compatible(V2) - Type-safe — Uses the
confighook (V1) / provider transforms (V2) for provider registration that conforms to the OpenCode config schema
OpenCode V2 replaces the V1 plugin config key with plugins and the single-function plugin shape with Plugin.define({ id, setup }). This package handles both — the same published entrypoint is loaded by V1 and V2.
For a local checkout, keep the directory path in the V2 plugins list:
{
"plugins": ["/absolute/path/to/opencode-9router"]
}The local directory must expose a root server entrypoint; this repository provides server.js for that purpose. The V1 configuration key remains plugin and can continue to use the published package name as shown above.
Plugin options use the object form ({ "package": ..., "options": { ... } }) and are read from ctx.options:
{
"$schema": "https://opencode.ai/config.json",
"plugins": [
{
"package": "@vheins/opencode-9router@latest",
"options": {
"baseURL": "http://localhost:20128",
"apiKey": "{env:ROUTER_API_KEY}",
"cache": true,
"cacheTTL": 10800000,
"discoveryTimeout": 30000
}
}
]
}In V2, providers are declared under the providers key. Each 9router-family provider uses the OpenAI-compatible runtime package and carries the endpoint in settings.baseURL. The plugin discovers models for every provider whose ID starts with 9router and fills in the model list.
{
"$schema": "https://opencode.ai/config.json",
"plugins": ["@vheins/opencode-9router@latest"],
"providers": {
"9router": {
"name": "9Router",
"package": "@opencode/ai/providers/openai-compatible",
"settings": {
"baseURL": "http://localhost:20128/v1",
"apiKey": "{env:ROUTER_API_KEY}"
}
},
"9router-remote": {
"name": "Remote 9Router",
"package": "@opencode/ai/providers/openai-compatible",
"settings": {
"baseURL": "https://your-9router-endpoint.example.com/v1"
}
}
}
}If no 9router-family provider exists, the plugin registers a default 9router provider pointing at http://localhost:20128. A background refresh re-discovers models and calls ctx.provider.reload() every 5 minutes; the discovery interval is cleaned up when the plugin unloads.
The default export exposes both implementations:
import { Plugin } from "@opencode/plugin"
import { NineRouterPlugin } from "./plugin.js" // V1
import { nineRouterV2 } from "./v2.js" // V2
const v2 = Plugin.define(nineRouterV2)
export default { id: v2.id, setup: v2.setup, server: NineRouterPlugin }
export { NineRouterPlugin }V2 reads id/setup; V1 (1.18.29+) reads server. The named NineRouterPlugin export is kept for older V1 consumers. For local development, the repository also includes a root server.js bridge because OpenCode resolves an absolute plugin directory through its server entrypoint. The published package exposes the same entry through the ./server export.
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@vheins/opencode-9router@latest"]
}No need to define a provider manually — the plugin registers it automatically.
Add one or more 9Router-family providers with custom options:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@vheins/opencode-9router@latest"],
"provider": {
"9router": {
"npm": "@ai-sdk/openai-compatible",
"name": "Your Provider",
"options": {
"baseURL": "https://your-9router-endpoint.example.com/v1",
"apiKey": "{env:ROUTER_API_KEY}",
"cache": true,
"cacheTTL": 10800000,
"discoveryTimeout": 60000
}
},
"9router-local": {
"npm": "@ai-sdk/openai-compatible",
"name": "Local Router",
"options": {
"baseURL": "http://127.0.0.1:20128/v1",
"apiKey": "sk-...",
"cache": true,
"discoveryTimeout": 60000
}
}
}
}Each provider key must start with 9router. The npm must be @ai-sdk/openai-compatible. The name is a display label shown in OpenCode.
Never commit real API keys. Replace the
baseURL/apiKeyabove with your own endpoint and a placeholder such as{env:ROUTER_API_KEY}(or a dummy value likesk-...). Real keys belong in environment variables, not inopencode.jsonor this README.
{
"provider": {
"9router": {
"npm": "@ai-sdk/openai-compatible",
"name": "Remote 9Router",
"options": {
"baseURL": "https://example.com/v1",
"apiKey": "{env:ROUTER_API_KEY}",
"cache": true,
"discoveryTimeout": 30000
}
}
}
}export ROUTER_API_KEY=your-api-key-here
opencode| Option | Type | Default | Description |
|---|---|---|---|
baseURL |
string |
http://localhost:20128 |
9Router API endpoint |
apiKey |
string |
— | API key (if required by backend) |
cache |
boolean |
true |
Cache discovery results to ~/.cache/opencode-9router/ |
cacheTTL |
number |
10800000 (3h) |
Cache TTL in milliseconds |
discoveryTimeout |
number |
30000 (30s) |
Timeout for /v1/models request in milliseconds |
{
"provider": {
"9router": {
"npm": "@ai-sdk/openai-compatible",
"name": "Local Router",
"options": {
"baseURL": "http://localhost:20128",
"cache": true,
"discoveryTimeout": 60000
}
},
"9router-remote": {
"npm": "@ai-sdk/openai-compatible",
"name": "Remote API",
"options": {
"baseURL": "https://your-9router-endpoint.example.com/v1",
"apiKey": "{env:ROUTER_API_KEY}",
"cacheTTL": 3600000
}
}
}
}{
"provider": {
"9router": {
"npm": "@ai-sdk/openai-compatible",
"name": "Local Router",
"options": {
"baseURL": "http://localhost:20128",
"apiKey": "sk-...",
"cache": false,
"discoveryTimeout": 30000
}
}
}
}/models
→ Find the 9Router provider
→ Pick any model (e.g., kr/claude-sonnet-4.5, cc/claude-opus-4-7)
| Prefix | Provider | Tier |
|---|---|---|
cc/ |
Claude Code | Subscription |
cx/ |
Codex | Subscription |
gh/ |
GitHub Copilot | Subscription |
cu/ |
Cursor IDE | Subscription |
kr/ |
Kiro AI | FREE |
oc/ |
OpenCode Free | FREE |
vertex/ |
Vertex AI | $300 credits |
glm/ |
GLM | $0.6/1M |
minimax/ |
MiniMax | $0.2/1M |
kimi/ |
Kimi | $9/mo flat |
openrouter/ |
OpenRouter | API key |
deepseek/ |
DeepSeek | API key |
groq/ |
Groq | API key |
opencode.json "plugin": ["@vheins/opencode-9router@latest"]
↓
Plugin loads at startup detects 9Router-family providers in config
↓
For each provider:
1. Check local cache (~/.cache/opencode-9router/discovery-*.json)
└─ Fresh cache (≤3h) → return cached models instantly
2. If cache miss/stale:
└─ GET {baseURL}/v1/models with 30s timeout (configurable)
└─ If OK → register live models + save to cache
└─ If fail → fallback to stale cache (if exists), else skip
↓
config hook creates/updates provider with discovered models
↓
Provider appears in /models
Cache files are stored at ~/.cache/opencode-9router/discovery-{base64url}.json, one per unique baseURL. The cache directory also stores the models.dev capability catalog (models-dev.json).
- Fix V2 plugin setup ordering: external plugins run before OpenCode's internal config-provider plugin, so config-defined
9router*providers are not visible during setup - Register a model transform (
ctx.model.transform) that injects the discovered inventory into providers once they become available - Refresh targets after setup with bounded retries and
provider.updatedevents, thenctx.model.reload() - Add a root
server.jsentrypoint and./serverpackage export so an absolute local plugin path resolves - Add
test-v2.mjscovering the package entrypoint and the late config-provider lifecycle
- Add a V2 plugin (
src/v2.ts) built withPlugin.define({ id: "9router", setup }), registering providers/models throughctx.provider.transform(...)and refreshing viactx.provider.reload() - Add
src/v2-map.tsmapping the discoveredModelConfigonto V2Model.Info(capabilities, requiredlimit.context/limit.output, variants) - Add
src/index.tsdual entrypoint: default export merges the V2 definition withserver: NineRouterPlugin(V1 1.18.29+); namedNineRouterPluginexport retained for older V1 - Package entry (
exports,main,types) now points atdist/index.js/dist/index.d.ts @opencode/plugin@^2.0.12added as a runtime dependency;@opencode-ai/plugin/@opencode-ai/sdkkept for V1 types- V1 behavior unchanged; discovery/capability/cache algorithms untouched
- Combos count as enriched only on an exact models.dev segment match (no more loose substring hits like
vision→gpt-4-turbo-vision) - Ignore bare
/models/infocapability stubs ({tools: true}) for combos, so backends that return stubs still get forced multimodal defaults
- Detect combo models via
owned_by: "combo"(fallback: no provider prefix) - Force multimodal defaults on combos with no resolved capability info: text/image/audio input, tool calling, reasoning, 256K context, 128K output
- Add
low/medium/high/xhigh/max/minimal/thinkingvariants, plussearch: false,context_length, andmax_completion_tokens - Version discovery cache so older entries are refetched
- Add INFO-level logging for cache hit/miss, fetch status, timeout, and stale fallback
- Model discovery cache — 3-hour TTL with stale fallback when backend is unreachable
- Configurable timeout — 30s default, adjustable via
discoveryTimeoutoption - Provider options —
cache,cacheTTL,discoveryTimeout - New constant:
DISCOVERY_CACHE_TTL,DISCOVERY_TIMEOUT
- Support multiple 9Router-family providers (
9router,9router-local, etc.) - Fallback models catalog via
models.devAPI - Per-model capability resolution (vision, tools, reasoning)
- Basic auto-discovery from
localhost:20128 - Fallback models when API unreachable
- Single provider support
git clone https://github.com/vheins/opencode-9router
cd opencode-9router
npm install
npm run buildnode test-minimal.mjs
opencode models 9router --print-logsAfter publishing to npm, verify the plugin works end-to-end:
opencode models 9router-local --print-logs --log-level DEBUGThis runs model discovery against the published package with full debug logging to confirm caching, timeout, and capability resolution are working correctly.
npm login
npm version patch # or minor, major
npm publish --access public
git push --follow-tagsopencode-9router/
src/
index.ts # Dual V1 + V2 package entry
plugin.ts # V1 plugin entry (provider registration + config hook)
v2.ts # V2 plugin entry (Plugin.define + provider transform)
v2-map.ts # V1 ModelConfig → V2 Model.Info mapping
discovery.ts # Model discovery pipeline
capabilities.ts # Capability resolution (catalog + per-model API)
cache.ts # Discovery + models.dev cache
types.ts # Shared types
utils.ts # Shared helpers
constants.ts # Models, defaults, prefixes
dist/ # Compiled output (generated)
package.json # npm package config
tsconfig.json # TypeScript config
README.md # This file
README.id.md # Bahasa Indonesia version
LICENSE # MIT
-
local-memory-mcp — A lightweight MCP server that gives AI agents persistent memory with semantic search, backed by SQLite. Local-first, with domain-based context isolation and no external database. Available on npm.
-
RustaSea framework — An expressive, Laravel-inspired web framework for Rust, with Rust-native safety, performance, and concurrency.
-
RustaSea skeleton — The RustaSea application skeleton (Blade variant), scaffolded with
cargo rustasea new.
{ "$schema": "https://opencode.ai/config.json", "plugins": ["@vheins/opencode-9router@latest"] }