mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 20:32:20 +03:00
* 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>
297 lines
28 KiB
Markdown
297 lines
28 KiB
Markdown
# @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`](https://github.com/diegosouzapw/OmniRoute/blob/main/%40omniroute/opencode-provider/src/index.ts#L48-L56)). 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:
|
|
|
|
```sh
|
|
# 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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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)
|
|
|
|
```jsonc
|
|
// opencode.json
|
|
{
|
|
"$schema": "https://opencode.ai/config.json",
|
|
"plugin": [
|
|
[
|
|
"./plugins/omniroute-opencode-plugin/dist/index.js",
|
|
{
|
|
"providerId": "omniroute",
|
|
"baseURL": "https://or.example.com",
|
|
},
|
|
],
|
|
],
|
|
}
|
|
```
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
# 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:
|
|
|
|
```jsonc
|
|
{
|
|
"$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:
|
|
|
|
```sh
|
|
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`:
|
|
|
|
```sh
|
|
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`, `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
|
|
|
|
```jsonc
|
|
{
|
|
"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:
|
|
|
|
```jsonc
|
|
"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)
|
|
|
|
```jsonc
|
|
{
|
|
"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`](https://github.com/diegosouzapw/OmniRoute/tree/main/%40omniroute/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](./LICENSE).
|