@omniroute/opencode-plugin
v0.1.0
Published
OpenCode plugin for the OmniRoute AI Gateway. Drives dynamic model discovery, /connect auth flow, and multi-instance OmniRoute providers via the official @opencode-ai/plugin contract.
Downloads
164
Maintainers
Readme
@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-pluginUntil 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=1Then 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-preprodEach 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-pluginopencode.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 + config |
| combo/<slug> namespace + Combo: prefix | Combos surface under combo/claude-primary (not the upstream UUID) and the picker shows Combo: claude-primary so they stand apart from raw provider/model pairs | both hooks |
| Nice names + cost | /api/pricing/models display names AND /api/pricing per-million-token cost overlaid onto the live catalog | both hooks |
| Canonical-twin dedup + alias-fallback | /v1/models exposes the same upstream model under both short alias (cc/claude-opus-4-7) and canonical name (claude/claude-opus-4-7); the plugin drops the canonical twin when an alias twin exists (no duplicate rows in the picker) and reverse-maps canonical → alias to pick up enrichment for short aliases (dg/nova-3 → Deepgram - Nova 3) that /api/pricing/models only indexes by canonical | both hooks |
| Compression pipeline tags | Combo names get tagged with their compression pipeline (e.g. Combo: claude-primary [rtk🟡 → caveman🟠]) when features.compressionMetadata: true. Intensity tokens render as a traffic-light emoji: 🟢 lite/minimal · 🟡 standard · 🟠 aggressive/full · 🔴 ultra | both hooks |
| Provider-tag prefix | Prepend short upstream-provider label to enriched names (e.g. Claude - Claude Opus 4.7 vs Kiro - Claude Opus 4.7, GHM - GPT 5) so same-id models routed via different upstream connections group visibly in the picker (default-on, opt-out via features.providerTag: false) | both hooks |
| Usable-only filter | Filter to providers with at least one healthy connection in /api/providers (opt-in via features.usableOnly) | both hooks |
| Disk-cache fallback | Last-known-good catalog persisted to disk; hydrates on a cold start when /v1/models is unreachable (default-on, opt-out via features.diskCache: false) | config |
| 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.15.5 fallback: writes static catalog into config.provider[id] (config hook is the only one that fires in serve mode on these versions) | 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. Combos are keyed under the combo/<slug> namespace and labelled Combo: <name> in the model picker so they're distinguishable from raw provider/model pairs. |
| 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. Combo: claude-primary [rtk🟡 → caveman🟠]. Intensity tokens render as traffic-light emoji (🟢 lite/minimal · 🟡 standard · 🟠 aggressive/full · 🔴 ultra) so the picker advertises "how compressed" each combo is at a glance. |
| providerTag | boolean | true | Prepend a short upstream-provider label to the enriched display name with " - " separator, so cc/claude-opus-4-7 → Claude - Claude Opus 4.7 differs visibly from kr/claude-opus-4-7 → Kiro - Claude Opus 4.7 in the OC TUI model picker. Label resolution: use /api/pricing/models[<alias>].name verbatim when ≤8 chars (e.g. Claude, Kiro, Codex, Qwen), otherwise fall back to UPPER(alias) (e.g. GitHub Models → GHM, Gemini-cli → GEMINI-CLI). Idempotent. Combos intentionally skipped (the Combo: prefix already conveys multi-upstream). |
| usableOnly | boolean | false | Read /api/providers and filter the catalog to providers that have at least one connection with isActive: true AND testStatus: 'active'. Subtract-filter semantics: providers unknown to BOTH the pricing-models catalog AND the connection table pass through (so synthetic prefixes like agentrouter/* survive). On fetch failure the filter is disabled for the refresh — never hides the whole catalog. |
| diskCache | boolean | true | Persist the last successful /v1/models + /api/combos + enrichment + connections + compression snapshot to ${OPENCODE_DATA_DIR ?? ~/.local/share/opencode}/plugins/omniroute-<providerId>.json. On a subsequent cold start where /v1/models throws (network down / IP whitelist drop / 5xx) the static block hydrates from the snapshot so OC's model picker survives offline. Soft-fail on read/write — never blocks publishing. |
| 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.
Example — production-leaning defaults (clean picker, offline resilience)
{
"plugin": [
[
"@omniroute/opencode-plugin",
{
"providerId": "omniroute",
"baseURL": "https://or.example.com",
"features": {
"combos": true,
"enrichment": true,
"compressionMetadata": true,
"usableOnly": true,
"diskCache": true,
},
},
],
],
}usableOnly: truedrops models whose canonical provider has no healthy connection in your OmniRoute instance — your/modelspicker stays focused on what you can actually call.diskCache: true(default) writes a snapshot to${OPENCODE_DATA_DIR}/plugins/omniroute-<providerId>.jsonon every healthy refresh. On a cold start where/v1/modelsis unreachable (laptop offline, IP whitelist drop), the snapshot hydrates the static block so OC still shows the catalog instead of a stub.compressionMetadata: trueannotates combo display names with their pipeline using traffic-light emoji for intensity (e.g.Combo: claude-primary [rtk🟡 → caveman🟠]) so the picker advertises which compression each combo applies and how heavy it is at a glance. Palette: 🟢 lite/minimal · 🟡 standard · 🟠 aggressive/full · 🔴 ultra. Unknown intensities fall through to raw text ([rtk:custom-thing]) so the plugin never hides a value OmniRoute knows but the plugin doesn't.providerTag: true(default) prepends a short upstream-provider label so the picker showsClaude - Claude Opus 4.7forcc/claude-opus-4-7,Kiro - Claude Opus 4.7forkr/claude-opus-4-7, andGHM - GPT 5forghm/gpt-5(slot.nameGitHub Models> 8 chars → abbreviated). Critical when the same model id is sold through multiple upstream connections with different cost/auth/rate-limit profiles. Set tofalseto keep the pre-v3.8.3 unsuffixed format.
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
[email protected]with@opencode-ai/[email protected]. - 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.
