Opencode Omniroute Plugin
FreeMaintainedOpenCode plugin for the OmniRoute AI Gateway. Drives dynamic model discovery, /connect auth flow, and multi-instance OmniRoute providers via the official @openc
About
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.
README
Recommended way to use OmniRoute with OpenCode. Pulls a live model catalog from
/v1/models(including-low/-medium/-high/-thinkingvariants as first-class IDs), aggregates combos via/api/combosusing 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.
Why this and not @omniroute/opencode-provider?
@omniroute/opencode-provider is the legacy config-generator package — it writes a frozen provider.omniroute block into opencode.json with a hardcoded list of 8 models (OMNIROUTE_DEFAULT_OPENCODE_MODELS). It works on the CLI but in the OpenCode Desktop / Web builds (Tauri / Electron) the runtime re-runs the model picker and the static block surfaces only a few of those — and they drift behind the live OmniRoute catalog.
This plugin solves that by:
- Fetching
/v1/modelsand/api/combosat OpenCode startup, in Node.js — no CORS, no WebView restrictions - Emitting the provider block dynamically in the plugin's
config/providerhook — soopencode.jsononly needs the plugin entry, not a staticprovider.omniroute - Re-fetching on a configurable TTL (default 5 min) so new models / combo changes appear without restarting OpenCode
- Running a one-shot startup sync that refreshes the cache + disk snapshot only when
/v1/modelsreturns a catalog that differs from the previous one (in-memory cache or disk snapshot). No periodic background timer — the TTL on-demand refresh covers mid-session changes. - Exposing a force-refresh path (
omniroute_sync_modelstool +/omni-synccommand template) equivalent to Pi/omni sync - Computing
limit.contextfor combos asmin(member.context_length)from the live catalog (no morenullvalues that cause 4K-token truncation) - Auto-pickup of
interleavedcapability for thinking models (merged via PR #3138)
If you only have the legacy opencode-provider block in your opencode.json, replace it with a single plugin entry. No other config changes required — the same auth.json API key works.
Install
The plugin ships pre-built inside the omniroute npm package since v3.8.23.
If you have OmniRoute installed, the plugin is already on disk:
# 1. One command — copy the plugin into OpenCode and update opencode.json
omniroute setup opencode --auth
# 2. Follow the interactive prompt to enter your OmniRoute API key
# 3. Restart OpenCode — /models lists the full live catalog
The --auth flag runs opencode auth login --provider omniroute automatically.
Use --base-url to point at a non-default OmniRoute address:
omniroute setup opencode --base-url https://or.example.com --auth
What it does
- Locates the bundled plugin inside the omniroute installation
- Copies
dist/+package.jsonto~/.config/opencode/plugins/omniroute/ - Writes/updates
opencode.jsonwith the plugin entry (idempotent, replaces legacy entries) - (With
--auth) runsopencode auth loginso the API key is stored
Re-run any time to update the plugin or change the base URL. Older entries for
@omniroute/opencode-provider or the legacy opencode-omniroute-auth package are
automatically cleaned up.
Manual install (without omniroute CLI)
If you cannot run omniroute setup opencode (local dev, CI, air-gapped), reference
the built artifact directly:
cd @omniroute/opencode-plugin && npm run build && npm pack
# then extract into ~/.config/opencode/plugins/omniroute-opencode-plugin/
And add the entry to opencode.json manually (see Quick Start below).
Peer dep: @opencode-ai/plugin (managed by your OpenCode install).
Quick start (single instance, manual)
// opencode.json
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
[
"./plugins/omniroute-opencode-plugin/dist/index.js",
{
"providerId": "omniroute",
"baseURL": "https://or.example.com",
// TTL on-demand refresh; the one-shot startup sync only writes
// when the catalog actually changed. Default 300000 (5 min).
"modelCacheTtl": 300000,
},
],
],
}
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.
Live catalog refresh (auto + force)
While OpenCode is running, the plugin keeps the model catalog fresh in two ways:
| Mechanism | Default | What it does |
|---|---|---|
modelCacheTtl |
300000 (5 min) |
On-demand TTL: next provider/models hook after expiry re-fetches /v1/models |
| Startup sync (one-shot) | n/a | At plugin init, fetch /v1/models once and only update cache + disk snapshot when the catalog has new models compared to the previous one. No periodic timer. |
autoSyncIntervalMs |
300000 (5 min) |
Legacy — accepted by the schema for backward compat but ignored at runtime. Background polling was removed; use modelCacheTtl for on-demand refreshes and /omni-sync for explicit force-refresh. |
Force sync now (Pi /omni sync equivalent) — OpenCode has no Pi-style slash-command registration API, so the plugin wires both a tool and command templates:
- Tool:
omniroute_sync_models— invalidates in-memory + disk caches, re-fetchesGET /v1/models(and combos/enrichment when enabled), returns{ ok, count, ... }. - Command templates (type these in OpenCode):
/omni-sync— asks the agent to callomniroute_sync_modelsand report the result/omni-autosync— asks the agent to report current startup-sync /modelCacheTtlstatus
/omni-sync
/omni-autosync
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 + 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 |
| Enabled-only filter | Filter to providers with isActive: true in /api/providers regardless of testStatus (opt-in via features.enabledOnly) |
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 |
managementReadToken |
string |
falls back to apiKey |
Optional read-only token for management catalog GETs; /v1 inference stays on the connected apiKey |
features |
object |
see below | Feature toggles (all opt-in/out, defaults preserve v0.1.0) |
For least-privilege deployments, set top-level managementReadToken to a read-only management token. It is sent only to catalog reads (/api/combos, /api/combos/auto, /api/pricing/models, /api/pricing, /api/context/combos, and /api/providers). Inference requests under /v1, including chat, continue to use the apiKey stored by OpenCode. features.mcpToken remains independent. If managementReadToken is omitted, catalog reads retain the previous apiKey behavior.
features block
Every field is optional. The picker shows only active providers' active models by default (enabledOnly + activeOnly both ON). Set either to false to restore the pre-v0.2.26 full-catalog behaviour.
| 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 → GEMINI). 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. |
enabledOnly |
boolean |
true |
Default ON. Read /api/providers and filter the catalog to providers whose connections have isActive: true. Unlike usableOnly, this ignores testStatus entirely — providers that are enabled but failing connection tests still surface their models. Subtract-filter semantics: synthetic prefixes like agentrouter/* (unknown to BOTH the connections table and the enrichment map) pass through; known-but-disabled prefixes are dropped. Soft-fails on fetch error/empty connections — never hides the whole catalog. Combos always pass through (they have no enable/disable in OmniRoute's dashboard). Set enabledOnly: false to restore full-catalog behavior. |
activeOnly |
boolean |
true |
Default ON. Read /api/models and filter the catalog to models whose available !== false. Models from disabled providers are already dropped by enabledOnly; this further drops models the operator has not explicitly pinned to the dashboard's active list. Soft-fails to no-filter on fetch error/empty response. Known limitation: /api/models IDs sometimes diverge from /v1/models IDs (upstream issue diegosouzapw/OmniRoute#10615) — models whose IDs don't match exactly pass through unfiltered. Combos always pass through. Set activeOnly: false to disable. |
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",
"managementReadToken": "<read-only-management-token>",
"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.
Example — enabled-only picker (dashboard toggle, ignores connection health)
{
"plugin": [
[
"@omniroute/opencode-plugin",
{
"providerId": "omniroute",
"features": {
"combos": true,
"enabledOnly": true,
},
},
],
],
}
enabledOnly: truemirrors the OmniRoute dashboardisActivetoggle per provider. UnlikeusableOnly, it does NOT checktestStatus— providers you've enabled in the dashboard stay in the picker even when their connection tests are momentarily failing (e.g. rate-limited, in-flight token refresh). Use this when the dashboard switch is your source-of-truth; useusableOnlywhen transient health matters more.- Combos (
auto/best-coding,combo/claude-primary, custom combos) always pass through regardless of provider state — OmniRoute's dashboard has no enable/disable for combos. The picker shows every combo thatfeatures.combos !== falsealready exposes.
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.
Install Opencode Omniroute Plugin in Claude Desktop, Claude Code & Cursor
unyly install opencode-omniroute-pluginInstalls into Claude Desktop, Claude Code, Cursor & VS Code — handles npx, uvx and build-from-source repos for you.
First time? Get the CLI: curl -fsSL https://unyly.org/install | sh
Or configure manually
Run in your terminal:
claude mcp add opencode-omniroute-plugin -- npx -y opencode-omniroute-pluginStep-by-step: how to install Opencode Omniroute Plugin
FAQ
Is Opencode Omniroute Plugin MCP free?
Yes, Opencode Omniroute Plugin MCP is free — one-click install via Unyly at no cost.
Does Opencode Omniroute Plugin need an API key?
No, Opencode Omniroute Plugin runs without API keys or environment variables.
Is Opencode Omniroute Plugin hosted or self-hosted?
Self-hosted: the server runs locally on your machine via the install command above.
How do I install Opencode Omniroute Plugin in Claude Desktop, Claude Code or Cursor?
Open Opencode Omniroute Plugin on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.
Changes
Versions and requested access over time.
- New version published
- New version published
- New version published
Related MCPs
Fetch
Web content fetching and conversion for efficient LLM usage.
Roblox Studio
Enables AI coding tools to control Roblox Studio for workspace exploration, instance manipulation, and script management. It provides tools for playtesting, sce
by paralovAWS KB Retrieval
Retrieval from AWS Knowledge Base using Bedrock Agent Runtime.
by modelcontextprotocolSpring AI MCP Server
Provides auto-configuration for setting up an MCP server in Spring Boot applications.
llm-analysis-assistant
A very streamlined mcp client that supports calling and monitoring stdio/sse/streamableHttp, and can also view request responses through the /logs page. It also
by xuzexin-hzMCP-Agent
A simple, composable framework to build agents using Model Context Protocol by [LastMile AI](https://www.lastmileai.dev)
by lastmile-aiSpring AI MCP Client
Provides auto-configuration for MCP client functionality in Spring Boot applications.
mcp.natoma.ai
A Hosted MCP Platform to discover, install, manage and deploy MCP servers by [Natoma Labs](https://www.natoma.ai)
MCPHub
Website to list high quality MCP servers and reviews by real users. Also provide online chatbot for popular LLM models with MCP server support.
MCP Servers Rating and User Reviews
Website to rate MCP servers, write authentic user reviews, and [search engine for agent & mcp](http://www.deepnlp.org/search/agent)
Compare Opencode Omniroute Plugin with
Not sure what to pick?
Find your stack in 60 seconds
Author?
Embed badge for your README
Browse similar
All ai MCPs
