Files
OmniRoute/@omniroute/opencode-plugin
Diego Rodrigues de Sa e Souza de60b4b171 Release v3.8.23
* chore(release): open v3.8.23 development cycle

* fix(anthropic): strip top_p when temperature is set to avoid 400 (#3691)

Integrated into release/v3.8.23

* fix(vertex): support Vertex AI Express-mode API keys (#3690)

Integrated into release/v3.8.23

* fix(stream): error on empty Claude SSE instead of synthetic success (#3689)

Integrated into release/v3.8.23

* fix(oauth): stop token-refresh invalidation loop + harden proxy resolution (#3692)

Integrated into release/v3.8.23

* docs: add FUNDING.yml and Support section to README (#3698)

Integrated into release/v3.8.23

* feat: gemini - handle known ratelimits (#3686)

Integrated into release/v3.8.23

* fix: stream combo fails over on empty content-filtered response (#3685) (#3702)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(antigravity): preserve gemini-3.1-pro high/low budget tiers (#3696) (#3703)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(auto-combo): add auto-updating model intelligence scoring (#3660)

Integrated into release/v3.8.23

* fix(gemini): context-mode fallback for signatureless tool calls (#3688) (#3704)

* chore(quality-gate): reconcile file-size baseline (27 files + providerLimits.ts) (#3705)

* feat(vertex): dynamic model discovery via Generative Language models API (#3712)

Integrated into release/v3.8.23. Vertex dynamic model discovery — surfaces image models (imagen-*, gemini-*-image), embeddings and audio from the live Generative Language catalog, with cached→static fallback and the shared parseGeminiModelsList helper. Validated: parser test 5/5, typecheck:core clean.

* fix(combo): gate reasoning token buffer (#3700)

Integrated into release/v3.8.23. Makes the #3588 reasoning token buffer safe and configurable: only inflates max_tokens when the model has a known, non-default output cap and the buffered value fits inside it; otherwise preserves/clamps the client limit. Adds the reasoningTokenBufferEnabled kill switch (default ON). Validated: combo-routing-engine 81/81, combo-config 25/25, combo-quality-validator-reasoning 12/12, phase1f 10/10, typecheck:core clean.

* refactor(#3501): god-component Phase 1g-1j — client 4062→3408 LOC (-654) (#3717)

Phase 1g-1j of #3501: client 4062→3408 LOC. Pure extraction (ProviderPlaygroundPanel, useCommandCodeAuth, useExternalLinkFlow+ExternalLinkModal, useAuthFileHandlers) + loadConnProxies ReferenceError fix + phase1f test path fix.

Co-authored-by: oyi77 <14921983+oyi77@users.noreply.github.com>

* refactor(#3501): god-component Phase 1k-1m — client 3408→2553 LOC (-855) (#3721)

Phase 1k-1m of #3501: client 3408→2553 LOC. Pure extraction (useModelImportHandlers+ImportProgressModal, useModelVisibilityHandlers, ProviderModelsSection).

Co-authored-by: oyi77 <14921983+oyi77@users.noreply.github.com>

* docs(changelog): restore #3590 bullet lost on the v3.8.20 release branch

The fix itself reached main pre-tag via cherry-pick #3591, but its changelog
bullet (commit e33fdd4ab) only ever existed on release/v3.8.20 after the
squash-merge. Restored under [3.8.20] per the 2026-06-12 release-branch
leftover audit (_tasks/release-audit/release-leftovers-audit-2026-06-12.md).

* fix(kiro): resolve quota for IAM Identity Center accounts missing a profileArn (#3722)

Integrated into release/v3.8.23

* refactor(#3501): god-component Phase 1n-1s — client 2553→1376 LOC (-1177) (#3725)

Phase 1n-1s of #3501: client 2553→1376 LOC. Pure extraction (ConnectionsListPanel, ConnectionsHeaderToolbar, ZedImportCard, BatchTestResultsModal, AdaptaTutorialModal, useApiKeySave + helpers).

Co-authored-by: oyi77 <14921983+oyi77@users.noreply.github.com>

* feat(model-lockout): settings UI, backend integration, error classification, and success-decay recovery (#3629)

Integrated into release/v3.8.23

* refactor(#3501): god-component Phase 1t — client 1376→781 LOC (≤800 TARGET REACHED ) (#3727)

Phase 1t of #3501: client 1376→781 LOC (≤800 reached). Original god-component 12,882→781 (−94%).

Co-authored-by: oyi77 <14921983+oyi77@users.noreply.github.com>

* fix: bundle @omniroute/opencode-plugin inside omniroute + add 'setup opencode' CLI command (#3726)

Integrated into release/v3.8.23

* feat(vertex): self-tracked USD spend since account added (#3724)

Integrated into release/v3.8.23

* fix(qwen-web): migrate to v2 chat API with full cookie-jar replay (#3288) (#3723)

Integrated into release/v3.8.23

* fix(sse): make safeLogEvents async — 'await' in a sync function broke every chatHelpers import

#3692 added a lazy 'await import(proxyEgress)' for egress-IP visibility inside
safeLogEvents, which is a sync function — an ES syntax error. It went unnoticed
because typecheck:core does not cover src/sse and no test in the merge gates
loaded chatHelpers via tsx; any consumer that did (chat-context-relay and
chat-route-coverage suites, integration harnesses) failed at module load with
'await can only be used inside an async function'.

safeLogEvents is fire-and-forget logging with an outer try/catch, so making it
async (and 'void'-ing the single chat.ts call site) preserves behavior exactly.

Validation: tests/unit/chat-context-relay.test.ts + chat-route-coverage.test.ts
went from failing-at-load to green (+14 tests destravados).

* fix(sse): remove cross-provider credential leak in emergency fallback + combo/proxy audit fixes (#3699)

Integrated into release/v3.8.23

* fix(executors): inject MiMoCode anti-abuse marker so free endpoint stops 403ing (#3728)

Integrated into release/v3.8.23

* fix(dashboard): repair "Test all models" — toast crash, status icons, auto-hide (#3729)

Integrated into release/v3.8.23

* chore(deps): bump actions/upload-artifact from 4 to 7 (#3735)

Integrated into release/v3.8.23 — aligns upload-artifact to v7 (already used across ci.yml).

* chore(deps): bump actions/cache from 4 to 5 (#3734)

Integrated into release/v3.8.23 — actions/cache v4→v5.

* chore(deps): bump actions/download-artifact from 4 to 8 (#3733)

Integrated into release/v3.8.23 — download-artifact v4→v8.

* feat(fallback): add OMNIROUTE_EMERGENCY_FALLBACK env switch (#3741)

Adds an OMNIROUTE_EMERGENCY_FALLBACK env switch to disable the emergency budget-exhaustion fallback (reroute to free nvidia/gpt-oss-120b). Default unchanged (enabled). Closes #3739, related #2879.

Integrated into release/v3.8.23.

* i18n: comprehensive zh-CN translation improvements (#3736)

Aligns zh-CN to en (hundreds of entries), translates batch-action labels + settings sidebar menu, adds categoryConfig/endpointTokenSaver keys, resolves __MISSING__ stubs. Sidebar/SidebarTab hardcoded strings replaced with t(). en.json purely additive (8 new sidebar.* keys, 0 removed); cli-i18n gate green.

Integrated into release/v3.8.23.

* chore(release): v3.8.23 — 2026-06-12

- CHANGELOG: complete v3.8.23 section (28 bullets, 27 commits)
- fix(webdav): resolve promise on writeStream finish, not req end — eliminates
  intermittent 500 on PUT update (writeStream may not have flushed at rename time)
- test(autoCombo): stub DB calls from PR #3660 in tieredRotation.test.ts to prevent
  5s timeout in vitest (getModelIntelligenceBySource DB init path)
- chore(env-sync): add XDG_DATA_HOME + OMNIROUTE_OPENCODE_PLUGIN_DIR to IGNORE_FROM_CODE
  allowlist (introduced by PR #3726 setup-open-code.mjs, not OmniRoute config vars)
- chore(cli): regenerated bin/cli/api-commands/*.mjs (7 new, 27 updated)

* fix(model-family): fallback lookup also tries bare model name with dots

getNextFamilyFallback normalized dots-to-hyphens ("gemini-3.1-pro-high" →
"gemini-3-1-pro-high") but MODEL_FAMILIES keys use the literal dot form. The
lookup always missed, returning null for any model whose dots are part of the
name rather than a version separator.

Fallback: try MODEL_FAMILIES[lookupKey] ?? MODEL_FAMILIES[bareModel] so both
naming conventions are covered. Fixes T30 test (pre-existing since v3.8.22).

* feat: expose API key cost drilldown + quota % used (#3742)

Adds all-time USD cost per API key in the API Key Manager, a per-key deep-link into the Cost Explorer (filtered + grouped by model), URL-param hydration of range/groupBy/apiKeyIds, and a '% used' quota display. Review adjustments: extracted URL-param parsers to a tested module (Rule #18), i18n'd the new strings (en + zh-CN), dropped the redundant webdav-handler entry already on release.

Integrated into release/v3.8.23.

* feat: add provider display modes — All / Configured / Compact (#3743)

Replaces the Providers page configured-only toggle with All/Configured/Compact display modes (Compact = flat deduped grid, no-auth last). Persists the preference and migrates the legacy localStorage key. Rebased onto release/v3.8.23.

Integrated into release/v3.8.23.

* fix(cache): scope semantic-cache signature to API key (#3740)

Adds the api_key_id dimension to generateSignature's SHA-256 hash so two callers with different API keys never receive each other's cached responses. Threads apiKeyId through checkSemanticCache + both write sites; migration 098 clears pre-existing key-less entries; unauthenticated requests stay isolated from keyed ones. 3 TDD tests.

Integrated into release/v3.8.23.

* fix(responses): apply OpenAI Responses API stream=false spec default (#3708)

resolveStreamFlag now applies the stream=false-when-omitted default for sourceFormat=openai-responses (same as the existing claude path), so spec-compliant /v1/responses upstreams that return JSON no longer fall through to the wildcard-Accept heuristic and trigger STREAM_EARLY_EOF / 502. Codex CLI (stream:true) and explicit text/event-stream clients unaffected.

Integrated into release/v3.8.23.

* chore(release): reconcile CI gates for v3.8.23

- file-size baseline: re-freeze 8 files grown by PRs #3742/#3743/#3740
  (cost drilldown, provider display modes, cache key isolation)
- ARCHITECTURE.md: update executor count 55→60 (check:docs-counts drift)
- .env.example: add OMNIROUTE_EMERGENCY_FALLBACK (#3741, env-doc-sync)
- CHANGELOG: add formatted bullets for #3742, #3743, #3708, #3740,
  model-family-fallback fix; remove duplicate raw ### Fixed section

* test: restore assert count to satisfy check:test-masking gate

Three test files had net assertion removals after behavior-changing PRs:
- chatcore-translation-paths: emergency fallback moved to routing layer
  (#3699) — add body error assertion + model-name guard
- executor-vertex-extended: non-JSON is now Express API key (#3690) —
  add projects/-path guard to the express-key URL test
- stream-utils: empty streams now emit error (#3685) — add code/message/
  status/completePayload guards to both passthrough and translate variants

All new assertions are meaningful (code enum value, 5xx range, non-empty
message, onComplete must-not-fire contract).

* fix(ci): move rtl-logical-classes test to ui/ so vitest:ui runner collects it

---------

Co-authored-by: Felipe Almeman <4226997+zhiru@users.noreply.github.com>
Co-authored-by: NOXX - Commiter <artur1992123@mail.ru>
Co-authored-by: Nick Sullivan <142708+TechNickAI@users.noreply.github.com>
Co-authored-by: Markus Hartung <mail@hartmark.se>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: PizzaV <103120356+pizzav-xyz@users.noreply.github.com>
Co-authored-by: Randi <55005611+rdself@users.noreply.github.com>
Co-authored-by: oyi77 <14921983+oyi77@users.noreply.github.com>
Co-authored-by: Chewji <126886556+Chewji9875@users.noreply.github.com>
Co-authored-by: Hernan Javier Ardila Sanchez <hjasgr@gmail.com>
Co-authored-by: Felipe Sartori <felipesartori.ti@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Zois Pagoulatos <zpagoulatos@hotmail.com>
Co-authored-by: sdfsdfw2 <167810361+sdfsdfw2@users.noreply.github.com>
Co-authored-by: Witroch4 <witalo_rocha@hotmail.com>
2026-06-12 23:49:22 -03:00
..
2026-06-12 23:49:22 -03:00
2026-06-11 18:52:29 -03:00
2026-05-23 01:46:59 -03:00
2026-05-23 01:46:59 -03:00
2026-05-23 01:46:59 -03:00
2026-06-11 18:52:29 -03:00
2026-06-12 23:49:22 -03:00
2026-05-23 01:46:59 -03:00
2026-05-23 01:46:59 -03:00

@omniroute/opencode-plugin

Recommended way to use OmniRoute with OpenCode. 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.

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/models and /api/combos at OpenCode startup, in Node.js — no CORS, no WebView restrictions
  • Emitting the provider block dynamically in the plugin's config/provider hook — so opencode.json only needs the plugin entry, not a static provider.omniroute
  • Re-fetching on a configurable TTL (default 5 min), so new models / combo changes in the OmniRoute UI appear without restarting OpenCode
  • Computing limit.context for combos as min(member.context_length) from the live catalog (no more null values that cause 4K-token truncation)
  • Auto-pickup of interleaved capability 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

  1. Locates the bundled plugin inside the omniroute installation
  2. Copies dist/ + package.json to ~/.config/opencode/plugins/omniroute/
  3. Writes/updates opencode.json with the plugin entry (idempotent, replaces legacy entries)
  4. (With --auth) runs opencode auth login so 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",
      },
    ],
  ],
}
opencode auth login --provider omniroute
# prompts for the OmniRoute API key, writes to ~/.local/share/opencode/auth.json

⚠ Use the --provider flag explicitly. opencode auth login omniroute is parsed as a positional url argument by current OC releases (≤1.15.5) and fails with fetch() 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 same dist/index.js collapse 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 an instances: [...] 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
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, cachedcacheRead, cache_creationcacheWrite) 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 ModelsGHM, Gemini-cliGEMINI-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: true drops models whose canonical provider has no healthy connection in your OmniRoute instance — your /models picker stays focused on what you can actually call.
  • diskCache: true (default) writes a snapshot to ${OPENCODE_DATA_DIR}/plugins/omniroute-<providerId>.json on every healthy refresh. On a cold start where /v1/models is unreachable (laptop offline, IP whitelist drop), the snapshot hydrates the static block so OC still shows the catalog instead of a stub.
  • compressionMetadata: true annotates 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 shows Claude - Claude Opus 4.7 for cc/claude-opus-4-7, Kiro - Claude Opus 4.7 for kr/claude-opus-4-7, and GHM - GPT 5 for ghm/gpt-5 (slot.name GitHub Models > 8 chars → abbreviated). Critical when the same model id is sold through multiple upstream connections with different cost/auth/rate-limit profiles. Set to false to 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 (per engines.node); tested on Node 22 and 24.
  • OpenCode: verified end-to-end against opencode@1.15.5 with @opencode-ai/plugin@1.15.6.
  • OC plugin peer (@opencode-ai/plugin) >=1.14.49 for the full feature set (provider hook surfaces models in /models). On <=1.14.48, the plugin falls back to its config hook, writing a static catalog snapshot into config.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.