* chore(config): ignore additional agent workflow command files Add newly introduced agent workflow and Claude command files to .gitignore so proprietary automation assets are not committed. * feat(deepseek-web): fix auth to use userToken + WASM PoW solver Rewrite deepseek-web executor from broken cookie auth to userToken Bearer flow (like Chat2API). Replace pure JS Keccak PoW with WASM solver (5.8s → 86ms). Add 14 models, validation, and dashboard UX. * fix(deepseek-web): update target_path to use challenge property * refactor(deepseek-web): streamline token handling and implement cache eviction * fix(deepseek-web): fix SSE parser, prompt format, and error handling - Handle all 3 DeepSeek SSE stream formats: initial fragments, APPEND operations, and bare string tokens (fixes truncated responses) - Simplify prompt builder to send system + last user message only (DeepSeek web API is single-turn, full history caused marker leakage) - Check json.code before token extraction (fixes "did not return access token: Authorization" on code 40003 with HTTP 200) - Clear session cache alongside token cache on auth errors - Add dev origin for remote testing Co-authored-by: Cursor <cursoragent@cursor.com> * chore: ignore memory-bank and cursor agent rules from tracking Co-authored-by: Cursor <cursoragent@cursor.com> * feat: enhance documentation and configuration for Fumadocs integration - Added Fumadocs MDX support in the Next.js configuration. - Updated transpile packages to include fumadocs-ui and fumadocs-core. - Implemented a comprehensive set of redirects for documentation paths to improve navigation. - Removed the generate-docs-index script as it is no longer needed. - Updated various documentation titles for consistency and clarity. - Enhanced global styles to incorporate Fumadocs UI themes and styles. * refactor(docs): cleanup fumadocs PR — revert deepseek, add i18n fallback, restore LanguageSelector - Revert unrelated deepseek-web.ts changes (should be separate PR) - Add .source/ to .gitignore (Fumadocs generated files) - Remove contributor IP from allowedDevOrigins - Add i18n runtime fallback: reads NEXT_LOCALE cookie, loads translated .md from docs/i18n/<locale>/docs/ (preserves existing translation pipeline) - Restore LanguageSelector in Fumadocs layout nav - Restore SEO metadata (title template, description, robots) * fix(codex): use allowlist to strip non-Responses-API fields in non-passthrough path (#2608) (#2615) Integrated into release/v3.8.3 — fix(codex): allowlist-based sanitization for gpt-5.5 Responses API * fix(deepseek-web): fix SSE parser, prompt format, error handling, and cache keys (#2616) Integrated into release/v3.8.3 — fix(deepseek-web): SSE parser (APPEND + bare tokens), prompt builder, error handling, session cache cleanup * chore(config): ignore additional agent workflow command files Add newly introduced agent workflow and Claude command files to .gitignore so proprietary automation assets are not committed. * feat(docs): migrate /docs to Fumadocs MDX with nested routes (#2614) Integrated into release/v3.8.3 — Fumadocs MDX migration with nested routes, search API, and 50+ URL redirects * fix(catalog): skip static PROVIDER_MODELS when synced models exist (#2625) Integrated into release/v3.8.3 * fix(qoder): Cosy auth fallback for PAT tokens + vision support for qwen3-vl-plus (#2629) Integrated into release/v3.8.3 * fix(cli): register tsx loader and add opencode config subcommand (#2631) Integrated into release/v3.8.3 * feat(dashboard): add search and filters to /dashboard/api-manager (#2628) Integrated into release/v3.8.3 * fix(claude): improve Pi and OpenCode compatibility (#2621) Integrated into release/v3.8.3 * fix: restore semantic passthrough system-role-only extraction instead of full normalization (#2620) Integrated into release/v3.8.3 * fix(kiro): stabilize conversationId across prompt compression (#2630) Integrated into release/v3.8.3 * fix(deepseek-web): SSE thinking/search routing and session lifecycle (#2624) Integrated into release/v3.8.3 — DeepSeek Web SSE thinking/search routing overhaul * feat(dashboard): free-tier grouping with symbolic link in /providers (#2632) Integrated into release/v3.8.3 * fix: close implementation gaps — t3-chat-web, stream_options, combo_strategy, batch config (#2634) Integrated into release/v3.8.3 * feat(dashboard): risk notice modal for sensitive providers (#2633) Integrated into release/v3.8.3 * fix(reasoning): extend reasoning_content injection to Kimi K2 and other replay models (#2639) Integrated into release/v3.8.3 * fix(cli): Linux autostart via systemd user service (fixes #2627) (#2635) Integrated into release/v3.8.3 * Refactor/providers free tier (#2640) Integrated into release/v3.8.3 * fix(tests): remove duplicate assertion in schema coercion & fix(cli): ignore system vars in env check * fix(combo): preserve omniModel tag in streaming output for round-trip context pinning (#2646) Integrated into release/v3.8.3 * feat(dashboard): media providers pages + Web Fetch category (#2645) Integrated into release/v3.8.3 * Feature provider adapta org com tutorial de conexão em modal (#2643) Integrated into release/v3.8.3 * fix(rtk): skip content-based filter matching for non-shell tool results (#2642) Integrated into release/v3.8.3 * fix(translator): enable Claude extended thinking for Copilot Responses-API requests (#2647) Integrated into release/v3.8.3 * feat(dashboard): add search and filters to /dashboard/api-manager (#2641) Integrated into release/v3.8.3 * feat(dashboard): risk notice modal for sensitive providers (#2638) Integrated into release/v3.8.3 * feat(dashboard): mini-playground inline (Phase 4) (#2648) Integrated into release/v3.8.3 * fix(settings): fix Require Login modal Cancel button text and dismissal (#2649) Integrated into release/v3.8.3 * feat(combos): universal context handoff for cross-model conversation continuity (#2653) Integrated into release/v3.8.3 * chore(release): bump to v3.8.3 — changelog, docs, version sync * feat(i18n): complete zh-CN translations for 1220 missing keys (#2655) Integrated into release/v3.8.3 * chore(release): include electron package changes in v3.8.3 * docs(changelog): integrate PR #2655 into v3.8.3 * feat(i18n): translate 377 additional zh-CN entries (81 new keys + 296 same-as-en) (#2659) Integrated into release/v3.8.3 * feat(dashboard): add Cmd+K / Ctrl+K command palette for sidebar navigation (#2656) Integrated into release/v3.8.3 * docs: update changelog for PR integrations under v3.8.3 * feat(cli): integrate native updates, autostart and headless CLI mode (#2662) Integrated into release/v3.8.3 * fix(proxy): save dashboard custom proxies in registry (#2661) Integrated into release/v3.8.3 * feat(dashboard): chat-first test slide-over (Option A) (#2660) Integrated into release/v3.8.3 * docs: update changelog with Batch 2 PR merges for v3.8.3 * fix: add xhigh+max to effortLevel schema; add opencode-plugin publish job (#2666) Integrated into release/v3.8.3 * docs: update changelog with Batch 3 PR #2666 merge for v3.8.3 * feat(quota+providers): card-grid layout, provider group headers, Codex race fix (#2667) Integrated into release/v3.8.3 * feat(dashboard): real-time live WebSocket monitoring (#2668) Integrated into release/v3.8.3 * feat(copilot): AI assistant with CodeGraph + CLI + knowledge base (#2669) Integrated into release/v3.8.3 * feat(pipeline): pre-request middleware hooks (#2670) Integrated into release/v3.8.3 * feat(resilience): credential health check + adaptive circuit breaker (#2671) Integrated into release/v3.8.3 * feat(playground): combo routing visual simulator (#2672) Integrated into release/v3.8.3 * feat(auth): API key groups with model-level permissions (#2673) Integrated into release/v3.8.3 * feat(pwa): enhanced manifest + push notification support (#2674) Integrated into release/v3.8.3 * feat(proxy): serverless relay endpoints with rate limiting (#2675) Integrated into release/v3.8.3 * docs(changelog): update changelog for PRs 2667-2675 & fix: resolve typescript compile-time errors * fix(db): remove transactions from migrations Remove explicit transaction wrappers from recent migrations and correct the API key groups migration metadata. Also fix codegraph path resolution for ESM environments and refresh generated fumadocs source output. --------- Co-authored-by: Ömer Vehbe <ovehbe@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Mr. Meowgi <mr@meowgi.dev> Co-authored-by: Hernan Javier Ardila Sanchez <hjasgr@gmail.com> Co-authored-by: amogus22877769 <y.lev357@gmail.com> Co-authored-by: Halil Tezcan KARABULUT <info@hlltzcnkb.com> Co-authored-by: Tentoxa <53821604+Tentoxa@users.noreply.github.com> Co-authored-by: HALDRO <121296348+HALDRO@users.noreply.github.com> Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com> Co-authored-by: janeza2 <49841619+janeza2@users.noreply.github.com> Co-authored-by: df4p <38404+df4p@users.noreply.github.com> Co-authored-by: ivan-mezentsev <ivan@mezentsev.me> Co-authored-by: Chewji <126886556+Chewji9875@users.noreply.github.com> Co-authored-by: L-aros <107354918+L-aros@users.noreply.github.com> Co-authored-by: M.M <mr.maatoug@gmail.com> Co-authored-by: Benson K B <bensonkbmca@gmail.com> Co-authored-by: terence71-glitch <mcdowellterence71@gmail.com> |
||
|---|---|---|
| .. | ||
| src | ||
| tests | ||
| .gitignore | ||
| LICENSE | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| tsup.config.ts | ||
@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
--providerflag explicitly.opencode auth login omnirouteis parsed as a positionalurlargument by current OC releases (≤1.15.5) and fails withfetch() URL is invalid. Tracked upstream.
Restart OpenCode. /models lists the full live catalog. Variants (-low, -medium, -high, -thinking) and combos appear as first-class IDs — OmniRoute is the source of truth, no client-side synthesis.
Multi-instance (prod + preprod side-by-side)
⚠ OC ≤1.15.5 dedupes plugin loads by absolute module path. Two
plugin:entries pointing at the samedist/index.jscollapse into one (last-listed options win). Workaround: install the plugin twice into separate directories so each entry resolves to a distinct module file. v0.2.x will introduce aninstances: [...]shape that registers N providers from a single load.
Dual-install workaround (works today on OC ≤1.15.5)
Pack the plugin once, extract it twice into named directories, then point each plugin: entry at its own copy:
# 1. Build + pack the plugin (run from the plugin worktree)
cd /path/to/OmniRoute/@omniroute/opencode-plugin
npm run build
npm pack
# produces omniroute-opencode-plugin-0.1.0.tgz
# 2. Extract one copy per OmniRoute endpoint
mkdir -p ~/.config/opencode/plugins/omniroute-opencode-plugin-prod
mkdir -p ~/.config/opencode/plugins/omniroute-opencode-plugin-preprod
tar -xzf omniroute-opencode-plugin-0.1.0.tgz -C ~/.config/opencode/plugins/omniroute-opencode-plugin-prod --strip-components=1
tar -xzf omniroute-opencode-plugin-0.1.0.tgz -C ~/.config/opencode/plugins/omniroute-opencode-plugin-preprod --strip-components=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, cached → cacheRead, cache_creation → cacheWrite) from /api/pricing, then overlay both onto the live catalog (so the UI shows Claude 4.7 Opus with cost.input: 5, cost.output: 25 instead of raw IDs and zeroed cost). |
compressionMetadata |
boolean |
false |
Pull /api/context/combos so combo names get tagged with their compression pipeline, e.g. Combo: claude-primary [rtk🟡 → caveman🟠]. Intensity tokens render as traffic-light emoji (🟢 lite/minimal · 🟡 standard · 🟠 aggressive/full · 🔴 ultra) so the picker advertises "how compressed" each combo is at a glance. |
providerTag |
boolean |
true |
Prepend a short upstream-provider label to the enriched display name with " - " separator, so cc/claude-opus-4-7 → Claude - Claude Opus 4.7 differs visibly from kr/claude-opus-4-7 → Kiro - Claude Opus 4.7 in the OC TUI model picker. Label resolution: use /api/pricing/models[<alias>].name verbatim when ≤8 chars (e.g. Claude, Kiro, Codex, Qwen), otherwise fall back to UPPER(alias) (e.g. GitHub Models → GHM, Gemini-cli → GEMINI-CLI). Idempotent. Combos intentionally skipped (the Combo: prefix already conveys multi-upstream). |
usableOnly |
boolean |
false |
Read /api/providers and filter the catalog to providers that have at least one connection with isActive: true AND testStatus: 'active'. Subtract-filter semantics: providers unknown to BOTH the pricing-models catalog AND the connection table pass through (so synthetic prefixes like agentrouter/* survive). On fetch failure the filter is disabled for the refresh — never hides the whole catalog. |
diskCache |
boolean |
true |
Persist the last successful /v1/models + /api/combos + enrichment + connections + compression snapshot to ${OPENCODE_DATA_DIR ?? ~/.local/share/opencode}/plugins/omniroute-<providerId>.json. On a subsequent cold start where /v1/models throws (network down / IP whitelist drop / 5xx) the static block hydrates from the snapshot so OC's model picker survives offline. Soft-fail on read/write — never blocks publishing. |
geminiSanitization |
boolean |
true |
Strip $schema/$ref/additionalProperties from tool params when the model id matches gemini |
mcpAutoEmit |
boolean |
false |
Auto-write an mcp.<providerId> remote entry into the OC config pointing at <baseURL>/api/mcp/stream with the resolved Bearer token |
mcpToken |
string |
unset | Optional separate Bearer for the auto-emitted MCP entry. Falls back to the provider's apiKey (from auth.json) when unset |
fetchInterceptor |
boolean |
true |
Inject Authorization: Bearer + default Content-Type on every outbound request targeting baseURL (suffix-spoof guarded) |
Example — enrichment + compression tags + MCP auto-emit
{
"plugin": [
[
"@omniroute/opencode-plugin",
{
"providerId": "omniroute",
"baseURL": "https://or.example.com",
"features": {
"combos": true,
"enrichment": true,
"compressionMetadata": true,
"mcpAutoEmit": true,
},
},
],
],
}
With mcpAutoEmit: true, the plugin synthesises an mcp.omniroute entry equivalent to a manual:
"mcp": {
"omniroute": {
"type": "remote",
"url": "https://or.example.com/api/mcp/stream",
"enabled": true,
"headers": { "Authorization": "Bearer <apiKey-from-auth.json>" }
}
}
If you want a narrower-scoped Bearer for MCP (different from the chat/inference key), set features.mcpToken. Operator overrides win: if you already set mcp.omniroute in opencode.json, the plugin will not overwrite it.
Example — production-leaning defaults (clean picker, offline resilience)
{
"plugin": [
[
"@omniroute/opencode-plugin",
{
"providerId": "omniroute",
"baseURL": "https://or.example.com",
"features": {
"combos": true,
"enrichment": true,
"compressionMetadata": true,
"usableOnly": true,
"diskCache": true,
},
},
],
],
}
usableOnly: truedrops models whose canonical provider has no healthy connection in your OmniRoute instance — your/modelspicker stays focused on what you can actually call.diskCache: true(default) writes a snapshot to${OPENCODE_DATA_DIR}/plugins/omniroute-<providerId>.jsonon every healthy refresh. On a cold start where/v1/modelsis unreachable (laptop offline, IP whitelist drop), the snapshot hydrates the static block so OC still shows the catalog instead of a stub.compressionMetadata: trueannotates combo display names with their pipeline using traffic-light emoji for intensity (e.g.Combo: claude-primary [rtk🟡 → caveman🟠]) so the picker advertises which compression each combo applies and how heavy it is at a glance. Palette: 🟢 lite/minimal · 🟡 standard · 🟠 aggressive/full · 🔴 ultra. Unknown intensities fall through to raw text ([rtk:custom-thing]) so the plugin never hides a value OmniRoute knows but the plugin doesn't.providerTag: true(default) prepends a short upstream-provider label so the picker showsClaude - Claude Opus 4.7forcc/claude-opus-4-7,Kiro - Claude Opus 4.7forkr/claude-opus-4-7, andGHM - GPT 5forghm/gpt-5(slot.nameGitHub Models> 8 chars → abbreviated). Critical when the same model id is sold through multiple upstream connections with different cost/auth/rate-limit profiles. Set tofalseto keep the pre-v3.8.3 unsuffixed format.
Comparison vs @omniroute/opencode-provider
@omniroute/opencode-provider is the existing config-generator package — it writes a frozen provider.<id> block into opencode.json at build time. This plugin is the runtime integration.
@omniroute/opencode-plugin (this) |
@omniroute/opencode-provider |
|
|---|---|---|
| Type | OC plugin | Config generator (CLI/build-time) |
| Models | Live from /v1/models |
Frozen at scaffold |
| Combos | LCD-aggregated live | None |
| Gemini sanitize | Yes | N/A |
| OC UI integration | /connect, /models |
None |
| Multi-instance | Native | Manual |
Both can coexist; pick the one that fits your environment.
Requirements
- Node
>=22.22.3(perengines.node); tested on Node 22 and 24. - OpenCode: verified end-to-end against
opencode@1.15.5with@opencode-ai/plugin@1.15.6. - 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.