Files
OmniRoute/@omniroute/opencode-plugin
Diego Rodrigues de Sa e Souza 68d5a0ab27 Release v3.8.10 (#3140)
* chore(release): open v3.8.10 development cycle

Bump 3.8.9 → 3.8.10 across package.json, lockfile, electron, open-sse, and
docs/reference/openapi.yaml; add the [3.8.10] CHANGELOG section (root + 41 i18n
mirrors) as the integration target for the cycle. Entries land here as work
merges into release/v3.8.10; finalized by the release flow.

* fix(providers): resolve web provider alias collisions

Assign unique aliases to HuggingChat, Kimi Web, and Qwen Web so they no longer shadow primary providers or trigger startup warnings.

Add a unit test to enforce provider alias uniqueness and prevent future collisions. Also expand local ignore and VS Code exclude rules for agent, build, and worktree artifacts.

* fix(responses): normalize image_url parts across input paths (#3150)

Normalize image_url parts across all Responses input paths. Integrated into release/v3.8.10.

* fix(api-manager): preserve API key expiration local time (#3146)

Preserve API key expiration local time + clear button. Integrated into release/v3.8.10.

* Strip previous_response_id for stateless Responses upstreams (#3143)

Strip previous_response_id for stateless Responses upstreams (auto/strip/preserve). Integrated into release/v3.8.10.

* fix(opencode-plugin): map thinking cap to interleaved in model+combo (#3138)

Map caps.thinking to ModelV2.capabilities.interleaved for opencode-plugin. Integrated into release/v3.8.10.

* fix(providers): use synced models as fallback for all providers (#3148)

Use synced models as authoritative local catalog for all providers (+regression test). Integrated into release/v3.8.10.

* fix(qoder): bifurcate validation by token type — PAT→Cosy, regular API key→dashscope (#3149)

Bifurcate Qoder validation by token type (PAT→Cosy, regular→dashscope) +regression test. Integrated into release/v3.8.10.

* fix(antigravity): dynamic model resolution via MITM alias table (#3144)

Dynamic antigravity MITM model resolution in the executor (+bug fix +regression test; DB import dropped from client-reachable config). Integrated into release/v3.8.10.

* Feature/batch allow big (#3128)

Podman deployment options + larger upload body-size limits (+CONTAINER_HOST docs). Integrated into release/v3.8.10.

* fix(fireworks): preserve fully-qualified router/model IDs (#3133) (#3160)

Fireworks router IDs (accounts/fireworks/routers/...) were double-prefixed
with accounts/fireworks/models/ → upstream 404. Add optional
acceptedModelIdPrefixes to the registry entry and skip the prepend when the
model already starts with an accepted prefix.

Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com>

* fix(llama-cpp): route to configured local baseUrl instead of OpenAI (#3136) (#3161)

llama-cpp was missing from the local-provider group in buildUrl(), so it
fell through to the OpenAI baseUrl and returned an OpenAI 401. Add the
case to resolve the connection's providerSpecificData.baseUrl.

Co-authored-by: tjengbudi <tjengbudi@users.noreply.github.com>

* fix(t3-chat-web): parse cookies + convexSessionId from stored credential (#3007) (#3162)

The executor read credentials.cookies/convexSessionId, but the pipeline
only stores the pasted string under apiKey → t3.chat always 400'd. Parse
both values from apiKey (fallback accessToken), mirroring validation.ts.

Co-authored-by: minhtran162 <minhtran162@users.noreply.github.com>

* fix(minimax): stop capping MiniMax-M3 / M2.7 max_tokens at 8192 (#3141) (#3163)

MiniMax-M3 had no MODEL_SPECS entry and capitalized MiniMax-M2.7 missed
its lowercase spec (case-sensitive lookup) → both fell to the 8192 default
cap. Add the M3 spec (512K output), alias the capitalized ids, and make
getModelSpec lookups case-insensitive.

Co-authored-by: totaltube <totaltube@users.noreply.github.com>

* fix(github-copilot): discover model catalog live from api.githubcopilot.com (#3120, #3121) (#3164)

The github (Copilot) provider had a static hardcoded catalog with no
discovery source, so Import Models never refreshed (#3120) and advertised
non-entitled models that 400 on use (#3121). Add a live /models fetch with
fallback to the static list.

Co-authored-by: gabrielmoreira <gabrielmoreira@users.noreply.github.com>

* fix(combo): invalidate nested-combo cache on edits + log DATA_DIR (#3147) (#3165)

Editing a combo did not invalidate the 10s nested-combo expansion caches
(chat.ts getCombosCachedForChat + chatCore.ts getCombosCached; the exported
clearCombosCache was dead code), so a removed nested target/model could be
served as a phantom for up to 10s. Wire a shared monotonic combos-cache
version in readCache (bumped by invalidateDbCache("combos") on every combo
write); both cache layers treat a version mismatch as a miss.

Also log the resolved DATA_DIR/SQLITE_FILE absolute path at DB init so the
reporter's 'persists across restart + volume wipe' symptom (a multi-replica
Docker volume/DATA_DIR mismatch, not a routing bug) is diagnosable from logs.

Includes consolidated CHANGELOG entries for #3133/#3136/#3007/#3141/#3120/#3121.

Co-authored-by: ViFigueiredo <ViFigueiredo@users.noreply.github.com>

* fix(web-tools): parse bare JSON tool calls (#3157)

Parse bare JSON tool calls for deepseek-web (#2820) + fuzzy tool-name matching. Integrated into release/v3.8.10.

* fix(misc): minor fixes across reasoning cache, account fallback, binary manager (#3177)

Misc: ProviderProfile export, DeepSeek reasoning regex, binary guard. Integrated into release/v3.8.10.

* fix(kiro): minor OAuth social exchange tweaks (#3176)

Kiro social OAuth: optional targetProvider passthrough. Integrated into release/v3.8.10.

* deps: bump hono from 4.12.18 to 4.12.23 (#3179)

Bump hono to 4.12.23. Integrated into release/v3.8.10.

* fix(providerRegistry): update kilocode format and executor (#3166)

kilocode: openai format + default executor (matches kilo-gateway) + registry test. Integrated into release/v3.8.10.

* feat(metrics): cross-request TTFT and gap latency after tool calls (#3173)

Cross-request TTFT + gap-after-tool latency metrics (+test). Integrated into release/v3.8.10.

* feat(dashboard): provider stats API endpoint and dashboard page (#3175)

Provider stats dashboard + API (SQL moved to db module per Hard Rule #5, +test). Integrated into release/v3.8.10.

* fix(usage): sequential+spaced OAuth quota sync, reactive force-refresh, actionable 401 (#3156)

Sequential+spaced OAuth quota sync, reactive force-refresh on 401, actionable 401 in UI. Integrated into release/v3.8.10.

* fix(healthcheck): per-provider proactive-refresh skip list (rescue short-TTL OAuth) (#3159)

Per-provider proactive-refresh skip list (OMNIROUTE_HEALTHCHECK_SKIP_PROVIDERS) to rescue short-TTL OAuth. Integrated into release/v3.8.10.

* feat(quota): show OAuth token expiry on provider cards (small, blue, informative) (#3178)

Show OAuth token expiry on provider cards (small, blue, informative). Integrated into release/v3.8.10.

* fix(providers): empty refresh must not resurface just-cleared synced models (#3181)

Empty refresh must not resurface just-cleared synced models (fixes the release-blocking provider-models-route test). Integrated into release/v3.8.10.

* chore(release): v3.8.10 — 2026-06-04 (finalize CHANGELOG)

---------

Co-authored-by: Wilson <pedbookmed@gmail.com>
Co-authored-by: Xiangzhe <32761048+xz-dev@users.noreply.github.com>
Co-authored-by: Jan Leon <Jan.gaschler@gmail.com>
Co-authored-by: M.M <mr.maatoug@gmail.com>
Co-authored-by: Hernan Javier Ardila Sanchez <hjasgr@gmail.com>
Co-authored-by: Markus Hartung <mail@hartmark.se>
Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com>
Co-authored-by: tjengbudi <tjengbudi@users.noreply.github.com>
Co-authored-by: minhtran162 <minhtran162@users.noreply.github.com>
Co-authored-by: totaltube <totaltube@users.noreply.github.com>
Co-authored-by: gabrielmoreira <gabrielmoreira@users.noreply.github.com>
Co-authored-by: ViFigueiredo <ViFigueiredo@users.noreply.github.com>
Co-authored-by: PizzaV <103120356+pizzav-xyz@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nicolas Lorin <androw95220@gmail.com>
2026-06-04 20:05:38 -03:00
..
2026-06-04 20:05:38 -03:00
2026-05-24 18:05:58 -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-05-23 01:46:59 -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

@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 --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.