opencode v2 loads plugins through a contract the existing
@omniroute/opencode-plugin cannot satisfy: v1 exports plugin factories with an
auth/provider/config/tool hook object, v2 expects a default define({id, setup})
carrying catalog and integration domains. One package would have to satisfy
both loaders from a single entrypoint. An opencode v2 install therefore has no
route to an OmniRoute gateway at all: no model discovery, no combos, no
enrichment.
This adds @omniroute/opencode-plugin-v2, a self-contained package. The v1
plugin is untouched, so v1 users see no move, no migration and no breaking
version. The two packages deliberately share no code and no release: the
mapping logic here began as a port of v1's and now lives in this package, which
keeps either one free to change without a coordinated publish.
The plugin publishes models, combos and auto-combos into the host catalog,
refreshes them lazily behind a 300s TTL, and keeps serving the last known
catalog from an on-disk snapshot when the gateway is unreachable. Publishing is
staged: models and combos are what a catalog is, so they go out as soon as they
are known, while auto-combos, the provider list and the enrichment overlay fold
into the snapshot when they land. Gating the publish on all of them made the
catalog hostage to the slowest source — a gateway that accepts the connection
and never answers /api/combos/auto left everything unpublished until that fetch
timed out, which is longer than a short-lived host stays alive.
Display names carry what the gateway knows about a model: the upstream provider
it routes to, whether it is free, and the budget that comes with it. Those parts
were already fetched and then dropped, so two connections selling the same model
looked identical in the picker. The provider prefix can be turned off with
`providerTag: false`.
The on-disk snapshot carries that overlay too, under a size cap, so a cold start
opens on named models rather than raw ids. The host is asked to reload only when
the catalog or the overlay actually moved, never once per refresh window.
The gateway key comes from the host credential store when one is connected, so
connecting the integration from opencode is enough and no secret needs to sit
in opencode.json; a plugin option and an environment variable remain as
fallbacks, and a host too old to expose a credential store still loads. Nothing
is silent when a key is missing or refused: an absent key is named once at
startup with the three ways to supply one, and an enrichment source the gateway
rejects is reported per endpoint with what the catalog loses. Those three
failures used to be empty catch blocks, which turned a management token the
gateway refuses into a catalog of raw model ids with no explanation.
Tool calling to Gemini keeps working. Gemini answers 400 INVALID_ARGUMENT for
an entire request whose tool declarations carry $schema, $ref or
additionalProperties. The v1 plugin handled it by wrapping fetch and rewriting
the JSON body; v2 does it on the language model, where the tools are still
structured data, and only for Gemini models of this provider. It can be turned
off with geminiSanitization: false, and a host exposing no aisdk domain loads
without it.
The catalog contract itself is a moving target, so the plugin adapts to the
host instead of assuming one shape. The released CLI keeps the aisdk package,
the endpoint (as settings.baseURL), the request headers and the variant options
directly on the model and provider; the current SDK types keep the same
information inside an api block. Writing only the api block yields a catalog
the released CLI lists but cannot route. Rather than key off a version list
that goes stale on the next release, the plugin reads the shape the host seeds
into the catalog draft and publishes accordingly: a seed with a top-level
package and no api block gets both field sets, a seed with an api block gets
that block alone, and an undisclosed seed gets both. None of the legacy keys
collide with a key of the current types, so the two shapes coexist on one
object, variants included.
Four v1 behaviours are deliberately not carried over, because v2 either owns
them or no longer needs them: the plugin-side debug log (the host has its own
logging), the compression-metadata suffix on combo names, the MCP auto-emit
(the v2 host owns MCP), and the omni-sync command plus its background timer
(the TTL and a content fingerprint drive catalog.reload instead).
A refresh never downgrades what is already published: the previous overlay is
carried forward until the new one lands, so names, pricing and the usable
filter no longer drop out for the length of every TTL window. The disk snapshot
is read after the credential is resolved, because it is keyed by that
credential — reading it earlier looked up the identity the options carry rather
than the one in use, and rejected a perfectly good catalog exactly when the
gateway was down.
The tool-schema cleaner now knows where a schema ends and a property name
begins. Stripping keywords by name anywhere in the tree deleted a tool
parameter called `ref` while leaving it in `required`, handing the model a
schema it could not satisfy; a `$ref` it cannot resolve now forwards the tool
untouched instead of widening it to accept anything. Gemini detection is
anchored on the model family, so `gemini-compatible-proxy` is no longer treated
as a Gemini model.
A source the gateway refuses is reported on the library entry point as well,
not only through the plugin, so the usable-provider filter can no longer disable
itself in silence. `providerId` is bounded to a safe character set because it
reaches a filesystem path, `hiddenModels` covers combos as it already covered
models, the Anthropic block gets the gateway root rather than a doubled `/v1`, an unparseable tool schema forwards the tool instead
of failing the request, and the package typechecks under the same settings as
the v1 plugin.
CI mirrors the existing plugin workflow: install, build and test on Node 22 and
24, for both packages. The plugin SDK stays pinned, and the host-shape assertions carry the risk of
a contract move rather than a check against a rolling upstream tag.
Co-authored-by: Max <maxmad64@gmail.com>
24 KiB
title, version, lastUpdated
| title | version | lastUpdated |
|---|---|---|
| CLI Integrations — point any coding CLI at OmniRoute | 3.8.50 | 2026-08-18 |
CLI Integrations
OmniRoute ships a family of setup-* commands that configure a coding
CLI (Codex, Claude Code, OpenCode, Cline, …) to use OmniRoute as its backend — so
the tool talks to one endpoint and OmniRoute routes to the right provider with
auto-fallback. Each command reads the live model catalog from a running
OmniRoute (local or remote) and writes the tool's own config file on your
machine. The API key is referenced by an environment variable wherever the tool
supports it. Commands that persist a tool-local environment file are noted below.
There is also a generic launcher — omniroute run <target> — that spawns
claude, codex, aider, goose, opencode, qwen or gemini with the
right env injected, without writing any config at all. Targets and their
aliases come from the canonical manifest bin/cli/cli-manifest.mjs
(claude-code|cc|anthropic, codex-cli|openai-codex|openai, goose-cli,
open-code, qwen-code, gemini-cli), and omniroute completion offers the
same manifest-derived target words. The legacy per-tool launchers —
omniroute launch (Claude Code) and omniroute launch-codex (Codex) — remain
available.
Provider onboarding is available from the same local/remote context. The API-first commands below keep management authentication separate from provider credentials and never print a credential in structured output:
omniroute providers add glm --credential-env GLM_API_KEY --name work
omniroute providers import ./providers.json --dry-run --json
omniroute providers auth openai
omniroute providers edit <connection-id> --default-model glm/glm-5.2
omniroute providers remove <connection-id> --yes
For scripts, prefer --credential-stdin or --credential-env; --credential
is retained for controlled local use. providers remove requires --yes on a
non-interactive terminal, and all five commands honor the active context or the
global --base-url/--api-key options.
For the one-time, hand-written base setup of the two richest integrations, see the per-tool deep dives:
- Claude Code configuration
- Codex CLI configuration
- Remote Mode — drive a remote OmniRoute (VPS / Tailnet) from your laptop
- VS Code Copilot Chat — the OmniCopilot extension; it can also run these
setup-*commands for you from inside the editor
Master table
Every command honours the active context (set with omniroute connect, see
Remote Mode) or explicit --remote <url> --api-key <key> flags.
"Local vs remote" below means: with no flags it targets http://localhost:20128;
with --remote (or an active remote context) it fetches the catalog from that
server and writes the config locally.
| Command | Tool | What it writes | Key flags | Local vs remote |
|---|---|---|---|---|
omniroute setup-codex |
OpenAI Codex CLI | ~/.codex/<name>.config.toml — one profile per compatible text model (codex --profile <name>) |
--remote --api-key --only --dry-run --port --codex-home |
Both |
omniroute setup-claude |
Claude Code | ~/.claude/profiles/<name>/settings.json — one profile per matched model (CLAUDE_CONFIG_DIR) |
--remote --api-key --only --dry-run --port --claude-home |
Both |
omniroute setup-opencode |
OpenCode (openai-compatible) | ~/.config/opencode/opencode.json — omniroute provider with every catalog model (opencode -m omniroute/<model>) |
--remote --api-key --only --model --dry-run --port |
Both |
omniroute setup-cline |
Cline | ~/.cline/data/{globalState,secrets}.json (CLI mode) + prints VS Code extension settings |
--remote --api-key --model --yes --dry-run --port --cline-dir |
Both |
omniroute setup-kilo |
Kilo Code | ~/.local/share/kilo/auth.json (CLI) + merges kilocode.* into VS Code settings.json if present |
--remote --api-key --model --yes --dry-run --port --auth-path --vscode-settings |
Both |
omniroute setup-continue |
Continue / cn CLI |
~/.continue/config.yaml — provider: openai models, key via ${{ secrets.OMNIROUTE_API_KEY }} |
--remote --api-key --only --dry-run --port --config-path |
Both |
omniroute setup-cursor |
Cursor | Nothing — prints the in-app steps (Cursor config is opaque SQLite) | --remote --api-key --only --port |
Both |
omniroute setup-roo |
Roo Code | ~/.omniroute/roo-settings.json (import doc) + sets roo-cline.autoImportSettingsPath if a VS Code settings.json exists |
--remote --api-key --model --yes --dry-run --port --import-path --vscode-settings |
Both |
omniroute setup-crush |
Crush | ~/.config/crush/crush.json — openai-compat provider, key via $OMNIROUTE_API_KEY |
--remote --api-key --only --dry-run --port --config-path |
Both |
omniroute setup-goose |
Goose | ~/.config/goose/config.yaml (GOOSE_PROVIDER/OPENAI_HOST/GOOSE_MODEL) + prints env recipe |
--remote --api-key --model --yes --dry-run --port --config-path |
Both |
omniroute setup-aider |
Aider | ~/.aider.conf.yml (openai-api-base + model: openai/<id>) + prints env recipe |
--remote --api-key --model --yes --dry-run --port --config-path |
Both |
omniroute setup-qwen |
Qwen Code | ~/.qwen/settings.json — V4 modelProviders.openai array + OMNIROUTE_API_KEY in ~/.qwen/.env |
--remote --api-key --model --yes --dry-run --port --config-path --env-path |
Both |
omniroute setup-5dive |
5dive (agent fleet) | Nothing under $HOME — writes a 5dive auth profile (/var/lib/5dive/auth-profiles/<name>/) via 5dive agent auth set; root-only, runs on the fleet host |
--remote --api-key --model --auth-profile --agent --byo-provider --fivedive-bin --no-sudo --yes --dry-run --port |
Both |
omniroute run <target> |
Runtime launch (generic) | Nothing — spawn claude/codex/aider/goose/opencode/qwen/gemini with the right env and args; Qwen and Gemini use a temporary isolated home |
--remote --base-url --context --provider --model --api-key --api-key-env --dry-run --json --port --profile --token |
Both |
omniroute launch |
Claude Code | Nothing — spawns claude with ANTHROPIC_BASE_URL/ANTHROPIC_AUTH_TOKEN injected |
--remote --api-key --token --profile --port |
Both |
omniroute launch-codex |
OpenAI Codex CLI | Nothing — spawns codex with the omniroute provider injected via -c flags |
--remote --api-key --profile (-p) --port |
Both |
Notes on flags (verified in the command source):
--remote <url>— fetch the catalog from a remote OmniRoute (overrides--portand the active context).--api-key <key>supplies the credential for that server (defaults to theOMNIROUTE_API_KEYenv var, or the active context's token).--only <patterns>— comma-separated substrings; keep only model IDs that match (e.g.--only glm,kimi). Available onsetup-codex,setup-claude,setup-opencode,setup-continue,setup-cursor,setup-crush.--dry-run— print exactly what would be written without touching the filesystem. Available on everysetup-*command exceptsetup-cursor(which never writes a file).--model <id>— required (or picked interactively) for the tools that have no model auto-discovery: Cline, Kilo, Roo, Goose, Qwen, Aider, 5dive. Those tools also accept--yesfor non-interactive runs (which then requires--model).setup-opencodetakes--modelto set the default top-level model.--model <id>onomniroute runfollows the manifest's per-target wiring (bin/cli/cli-manifest.mjs): aider receives--model openai/<id>and opencode--model omniroute/<id>(the prefix is added only when the id does not already carry it); qwen and gemini receive the id verbatim; claude gets it viaANTHROPIC_MODEL, goose viaGOOSE_MODEL, and codex via-c model_providers.omniroute.*args. Qwen is the only run target that hard-requires--model—omniroute run qwenwithout it exits2with an explicit error.--port <port>— local OmniRoute port (default20128, ignored when--remoteis set). Present on allsetup-*and both launchers.omniroute runexit codes: the child CLI's own exit code is propagated verbatim;2= invalid arguments (unsupported target, missing required--model, container guard);127= the target binary is not inPATH;130/143/129when the launch is ended bySIGINT/SIGTERM/SIGHUP;1= other runtime launch failure.- The two launchers (
launch,launch-codex) accept--profile <name>to select a profile written bysetup-claude/setup-codex, plus pass-through args for the underlyingclaude/codexbinary.
The interactive picker is also shared by the setup recipes:
# Pick from the active local or remote model catalog and configure the target.
omniroute configure claude
omniroute configure opencode --provider glm
omniroute configure qwen --model qwen/qwen3.8-max-preview --yes
configure currently delegates to the tested recipes for codex, claude,
opencode, qwen, aider, goose, cline, continue, kilo, and 5dive.
IDE-only,
MITM, and guide-only catalog entries remain explicit setup-*/manual flows and
are not presented as launchable targets.
setup-opencodeis the lightweight openai-compatible OpenCode integration. There is also a richer plugin integration —omniroute setup opencode— which installs@omniroute/opencode-plugin. They are different commands; the table above documentssetup-opencode.The plugin comes in two packages, one per OpenCode major, because the two loaders expect different entrypoints:
@omniroute/opencode-pluginfor OpenCode v1 and@omniroute/opencode-plugin-v2for OpenCode v2. The v2 package is new (0.1.0) and follows a host contract that is still moving, so it reads the shape OpenCode seeds into the catalog draft rather than assuming one. Install it by adding apluginsentry toopencode.json;omniroute setup opencodestill installs the v1 package. Options and the credential lookup order are in the package README.
Local usage
With OmniRoute running on localhost:20128, just run the setup command for your
tool. The catalog is fetched from the local server.
# Codex: write a profile per matched model into ~/.codex/
omniroute setup-codex
codex --profile glm52 # use a generated profile
# Claude Code: write per-model profiles, then launch one
omniroute setup-claude
omniroute launch --profile glm52
# OpenCode: write the openai-compatible provider with all catalog models
omniroute setup-opencode
export OMNIROUTE_API_KEY=sk-... # referenced via {env:OMNIROUTE_API_KEY}, never on disk
opencode -m omniroute/glm/glm-5.2 "..."
# Tools without auto-discovery need an explicit model:
omniroute setup-aider --model glm/glm-5.2
omniroute setup-qwen --model qwen/qwen3.8-max-preview
# Preview without writing anything:
omniroute setup-continue --dry-run
Launch without writing any config at all (env-injection only):
omniroute launch # Claude Code → local OmniRoute
omniroute launch-codex # Codex CLI → local OmniRoute
omniroute launch-codex --profile glm52
omniroute run claude --model openai/gpt-5.4
omniroute run codex --model openai/gpt-5.4 --dry-run --json
omniroute run aider --model glm/glm-5.2 -- --message "reply OK"
omniroute run goose --model glm/glm-5.2
omniroute run opencode --model glm/glm-5.2 -- run "reply OK"
omniroute run qwen --model glm/glm-5.2 -- -p "reply OK"
omniroute run gemini --model glm/glm-5.2 -- --skip-trust -p "reply OK"
# Explicit command path: pass through whatever comes after --
omniroute run claude -- --print-system-prompt "review this diff"
Remote usage
Point any setup command at a remote OmniRoute with --remote + --api-key. The
catalog is fetched from the remote; the config is written on your local machine.
# OpenCode against a remote VPS, keep only glm/kimi models
omniroute setup-opencode --remote http://192.168.0.15:20128 --api-key oma_live_xxx \
--only glm,kimi
opencode -m omniroute/glm/glm-5.2 "..." # export OMNIROUTE_API_KEY first
# Codex profiles from a remote catalog
omniroute setup-codex --remote http://192.168.0.15:20128 --api-key oma_live_xxx
# Launch a CLI straight against the remote
omniroute launch --remote http://192.168.0.15:20128 --api-key oma_live_xxx
omniroute launch-codex --remote http://192.168.0.15:20128 --api-key oma_live_xxx
Instead of passing --remote/--api-key every time, log in once and let the
active context supply them automatically:
omniroute connect 192.168.0.15 # mints a scoped token, stores the context
omniroute setup-codex # ← now uses the remote catalog
omniroute setup-opencode # ← same
omniroute launch # ← Claude Code against the remote
See Remote Mode for contexts, scopes, and token management.
5dive agent fleets
5dive runs a fleet of long-running coding agents, each one a
systemd unit under its own Unix user. It is not a coding CLI itself, so there is
nothing for omniroute run to launch — 5dive is a configure-only target.
omniroute configure 5dive --model failover-demo --yes
omniroute setup-5dive --model failover-demo --auth-profile omniroute --agent worker1
Both forms write one 5dive auth profile, and every claude seat bound to that
profile then talks to OmniRoute. Three things are specific to this target:
- It runs on the fleet host, as root. 5dive's verbs act on local systemd units
and a root-owned state dir; there is no remote mode. The recipe re-execs through
sudowhen it is not already root (--no-sudoturns that off and prints the command instead). - The endpoint must be
https://unless it is loopback. The agent's API key rides that URL on every request, and 5dive refuses a plaintext off-box endpoint. A private LAN address is not an exception. - Each seat's own model pin outranks the profile. The profile carries
ANTHROPIC_DEFAULT_{OPUS,SONNET,HAIKU}_MODEL, but a seat still pinned to a stock model id fails its first turn with "There's an issue with the selected model". Pass--agent <name>(repeatable) to pin the seats as well; the recipe prints the command when you do not.
The API key is handed to 5dive on stdin (--api-key=-), so it never appears in
ps output.
Pointing the profile at an OmniRoute combo rather than a single model is what gets the fleet provider failover: when the primary endpoint went hard-down mid-turn in the run recorded on #11578, the agent finished its remaining steps on the fallback and never surfaced the outage.
Base URL conventions (which tools want /v1)
OmniRoute exposes the OpenAI surface at /v1, the Anthropic surface at the root,
and a native Gemini surface at /v1beta. Each integration is wired to the form its
tool expects (verified in the command source):
| Integration | Base URL written | /v1? |
|---|---|---|
setup-cline (openAiBaseUrl) |
root | No — Cline appends /v1/chat/completions |
setup-goose (OPENAI_HOST) |
root | No — Goose appends the path |
setup-aider (OPENAI_API_BASE) |
root | No — LiteLLM appends /v1/chat/completions |
setup-kilo, setup-roo, setup-continue, setup-crush, setup-cursor |
with /v1 |
Yes |
setup-claude (ANTHROPIC_BASE_URL), launch |
root | No — Claude Code appends /v1/messages |
setup-codex, launch-codex (model_providers.omniroute.base_url) |
with /v1 |
Yes |
setup-qwen (modelProviders.openai[].baseUrl) |
with /v1 |
Yes |
run gemini (GOOGLE_GEMINI_BASE_URL) |
root | No — the SDK appends /v1beta/models/… |
setup-5dive (ANTHROPIC_BASE_URL in the auth profile) |
root | No — Claude Code appends /v1/messages |
Keeping native deps on update: --include=optional
When you update with omniroute update (after confirming, or with --apply),
OmniRoute runs the install with --include=optional baked in:
npm install -g omniroute@latest --include=optional
This is not a flag you pass to omniroute update — it is always applied by the
updater. It guarantees the optionalDependencies (better-sqlite3, keytar,
tls-client, the LLMLingua SLM stack) survive the update even if your npm config
has omit=optional set, which would otherwise silently drop the native SQLite
driver and OS-keyring binding. To preview the exact command without applying:
omniroute update --dry-run
# [DRY RUN] Would run: npm install -g omniroute@latest --include=optional
Other omniroute update flags (verified in source): --check (exit 1 if
outdated), --apply (install without prompting), --changelog, --no-backup,
--yes.
Google Gemini CLI via omniroute run gemini
Contract verified against @google/gemini-cli 0.50.0: the CLI honors
GOOGLE_GEMINI_BASE_URL and issues POST /v1beta/models/<model>:generateContent
(and :streamGenerateContent?alt=sse) against it — exactly OmniRoute's native
Gemini surface (/v1beta). omniroute run gemini wires that automatically:
GOOGLE_GEMINI_BASE_URL→ the active OmniRoute base URL (root, no/v1);GEMINI_API_KEY→ the resolved OmniRoute credential (option/env/context);- a temporary isolated
GEMINI_CLI_HOMEwhose.gemini/settings.jsonselectsgemini-api-keyauth, so a stored Google OAuth session (Code Assist) never overrides the OmniRoute-directed launch — removed after exit; - env hygiene: the child env is scrubbed of
GOOGLE_API_KEY,GOOGLE_GENAI_USE_VERTEXAIandGOOGLE_GENAI_USE_GCA(which would redirect auth to Vertex/Code Assist), andGEMINI_DEFAULT_AUTH_TYPE=gemini-api-keyis set as a belt-and-suspenders fallback — the otherruntargets get the same treatment for their own conflicting variables; --model <id>injection from--provider/--model.
omniroute run gemini --model glm/glm-5.2 -- --skip-trust -p "hello"
Gemini's workspace-trust guard still applies in headless mode — pass
--skip-trust (or trust the directory interactively) yourself; the launcher
deliberately does not bypass it. This launcher is distinct from the ACP
registration (src/lib/acp/registry.ts, gemini --acp), which remains the
agent-protocol integration for /dashboard/acp-agents.
Real smoke sweep (opt-in)
Deterministic launch-plan regression runs in CI (tests/unit/cli/run-command.test.ts,
tests/unit/cli/run-execution.test.ts). To validate the REAL binaries against a REAL
OmniRoute server, an opt-in harness exists at
tests/integration/upstream-cli-smoke.int.test.ts. It never runs automatically
(every sub-test skips unless RUN_CLI_SMOKE=1), passes the credential by env-var
NAME (never by value), redacts key-shaped strings from any recorded output, skips
targets whose binary is not installed, and classifies failures as
auth / upstream / config instead of a bare boolean:
RUN_CLI_SMOKE=1 \
OMNIROUTE_SMOKE_BASE_URL="http://localhost:20128" \
OMNIROUTE_SMOKE_MODEL="<provider/model>" \
OMNIROUTE_SMOKE_API_KEY_ENV="OMNIROUTE_API_KEY" \
node --import tsx/esm --test tests/integration/upstream-cli-smoke.int.test.ts
Optional: OMNIROUTE_SMOKE_TARGETS="codex,opencode,qwen" restricts the sweep;
OMNIROUTE_SMOKE_TIMEOUT_MS overrides the 120s per-target timeout.
See also
- Claude Code configuration — the deeper Claude Code guide
- Codex CLI configuration — the one-time
[model_providers.omniroute]base setup - Remote Mode — contexts, scoped access tokens, driving a remote server
- CLI Tools reference — the full catalog of supported tools + dashboard pages
- Setup Guide — install methods and first-run onboarding