@vheins/opencode-9router
v0.9.1
Published
OpenCode plugin provider for 9Router — FREE AI Router & Token Saver. 40+ providers, 100+ models.
Maintainers
Readme
@vheins/opencode-9router
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.
Quick Start
OpenCode V1
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@vheins/opencode-9router@latest"]
}- Add the plugin to
opencode.json - Restart OpenCode
/models→ select a 9Router model
OpenCode V2
{
"$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).
Features
- 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
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.
Configure (V2)
{
"$schema": "https://opencode.ai/config.json",
"plugins": ["@vheins/opencode-9router@latest"]
}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
}
}
]
}Providers (V2)
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.
Dual V1 + V2 entrypoint
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.
Installation
From npm (recommended)
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@vheins/opencode-9router@latest"]
}No need to define a provider manually — the plugin registers it automatically.
Multiple Providers
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.
With Environment Variable
{
"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
opencodeProvider Options
| 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 |
Example with cache and timeout tuning
{
"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
}
}
}
}Disable cache
{
"provider": {
"9router": {
"npm": "@ai-sdk/openai-compatible",
"name": "Local Router",
"options": {
"baseURL": "http://localhost:20128",
"apiKey": "sk-...",
"cache": false,
"discoveryTimeout": 30000
}
}
}
}Usage
Select a model
/models
→ Find the 9Router provider
→ Pick any model (e.g., kr/claude-sonnet-4.5, cc/claude-opus-4-7)Model Prefixes
| 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 |
How It Works
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 /modelsCache storage
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).
Changelog
v0.9.1 — V2 model registration for configured providers
- 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
v0.9.0 — OpenCode V2 support (dual V1 + V2)
- 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
v0.8.1 — Strict combo enrichment
- 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
v0.8.0 — Combo 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
v0.7.1 — Discovery logging
- Add INFO-level logging for cache hit/miss, fetch status, timeout, and stale fallback
v0.7.0 — Caching & timeout
- 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
v0.6.0 — Multi-provider & fallback
- Support multiple 9Router-family providers (
9router,9router-local, etc.) - Fallback models catalog via
models.devAPI - Per-model capability resolution (vision, tools, reasoning)
v0.5.x — Initial releases
- Basic auto-discovery from
localhost:20128 - Fallback models when API unreachable
- Single provider support
Development
git clone https://github.com/vheins/opencode-9router
cd opencode-9router
npm install
npm run buildTest
node test-minimal.mjs
opencode models 9router --print-logsValidate after publish
After 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.
Publish
npm login
npm version patch # or minor, major
npm publish --access public
git push --follow-tagsFiles
opencode-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