@omniroute/opencode-plugin
First-class OpenCode plugin for the OmniRoute AI Gateway. Pulls a live model catalog from /v1/models (including -low/-medium/-high/-thinking variants as first-class IDs), aggregates combos via /api/combos using a least-common-denominator capability/limit join, sanitizes Gemini tool schemas in flight, and supports multiple side-by-side OmniRoute instances out of the box.
Install
Once published to npm:
npm install @omniroute/opencode-plugin
Until then (or for local development), reference the built artifact directly. Either extract the package into your OpenCode plugins dir and point at the extracted dist/index.js:
# from inside the OmniRoute repo
cd @omniroute/opencode-plugin && npm run build && npm pack
# then extract into ~/.config/opencode/plugins/omniroute-opencode-plugin/
Peer dep: @opencode-ai/plugin (managed by your OpenCode install).
Quick start (single instance)
// opencode.json
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
[
"@omniroute/opencode-plugin",
{
"providerId": "omniroute",
"baseURL": "https://or.example.com",
},
],
],
}
opencode auth login --provider omniroute
# prompts for the OmniRoute API key, writes to ~/.local/share/opencode/auth.json
⚠ Use the
--providerflag explicitly.opencode auth login omnirouteis parsed as a positionalurlargument by current OC releases (≤1.15.5) and fails withfetch() URL is invalid. Tracked upstream.
Restart OpenCode. /models lists the full live catalog. Variants (-low, -medium, -high, -thinking) and combos appear as first-class IDs — OmniRoute is the source of truth, no client-side synthesis.
Multi-instance (prod + preprod side-by-side)
⚠ OC ≤1.15.5 dedupes plugin loads by absolute module path. Two
plugin:entries pointing at the samedist/index.jscollapse into one (last-listed options win). Workaround: install the plugin twice into separate directories so each entry resolves to a distinct module file. v0.2.x will introduce aninstances: [...]shape that registers N providers from a single load.
Dual-install workaround (works today on OC ≤1.15.5)
Pack the plugin once, extract it twice into named directories, then point each plugin: entry at its own copy:
# 1. Build + pack the plugin (run from the plugin worktree)
cd /path/to/OmniRoute/@omniroute/opencode-plugin
npm run build
npm pack
# produces omniroute-opencode-plugin-0.1.0.tgz
# 2. Extract one copy per OmniRoute endpoint
mkdir -p ~/.config/opencode/plugins/omniroute-opencode-plugin-prod
mkdir -p ~/.config/opencode/plugins/omniroute-opencode-plugin-preprod
tar -xzf omniroute-opencode-plugin-0.1.0.tgz -C ~/.config/opencode/plugins/omniroute-opencode-plugin-prod --strip-components=1
tar -xzf omniroute-opencode-plugin-0.1.0.tgz -C ~/.config/opencode/plugins/omniroute-opencode-plugin-preprod --strip-components=1
Then in ~/.config/opencode/opencode.json reference each directory by absolute path:
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
[
"./plugins/omniroute-opencode-plugin-prod/dist/index.js",
{
"providerId": "omniroute",
"displayName": "OmniRoute",
"baseURL": "https://or.example.com",
},
],
[
"./plugins/omniroute-opencode-plugin-preprod/dist/index.js",
{
"providerId": "omniroute-preprod",
"displayName": "OmniRoute Preprod",
"baseURL": "https://or-preprod.example.com",
},
],
],
}
Paths are relative to ~/.config/opencode/. Each entry now resolves to a distinct module file, so OC loads them as two separate plugin instances. Authenticate each:
opencode auth login --provider omniroute
opencode auth login --provider omniroute-preprod
Each entry gets its own provider id, its own model picker entry, its own slot in auth.json, and its own TTL cache. Closures are isolated per plugin instance — no cross-talk.
After publish (@omniroute/opencode-plugin npm)
Once the package is published, the dual-install becomes two npm install --prefix commands instead of tar -xzf:
mkdir -p ~/.config/opencode/plugins/omniroute-opencode-plugin-prod
mkdir -p ~/.config/opencode/plugins/omniroute-opencode-plugin-preprod
npm install --prefix ~/.config/opencode/plugins/omniroute-opencode-plugin-prod @omniroute/opencode-plugin
npm install --prefix ~/.config/opencode/plugins/omniroute-opencode-plugin-preprod @omniroute/opencode-plugin
opencode.json paths become ./plugins/omniroute-opencode-plugin-prod/node_modules/@omniroute/opencode-plugin/dist/index.js (and the preprod equivalent).
Features
| Feature | What it does | Hook |
|---|---|---|
Dynamic /v1/models |
Pulls live catalog (455+ entries on prod) on each refresh, TTL-cached | provider.models |
| Variants pass-through | -low/-medium/-high/-thinking ship as first-class IDs from OmniRoute (no client synthesis) |
provider.models |
| Combo LCD aggregation | Combos appear with intersected capabilities + min context/output across members | provider.models |
| Nice names | combo.name / model.id surfaces as ModelV2.name |
provider.models |
| Bearer injection + suffix-spoof guard | Adds Authorization on baseURL-matched requests only |
auth.loader.fetch |
| Gemini schema sanitization | Strips $schema/$ref/additionalProperties for gemini-*/google-vertex-gemini/* |
auth.loader.fetch wrap |
| Multi-instance | Each plugin entry binds to its own providerId; closures isolated |
factory |
| Config-hook shim | OC ≤1.14.48 fallback: writes static catalog into config.provider[id] |
config |
Plugin options
| Option | Type | Default | Description |
|---|---|---|---|
providerId |
string |
"omniroute" |
OpenCode provider id; must be unique across plugin entries |
displayName |
string |
"OmniRoute" or OmniRoute (<id>) |
Label in the OC UI |
modelCacheTtl |
number |
300000 (5 min) |
/v1/models TTL in ms |
baseURL |
string |
resolved from auth.json after /connect |
Override OmniRoute base URL |
features |
object |
see below | Feature toggles (all opt-in/out, defaults preserve v0.1.0) |
features block
Every field is optional. Defaults mirror v0.1.0 behaviour so existing opencode.json files do not need to change.
| Feature | Type | Default | What it does |
|---|---|---|---|
combos |
boolean |
true |
Discover /api/combos and surface them as pseudo-models with LCD capabilities |
enrichment |
boolean |
true |
Pull display names from /api/pricing/models AND per-million-token pricing (input, output, cached → cacheRead, cache_creation → cacheWrite) from /api/pricing, then overlay both onto the live catalog (so the UI shows Claude 4.7 Opus with cost.input: 5, cost.output: 25 instead of raw IDs and zeroed cost). |
compressionMetadata |
boolean |
false |
Pull /api/context/combos so combo names get tagged with their compression pipeline, e.g. claude-primary [rtk:standard → caveman:full] |
geminiSanitization |
boolean |
true |
Strip $schema/$ref/additionalProperties from tool params when the model id matches gemini |
mcpAutoEmit |
boolean |
false |
Auto-write an mcp.<providerId> remote entry into the OC config pointing at <baseURL>/api/mcp/stream with the resolved Bearer token |
mcpToken |
string |
unset | Optional separate Bearer for the auto-emitted MCP entry. Falls back to the provider's apiKey (from auth.json) when unset |
fetchInterceptor |
boolean |
true |
Inject Authorization: Bearer + default Content-Type on every outbound request targeting baseURL (suffix-spoof guarded) |
Example — enrichment + compression tags + MCP auto-emit
{
"plugin": [
[
"@omniroute/opencode-plugin",
{
"providerId": "omniroute",
"baseURL": "https://or.example.com",
"features": {
"combos": true,
"enrichment": true,
"compressionMetadata": true,
"mcpAutoEmit": true,
},
},
],
],
}
With mcpAutoEmit: true, the plugin synthesises an mcp.omniroute entry equivalent to a manual:
"mcp": {
"omniroute": {
"type": "remote",
"url": "https://or.example.com/api/mcp/stream",
"enabled": true,
"headers": { "Authorization": "Bearer <apiKey-from-auth.json>" }
}
}
If you want a narrower-scoped Bearer for MCP (different from the chat/inference key), set features.mcpToken. Operator overrides win: if you already set mcp.omniroute in opencode.json, the plugin will not overwrite it.
Comparison vs @omniroute/opencode-provider
@omniroute/opencode-provider is the existing config-generator package — it writes a frozen provider.<id> block into opencode.json at build time. This plugin is the runtime integration.
@omniroute/opencode-plugin (this) |
@omniroute/opencode-provider |
|
|---|---|---|
| Type | OC plugin | Config generator (CLI/build-time) |
| Models | Live from /v1/models |
Frozen at scaffold |
| Combos | LCD-aggregated live | None |
| Gemini sanitize | Yes | N/A |
| OC UI integration | /connect, /models |
None |
| Multi-instance | Native | Manual |
Both can coexist; pick the one that fits your environment.
Requirements
- Node
>=22.22.3(perengines.node); tested on Node 22 and 24. - OpenCode: verified end-to-end against
opencode@1.15.5with@opencode-ai/plugin@1.15.6. - OC plugin peer (
@opencode-ai/plugin)>=1.14.49for the full feature set (provider hook surfaces models in/models). On<=1.14.48, the plugin falls back to itsconfighook, writing a static catalog snapshot intoconfig.provider[id]so models still appear. - The plugin uses the OC v1 plugin shape (
default: { id, server }) — older OC releases that only walk named exports will reject it. Stay on OC ≥1.15.
License
MIT. See LICENSE.