chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:12 +08:00
commit d8dcd5f6d1
8604 changed files with 2479390 additions and 0 deletions
+152
View File
@@ -0,0 +1,152 @@
---
title: "Claude Code CLI — Configuration with OmniRoute"
version: 3.8.40
lastUpdated: 2026-06-28
---
# Claude Code CLI — Configuration with OmniRoute
Point the **Claude Code** CLI (`claude`) at OmniRoute — local or a remote VPS —
with per-model profiles, mirroring the Codex setup.
---
## Quick start
```bash
# Launch Claude Code against a local OmniRoute (auto-detects the active context)
omniroute launch
# Against a remote OmniRoute (after `omniroute connect <host>`, this is automatic)
omniroute launch --remote http://192.168.0.15:20128 --api-key oma_live_xxx
# Generate per-model profiles, then launch one
omniroute setup-claude # writes ~/.claude/profiles/<name>/settings.json
omniroute launch --profile glm52 # Claude Code using glm/glm-5.2 via OmniRoute
```
---
## How Claude Code connects to a gateway
Claude Code talks the **Anthropic Messages API** and is pointed at a custom
endpoint with environment variables (it has no `--base-url` flag):
| Variable | Purpose |
| -------------------------------------------- | -------------------------------------------------------------------------------------- |
| `ANTHROPIC_BASE_URL` | Gateway root URL (Claude Code appends `/v1/messages`). **No `/v1` suffix.** |
| `ANTHROPIC_AUTH_TOKEN` | Sent as `Authorization: Bearer …` — use your OmniRoute access token / API key |
| `ANTHROPIC_API_KEY` | Alternative: sent as `x-api-key`. If both set, `ANTHROPIC_AUTH_TOKEN` wins |
| `ANTHROPIC_MODEL` | Force a specific model (overrides the `/model` picker default) |
| `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY` | `1` → the native `/model` picker lists `claude*`/`anthropic*` models from `/v1/models` |
| `CLAUDE_CODE_MAX_OUTPUT_TOKENS` | Cap output tokens per response (e.g. `65536`) |
| `CLAUDE_CODE_AUTO_COMPACT_WINDOW` | Token threshold for auto-compaction |
> Env vars are read **once at startup** — restart Claude Code after changing them.
`omniroute launch` sets all of these for you: it resolves the base URL + token
from the active context (so `omniroute connect <vps>` then `omniroute launch`
just works), health-checks the server, and execs `claude`.
---
## Profiles (`CLAUDE_CONFIG_DIR`)
Claude Code has **no native profile files** (unlike Codex's `~/.codex/<name>.config.toml`).
The idiomatic mechanism is `CLAUDE_CONFIG_DIR` — a separate config directory per
profile, each with its own `settings.json`, credentials, history and cache.
`omniroute setup-claude` fetches the live `/v1/models` catalog and writes one
profile per model at `~/.claude/profiles/<name>/settings.json`, reusing the
**same names as `setup-codex`** (`glm52`, `kimi-k27`, `deepseek-pro`, …):
```jsonc
// ~/.claude/profiles/glm52/settings.json
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"model": "glm/glm-5.2",
"effortLevel": "xhigh",
"env": {
"ANTHROPIC_BASE_URL": "http://192.168.0.15:20128",
"ANTHROPIC_MODEL": "glm/glm-5.2",
"CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY": "1",
"CLAUDE_CODE_AUTO_COMPACT_WINDOW": "190000",
},
}
```
> **The auth token is never written to the profile.** Launch with
> `omniroute launch --profile <name>` (it injects `ANTHROPIC_AUTH_TOKEN` from the
> active context), or export `ANTHROPIC_AUTH_TOKEN` yourself and run
> `CLAUDE_CONFIG_DIR=~/.claude/profiles/<name> claude`.
**Auto-sync after model discovery (opt-in).** OmniRoute can regenerate these same
`~/.claude/profiles/<name>/settings.json` files automatically whenever a provider model
sync changes the live catalog — so new/renamed models get profiles without re-running the
command. It is **off by default**: toggle it from the **CLI Code dashboard** ("CLI profile
auto-sync" → Claude Code), or set `OMNIROUTE_AUTO_SYNC_CLAUDE_PROFILES=true` (it also honors
`CLI_ALLOW_CONFIG_WRITES`, on by default). When enabled it only writes profile files; it never
changes your active/default Claude config, auth, or the `~/.claude/settings.json`.
### Generating + using profiles
```bash
# Local OmniRoute
omniroute setup-claude
# Remote VPS (bakes the VPS URL into every profile)
omniroute setup-claude --remote http://192.168.0.15:20128 --api-key oma_live_xxx
# Only some providers
omniroute setup-claude --only glm,kimi
# Preview without writing
omniroute setup-claude --dry-run
# Launch a profile
omniroute launch --profile kimi-k27
```
---
## Model tiers (optional)
Claude Code routes to capability tiers. Map each to an OmniRoute model via env /
settings if you want different providers per tier:
```bash
export ANTHROPIC_DEFAULT_OPUS_MODEL="glm/glm-5.2"
export ANTHROPIC_DEFAULT_SONNET_MODEL="kmc/kimi-k2.6"
export ANTHROPIC_DEFAULT_HAIKU_MODEL="glm/glm-4.7-flash"
```
Otherwise a single `ANTHROPIC_MODEL` (what profiles set) is used for everything.
---
## Remote mode
Once you've run `omniroute connect <host>` (see
[Remote Mode](./REMOTE-MODE.md)), `omniroute launch` and `omniroute setup-claude`
automatically target that remote server and use its scoped access token — no
extra flags needed. Override per-invocation with `--remote` / `--api-key`.
---
## Troubleshooting
**Claude Code ignores the gateway** — confirm `ANTHROPIC_BASE_URL` has **no
`/v1`** and restart `claude` (env is read once at startup). `omniroute launch`
handles this for you.
**`/model` picker is empty / missing gateway models** — needs Claude Code
v2.1.129+ and `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1`. Only `claude*` /
`anthropic*` model IDs appear in the picker; force any other model with
`ANTHROPIC_MODEL=<id>` (this is what profiles do).
**Auth errors** — the profile holds no token. Use `omniroute launch --profile`
(injects it) or export `ANTHROPIC_AUTH_TOKEN`.
**Profiles don't isolate** — each profile is a distinct `CLAUDE_CONFIG_DIR`;
verify `echo $CLAUDE_CONFIG_DIR` inside the session points at
`~/.claude/profiles/<name>`.
+202
View File
@@ -0,0 +1,202 @@
---
title: "CLI Integrations — point any coding CLI at OmniRoute"
version: 3.8.40
lastUpdated: 2026-06-28
---
# 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 env var wherever the tool supports it, so the
secret is never written to disk (the exceptions are noted below).
There are also two launchers — `omniroute launch` (Claude Code) and
`omniroute launch-codex` (Codex) — that spawn the CLI with the right env injected,
without writing any config at all.
For the one-time, hand-written base setup of the two richest integrations, see the
per-tool deep dives:
- [Claude Code configuration](./CLAUDE-CODE-CONFIGURATION.md)
- [Codex CLI configuration](./CODEX-CLI-CONFIGURATION.md)
- [Remote Mode](./REMOTE-MODE.md) — drive a remote OmniRoute (VPS / Tailnet) from your laptop
---
## Master table
Every command honours the **active context** (set with `omniroute connect`, see
[Remote Mode](./REMOTE-MODE.md)) 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-qwen` | Qwen Code | `~/.qwen/settings.json` — openai `modelProvider`, key via `envKey` (`OMNIROUTE_API_KEY`) | `--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 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 `--port`
and the active context). `--api-key <key>` supplies the credential for that
server (defaults to the `OMNIROUTE_API_KEY` env var, or the active context's token).
- `--only <patterns>` — comma-separated substrings; keep only model IDs that match
(e.g. `--only glm,kimi`). Available on `setup-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 every `setup-*` command **except** `setup-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. Those tools
also accept `--yes` for non-interactive runs (which then requires `--model`).
`setup-opencode` takes `--model` to set the default top-level model.
- `--port <port>` — local OmniRoute port (default `20128`, ignored when `--remote`
is set). Present on all `setup-*` and both launchers.
- The two launchers (`launch`, `launch-codex`) accept `--profile <name>` to select
a profile written by `setup-claude` / `setup-codex`, plus pass-through args for
the underlying `claude` / `codex` binary.
> `setup-opencode` is 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 documents `setup-opencode`.
---
## Local usage
With OmniRoute running on `localhost:20128`, just run the setup command for your
tool. The catalog is fetched from the local server.
```bash
# 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 kmc/kimi-k2.7
# Preview without writing anything:
omniroute setup-continue --dry-run
```
Launch without writing any config at all (env-injection only):
```bash
omniroute launch # Claude Code → local OmniRoute
omniroute launch-codex # Codex CLI → local OmniRoute
omniroute launch-codex --profile glm52
```
---
## 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.
```bash
# 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:
```bash
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](./REMOTE-MODE.md) for contexts, scopes, and token management.
---
## 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-qwen`, `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 |
---
## 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:
```bash
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:
```bash
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`.
---
## See also
- [Claude Code configuration](./CLAUDE-CODE-CONFIGURATION.md) — the deeper Claude Code guide
- [Codex CLI configuration](./CODEX-CLI-CONFIGURATION.md) — the one-time `[model_providers.omniroute]` base setup
- [Remote Mode](./REMOTE-MODE.md) — contexts, scoped access tokens, driving a remote server
- [CLI Tools reference](../reference/CLI-TOOLS.md) — the full catalog of supported tools + dashboard pages
- [Setup Guide](./SETUP_GUIDE.md) — install methods and first-run onboarding
+413
View File
@@ -0,0 +1,413 @@
---
title: "Codex CLI — Configuration with OmniRoute"
version: 3.8.40
lastUpdated: 2026-06-28
---
# Codex CLI — Configuration with OmniRoute
Complete guide for using the Codex CLI pointed at OmniRoute as an OpenAI-compatible backend.
---
## Ready-to-paste config.toml
Replace `<YOUR_HOST>` and `<YOUR_KEY>` with your values:
```toml
# ~/.codex/config.toml
model = "cx/gpt-5.5"
model_provider = "omniroute"
model_reasoning_effort = "xhigh"
model_context_window = 400000
model_auto_compact_token_limit = 350000
tool_output_token_limit = 32768 # history storage cap per tool call
[model_providers.omniroute]
name = "OmniRoute"
base_url = "http://<YOUR_HOST>:20128/v1"
env_key = "OMNIROUTE_API_KEY"
requires_openai_auth = false
wire_api = "responses"
```
```bash
# ~/.bashrc or ~/.zshrc — actual key value, never in config.toml
export OMNIROUTE_API_KEY="<YOUR_KEY>"
```
> **Common host options**
>
> | Access | URL |
> | ------------- | ----------------------------- |
> | Local network | `http://192.168.0.1:20128/v1` |
> | Tailscale | `http://100.x.x.x:20128/v1` |
> | Loopback | `http://localhost:20128/v1` |
---
## `wire_api = "responses"` — why it works for all models
Codex CLI deprecated `wire_api = "chat"` (Chat Completions) in February 2026 and now **requires** `wire_api = "responses"` (OpenAI Responses API). Setting `wire_api = "chat"` causes an immediate startup crash since v0.138.
DeepSeek, GLM, Kimi and others only expose a Chat Completions endpoint — not the Responses API. If you pointed Codex directly at them, it would fail.
**OmniRoute solves this transparently:**
```
Codex CLI
→ wire_api = "responses"
→ POST /v1/responses (OmniRoute)
→ OmniRoute Responses ↔ Chat Completions transformer
→ POST /chat/completions (DeepSeek / Mistral / GLM / Kimi / any provider)
```
You never need a separate translation proxy when using OmniRoute. **All models use `wire_api = "responses"`** — OmniRoute handles the rest.
> **`wire_api` is the default** — the field defaults to `"responses"` and can be omitted entirely from `config.toml`. Only ever set it explicitly if you're documenting intent.
---
## Context window and compaction
### Token configuration fields
| Field | Description |
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `model_context_window` | Total token budget for the active model. Set to the model's advertised limit. |
| `model_auto_compact_token_limit` | Threshold that triggers automatic history compaction. **Maximum: 90% of `model_context_window`** — values above 90% are silently ignored. |
| `tool_output_token_limit` | Cap on tokens stored per tool call output in history. Prevents a single large tool response from filling the window. **This is not the max output** — it is a history storage cap. |
| `compact_prompt` | Inline override for the system prompt used during compaction (v0.138+). |
> **Note on `model_max_output_tokens`**: This field is **not part of the Codex CLI config schema** (absent from the Codex Rust codebase). It is silently ignored if set. Do not rely on it — use `tool_output_token_limit` to control how much tool output is stored in history.
### Context windows by model
| Model | OmniRoute ID | Context window | `auto_compact` | `tool_output_limit` |
| ------------------------------------ | ------------------------------------ | ---------------------- | -------------- | ------------------- |
| GPT-5.5 | `cx/gpt-5.5` | 400k reliable (1M max) | 350,000 | 32,768 |
| Kimi K2.7 (thinking) | `kmc/kimi-k2.7` | 131,072 | 112,000 | 32,768 |
| Kimi K2.6 | `kmc/kimi-k2.6` | 131,072 | 112,000 | 32,768 |
| GLM-5.2 / 5.2-max (thinking) | `glm/glm-5.2` | 131,072 | 112,000 | 32,768 |
| MiMo V2.5 Pro (thinking) | `opencode-go/mimo-v2.5-pro` | 131,072 | 112,000 | 32,768 |
| Qwen 3.7 Plus (thinking) | `opencode-go/qwen3.7-plus` | 32,768 | 28,000 | 16,384 |
| DeepSeek V4 Pro (OllamaCloud) | `ollamacloud/deepseek-v4-pro` | 131,072 | 112,000 | 32,768 |
| DeepSeek V4 Pro | `ds/deepseek-v4-pro` | 1,000,000 | 900,000 | 65,536 |
| MiMo V2.5 | `opencode-go/mimo-v2.5` | 131,072 | 112,000 | 32,768 |
| Gemma 4 31B (OllamaCloud) | `ollamacloud/gemma4:31b` | 32,768 | 28,000 | 16,384 |
| Nemotron 3 Super (OllamaCloud) | `ollamacloud/nemotron-3-super` | 32,768 | 28,000 | 16,384 |
| GPT-OSS 20B (OllamaCloud) | `ollamacloud/gpt-oss:20b` | 32,768 | 28,000 | 16,384 |
| DeepSeek V4 Flash (OllamaCloud) | `ollamacloud/deepseek-v4-flash` | 65,536 | 56,000 | 16,384 |
| Gemini 3 Flash Preview (OllamaCloud) | `ollamacloud/gemini-3-flash-preview` | 1,000,000 | 850,000 | 32,768 |
| GLM-5 Turbo | `glm/glm-5-turbo` | 131,072 | 112,000 | 16,384 |
| GLM-4.7 Flash | `glm/glm-4.7-flash` | 131,072 | 112,000 | 16,384 |
| Mistral Large Latest | `mistral/mistral-large-latest` | 262,144 | 220,000 | 16,384 |
> **Compaction formula:** `effective_window = model_context_window - min(tool_output_token_limit, 20000)`. Values above 20k do not change the compaction trigger.
> **Rule of thumb:** set `model_auto_compact_token_limit` to 8588% of `model_context_window`. Never go above 90% — silently ignored.
---
## Model prefix: `cx/`
All Codex models in OmniRoute use the `cx/` prefix:
| Codex CLI name | OmniRoute model |
| ----------------------- | ------------------ |
| `cx/gpt-5.5` | GPT-5.5 standard |
| `cx/gpt-5.4` | GPT-5.4 standard |
| `cx/gpt-5.4-mini` | GPT-5.4 mini |
| `cx/gpt-5.1-codex-mini` | GPT-5.1 Codex mini |
Other providers use their own prefix (`kmc/`, `glm/`, `ds/`, `ollamacloud/`, `opencode-go/`, `mistral/`) — the prefix matches the OmniRoute provider alias.
---
## Reasoning Effort
Controls how much the model "thinks" before responding.
| Value | Use for |
| -------- | --------------------------------------------- |
| `none` | No reasoning — direct response |
| `low` | Trivial tasks (rename, format) |
| `medium` | **Server default** when not specified |
| `high` | Intermediate tasks (refactoring, debug) |
| `xhigh` | Architecture, deep analysis, complex problems |
```bash
# Per invocation override
codex -c model_reasoning_effort=low "rename variable x to count"
codex -c model_reasoning_effort=xhigh "design the auth module"
```
---
## Profiles — named configurations per model/workflow
Profiles let you switch model + context window with a single flag. Each profile is a flat
`~/.codex/<name>.config.toml` that overlays on top of the base `config.toml`.
> **Naming rule (Codex CLI v0.137+):** file must be `~/.codex/<name>.config.toml` — **no `profile-` prefix**.
> The CLI resolves `-p kimi-k27` → `~/.codex/kimi-k27.config.toml`. If the file is not found, the default applies silently.
```bash
codex --profile kimi-k27 "analyze 10k lines of this codebase"
codex -p glm52 "architecture review"
codex --profile deepseek-flash "rename variable" # fast, cheap
```
### Effort profiles (same model, different effort)
```bash
codex -p low # cx/gpt-5.5, effort=low
codex -p medium # cx/gpt-5.5, effort=medium
codex -p high # cx/gpt-5.5, effort=high
codex -p xhigh # cx/gpt-5.5, effort=xhigh (default)
codex -p chat # cx/gpt-5.5, no effort set (server default)
```
### Thinking models (alto pensamento) — xhigh + detailed summary
| Profile | Model | Context | Use for |
| ------------ | --------------------------- | ------- | ---------------------------- |
| `kimi-k27` | `kmc/kimi-k2.7` | 128k | Best thinking quality (Kimi) |
| `glm52` | `glm/glm-5.2` | 128k | GLM thinking |
| `glm52max` | `glm/glm-5.2-max` | 128k | GLM thinking max |
| `mimo-pro` | `opencode-go/mimo-v2.5-pro` | 128k | MiMo thinking |
| `qwen37plus` | `opencode-go/qwen3.7-plus` | 32k | Qwen thinking |
### Good models (bons) — high effort
| Profile | Model | Context | Use for |
| -------------- | ----------------------------- | ------- | --------------------------------- |
| `kimi-k26` | `kmc/kimi-k2.6` | 128k | General purpose (Kimi) |
| `deepseek-pro` | `ollamacloud/deepseek-v4-pro` | 128k | DeepSeek Pro via OllamaCloud |
| `deepseek` | `ds/deepseek-v4-pro` | 1M | DeepSeek Pro direct, huge context |
| `mimo` | `opencode-go/mimo-v2.5` | 128k | MiMo general |
### Simple models (simples) — no reasoning effort
| Profile | Model | Context | Use for |
| ---------- | ------------------------------ | ------- | ----------------------- |
| `gemma4` | `ollamacloud/gemma4:31b` | 32k | Cost-effective, capable |
| `nemotron` | `ollamacloud/nemotron-3-super` | 32k | NVIDIA Nemotron |
| `gptoss` | `ollamacloud/gpt-oss:20b` | 32k | Open-source GPT |
### Fast models — low effort
| Profile | Model | Context | Use for |
| ---------------- | ------------------------------------ | ------- | ----------------------- |
| `deepseek-flash` | `ollamacloud/deepseek-v4-flash` | 64k | Quick tasks |
| `gemini-flash` | `ollamacloud/gemini-3-flash-preview` | 1M | Very fast, huge context |
| `glm5turbo` | `glm/glm-5-turbo` | 128k | GLM Turbo |
| `glm47flash` | `glm/glm-4.7-flash` | 128k | GLM Flash |
| `mistral` | `mistral/mistral-large-latest` | 256k | Mistral Large |
### Quick decision table
| Task | Recommended profile |
| -------------------------------- | ------------------------------------------------ |
| Rename, format, boilerplate | `--profile deepseek-flash` or `-p low` |
| Explain, light review | `-p chat` or `-p gemini-flash` |
| Debug, moderate refactor | `-p medium` or `-p kimi-k26` |
| New feature, complex tests | `-p high` or `-p mimo` |
| Architecture, deep analysis | `-p kimi-k27` or `-p glm52` or `-p xhigh` |
| Codebase analysis (needs 1M ctx) | `--profile deepseek` or `--profile gemini-flash` |
| Maximum thinking quality | `-p glm52max` or `-p mimo-pro` |
| Cost-conscious | `-p gemma4` or `-p gptoss` |
---
## Generating profiles automatically with `omniroute setup-codex`
If you run OmniRoute on a VPS, you can auto-generate profile files from the live model catalog:
```bash
# From a VPS (uses local OmniRoute on port 20128)
omniroute setup-codex
# From any machine — point at your VPS
omniroute setup-codex --remote http://100.x.x.x:20128 --api-key sk-xxx
# Preview without writing files
omniroute setup-codex --remote http://100.x.x.x:20128 --dry-run
# Only generate GLM and Kimi profiles
omniroute setup-codex --only glm,kimi
# Write to a custom directory
omniroute setup-codex --codex-home /path/to/.codex
```
The command fetches `/v1/models`, uses tuned profiles for known models, falls back to catalog metadata for other compatible text models, and writes `~/.codex/<name>.config.toml` for each. Idempotent — safe to re-run.
OmniRoute can also **auto-sync** these same profile files after a successful provider model discovery/import changes the live catalog. This is **opt-in and off by default**: toggle it from the **CLI Code dashboard** ("CLI profile auto-sync" → Codex), or set `OMNIROUTE_AUTO_SYNC_CODEX_PROFILES=true` (it also honors `CLI_ALLOW_CONFIG_WRITES`, on by default). When enabled it only writes separate `~/.codex/*.config.toml` profile files; it never changes the active/default `~/.codex/config.toml`, Codex-lb settings, auth, or provider selection.
---
## Launching Codex with `omniroute launch-codex`
Health-checks your OmniRoute instance before launching Codex:
```bash
# Launch against local OmniRoute (default port 20128)
omniroute launch-codex
# Launch with a specific profile
omniroute launch-codex --profile kimi-k27
# Launch against a remote VPS
omniroute launch-codex --remote http://100.x.x.x:20128/v1 --api-key sk-xxx
# Pass extra args to codex
omniroute launch-codex --profile glm52 -- --yolo "fix this bug"
```
---
## New Codex CLI features (v0.138v0.141)
| Version | Feature |
| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| v0.138 | Desktop app handoff (`/app`), v2 personal access tokens, `--profile` as the exclusive profile selector (legacy in-file `[profiles]` tables crash on startup) |
| v0.139 | `web_search = "live"` — native web search from code mode; `oneOf`/`allOf` in MCP tool schemas; `codex doctor` env diagnostics |
| v0.140 | `/usage` token view in-session; `/import` from Claude Code sessions; `codex delete <SESSION_ID>` subcommand; Amazon Bedrock auth via `aws` object in provider config |
| v0.141 | E2E encrypted Noise relay for remote executors; SQLite WAL fix; P-521 TLS support |
### New `config.toml` fields (post-v0.137)
```toml
# Native web search (v0.139)
web_search = "live" # "disabled" | "cached" | "live"
# Separate developer system prompt (v0.138)
developer_instructions = "Always prefer functional style."
# Custom compaction prompt
compact_prompt = "Summarise the above as bullet points."
# Route /review to a cheaper model
review_model = "glm/glm-5-turbo"
# OpenAI service tier
service_tier = "fast" # "fast" | "flex"
```
### New `[model_providers.<id>]` fields
```toml
[model_providers.omniroute]
base_url = "http://100.x.x.x:20128/v1"
env_key = "OMNIROUTE_API_KEY"
requires_openai_auth = false
# Static extra headers on every request
[model_providers.omniroute.http_headers]
"X-Custom-Header" = "value"
# Headers read from env vars
[model_providers.omniroute.env_http_headers]
"X-Trace-Id" = "TRACE_ID"
# Extra URL query params (useful for Azure api-version)
[model_providers.omniroute.query_params]
"api-version" = "2024-12-01-preview"
```
### Amazon Bedrock auth (v0.140)
```toml
[model_providers.bedrock]
base_url = "https://bedrock-runtime.us-east-1.amazonaws.com"
[model_providers.bedrock.aws]
profile = "default" # ~/.aws/credentials profile
region = "us-east-1"
```
---
## Multiple servers
```toml
[model_providers.omniroute-main]
base_url = "http://192.168.0.1:20128/v1"
env_key = "OMNIROUTE_API_KEY"
[model_providers.omniroute-tailscale]
base_url = "http://100.x.x.x:20128/v1"
env_key = "OMNIROUTE_API_KEY"
```
---
## Claude Code — equivalent configuration
| Codex CLI (`config.toml`) | Claude Code (env var) | Effect |
| --------------------------------- | ------------------------------------- | ----------------------- |
| `tool_output_token_limit = 32768` | _(not directly exposed)_ | Per-tool history cap |
| `model_context_window = 400000` | _(determined by the model)_ | Context window |
| — | `CLAUDE_CODE_MAX_OUTPUT_TOKENS=65536` | Max tokens per response |
```bash
# ~/.bashrc — Claude Code token cap
export CLAUDE_CODE_MAX_OUTPUT_TOKENS=65536
```
---
## Quick reference — CLI flags
| Flag | Short | Effect |
| --------------------- | ----- | -------------------------------------------- |
| `--model <id>` | `-m` | Overrides `model` for this invocation |
| `--profile <name>` | `-p` | Loads `~/.codex/<name>.config.toml` |
| `--config key=value` | `-c` | Overrides any config.toml field (repeatable) |
| `--enable <feature>` | — | Force-enables a feature flag |
| `--disable <feature>` | — | Force-disables a feature flag |
| `--search` | — | Enable live web search for this invocation |
New in v0.140:
```bash
codex delete <SESSION_ID> # delete a session
codex delete <SESSION_ID> --force # skip confirmation
codex debug models --bundled # list bundled model catalog as JSON
```
Inside an interactive session:
| Command | Effect |
| --------- | ------------------------------------------- |
| `/model` | Opens the model picker |
| `/usage` | Shows token usage for this session (v0.140) |
| `/app` | Hands off to the desktop app (v0.138) |
| `/import` | Import a Claude Code session (v0.140) |
| `/help` | Lists all slash commands |
---
## Troubleshooting
**`Error: wire_api = "chat" is no longer supported`**
Remove `wire_api = "chat"` from your config. Set `wire_api = "responses"` or omit the field (defaults to `"responses"` since v0.138).
**`Error: model not found`**
Verify the model exists in OmniRoute with the correct prefix. Use `omniroute models list` or open `/dashboard/providers/<provider>`.
**`Authentication error`**
Confirm `OMNIROUTE_API_KEY` is exported: `echo $OMNIROUTE_API_KEY`.
**`Connection refused`**
Verify OmniRoute is running and the `base_url` host/port is correct for your network (local vs Tailscale vs VPS).
**Session crashes near context limit**
Set `model_context_window` and `model_auto_compact_token_limit` explicitly. See the context window table above.
**Compaction fires too late**
Lower `model_auto_compact_token_limit` to 8085% of the window. Never set above 90%.
**Profile not loading (`-p <name>` silently ignored)**
Confirm the file exists at `~/.codex/<name>.config.toml` (no `profile-` prefix). Run `ls ~/.codex/*.config.toml`.
+261
View File
@@ -0,0 +1,261 @@
---
title: "Cost & Spend Tracking"
version: 3.8.40
lastUpdated: 2026-06-28
---
# Cost & Spend Tracking
How OmniRoute estimates, records, and reports the cost of every request — and why the
dashboard number is a **savings tracker**, not a bill.
See also: [User Guide](./USER_GUIDE.md) · [Features Gallery](./FEATURES.md)
---
## What it is (and what it is not)
OmniRoute attributes a per-request USD cost to every completion by multiplying token
counts by a model's pricing rates. These numbers power the **Costs** dashboard, the
`omniroute cost` / `omniroute usage` CLI, CSV/JSON exports, and per-API-key budgets.
> **The dashboard "cost" is a savings tracker, not a bill.** OmniRoute never charges you
> — it routes your requests to providers you have already connected (your own
> subscriptions, free tiers, and API keys). A "$290 total cost" accrued entirely on free
> models means roughly **$290 you did _not_ pay** a paid API. The figure is an _estimate_
> of what the same traffic would have cost at standard list prices, so you can see where
> your usage is concentrated and how much routing to cheaper/free providers is saving you.
This framing is stated directly in the project [README](../../README.md) ("the dashboard
'cost' is a savings tracker, not a bill").
Because the number is an estimate:
- It depends on the pricing table OmniRoute has for each model. A model with no pricing
entry contributes `0` cost (it shows as a "Legacy / Free" row in the explorer).
- Free-tier and subscription traffic still accrues an _estimated_ cost — that is the
amount you are saving, not an amount owed.
---
## How costs are estimated
### The pricing source
Costs come from a pricing table resolved in this precedence order
([`src/lib/pricingSync.ts`](../../src/lib/pricingSync.ts)):
1. **User overrides** — prices you set in the dashboard / via `PATCH /api/pricing`.
2. **Synced external pricing** — fetched from LiteLLM's public
`model_prices_and_context_window.json` when sync is enabled (stored in a separate
`pricing_synced` namespace so it never clobbers your overrides).
3. **Hardcoded defaults** — shipped with OmniRoute.
External pricing sync is **opt-in**, disabled by default. Relevant env vars
(see [`.env.example`](../../.env.example)):
| Env var | Default | Purpose |
| ----------------------- | --------- | ---------------------------------------------------------------- |
| `PRICING_SYNC_ENABLED` | `false` | Enable the background LiteLLM pricing sync at startup. |
| `PRICING_SYNC_INTERVAL` | `86400` | Sync interval in **seconds** (default daily). |
| `PRICING_SYNC_SOURCES` | `litellm` | Comma-separated source list (only `litellm` is supported today). |
### The cost formula
Cost is computed per request from token counts and per-million-token rates in
[`src/lib/usage/costCalculator.ts`](../../src/lib/usage/costCalculator.ts)
(`computeCostFromPricing` / `calculateCost`):
- **Input tokens** (minus cache reads and cache-creation tokens) × `input` rate.
- **Cache-read tokens** × `cached` rate (falls back to the input rate).
- **Cache-creation tokens** × `cache_creation` rate (falls back to the input rate).
- **Output tokens** × `output` rate.
- **Reasoning tokens** × `reasoning` rate (falls back to the output rate).
All rates are interpreted as USD per 1,000,000 tokens. A Codex "fast"/"priority" or
"flex" service tier applies a cost multiplier (`getCodexFastCostMultiplier`) — e.g. flex
is billed at a 50% token discount, surfaced as **flex savings** in the dashboard.
Model names are normalized first (provider-path prefixes such as `openai/` or
`accounts/fireworks/models/` are stripped) so historical rows still match a price.
### How spend is recorded
- The per-request cost is computed after the response and recorded fire-and-forget so it
never adds latency to the client. Shared-quota consumption is scheduled on the next
event-loop tick via [`src/lib/quota/spendRecorder.ts`](../../src/lib/quota/spendRecorder.ts).
- API-key spend is buffered and flushed in batches by the
[`SpendBatchWriter`](../../src/lib/spend/batchWriter.ts) (default 60s flush interval,
1,000-entry buffer). Tunable via:
| Env var | Default | Purpose |
| ----------------------------------- | ------- | ---------------------------------- |
| `OMNIROUTE_SPEND_FLUSH_INTERVAL_MS` | `60000` | Flush interval in milliseconds. |
| `OMNIROUTE_SPEND_MAX_BUFFER_SIZE` | `1000` | Max buffered entries before flush. |
The dashboard's cost figures are **not** read from a stored per-row dollar amount — they
are recomputed on the fly from token counts and the current pricing table each time the
analytics endpoint runs. That means correcting a wrong price (and re-syncing) updates
historical cost estimates retroactively.
---
## Dashboard: the Costs page
The **Costs** page lives at `/dashboard/costs`
(`src/app/(dashboard)/dashboard/costs/`).
Its main view is the **Cost Overview** tab
(`src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx`),
which loads everything from `GET /api/usage/analytics`.
What it shows:
- **Spend tiles** — estimated spend for _Today (1d)_, _7d_, _30d_, and the selected
window. Range selector: `7d`, `30d`, `90d`, `all`.
- **Headline metrics** — requests in window, active providers, active models, average
cost per request.
- **Cost Explorer** — a sortable/filterable table grouped by **provider**, **model**,
**API key**, **account**, or **service tier**, with cost, requests, tokens, avg
cost/request, and share-of-total %.
- **Token usage** — total / input / output tokens and input:output ratio.
- **Routing efficiency** — fallback count, fallback rate, and requested-model coverage.
- **Monthly forecast** — projects month-end spend from the recent daily average.
- **Period comparison** — % change between the first and second half of the window.
- **Charts** — daily cost trend, provider share (pie), top providers, top models, cost
by API key, cost by account, weekly usage pattern, and an activity heatmap.
- **Export** — download the current window as **CSV** or **JSON** (the buttons appear
once there is non-zero cost data).
When there is no priced traffic, rows render as a "Legacy / Free" label instead of `$0`,
reflecting the savings-tracker model.
### Related Costs sub-pages
The Costs area also hosts (all under `/dashboard/costs/`):
- **Pricing** (`/dashboard/costs/pricing`) — view and override per-model prices (renders
the shared Pricing tab).
- **Budget** (`/dashboard/costs/budget`) — set per-scope spend limits (renders the shared
Budget tab).
- **Quota Share** (`/dashboard/costs/quota-share`) — shared-quota pools and burn-rate
views.
---
## API endpoints
All of these require management auth (loopback/JWT, via `requireManagementAuth`) unless
noted.
### Usage & cost analytics
| Method | Endpoint | Purpose |
| ------ | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET` | `/api/usage/analytics` | Full cost/usage analytics: summary, daily trend, by provider/model/API key/account/tier. Query: `range`, `startDate`, `endDate`, `apiKeyIds`, `presets`. |
| `GET` | `/api/usage/utilization` | Per-provider quota utilization over time. Query: `range` (`1h`/`24h`/`7d`/`30d`), `provider`. |
| `GET` | `/api/usage/history` | Raw usage history rows. |
| `GET` | `/api/usage/call-logs` | Per-request call logs (model, tokens, cost, latency, status). |
| `GET` | `/api/usage/quota` | Provider quota status. |
| `GET` | `/api/usage/proxy-logs` | Proxy request logs. |
### Budgets
| Method | Endpoint | Purpose |
| ------ | ------------------------ | ------------------------------------------------------------------------------ |
| `GET` | `/api/usage/budget` | Cost summary + budget check for one API key (`apiKeyId` query param required). |
| `POST` | `/api/usage/budget` | Set daily/weekly/monthly USD limits + warning threshold for an API key. |
| `GET` | `/api/usage/budget/bulk` | Bulk budget summaries across API keys. |
> The budget API is scoped per **API key** (`apiKeyId`). Limits returned by
> `GET /api/usage/budget` include `dailyLimitUsd`, `weeklyLimitUsd`, `monthlyLimitUsd`,
> a `warningThreshold`, and the running totals (`totalCostToday`, `totalCostMonth`, …).
### Pricing
| Method | Endpoint | Purpose |
| -------- | ----------------------- | ----------------------------------------------------------------------------------------------- |
| `GET` | `/api/pricing` | Current merged pricing (user + synced + defaults). `?includeSources=1` to see source per entry. |
| `PATCH` | `/api/pricing` | Override pricing for `{ provider: { model: { input, output, cached, … } } }`. |
| `DELETE` | `/api/pricing` | Reset pricing to defaults (optionally scoped by `?provider=&model=`). |
| `GET` | `/api/pricing/defaults` | Show default per-1M fallback rates. |
| `GET` | `/api/pricing/models` | Pricing keyed by model. |
| `POST` | `/api/pricing/sync` | Trigger a manual sync from external sources (LiteLLM). |
| `GET` | `/api/pricing/sync` | Current sync status. |
| `DELETE` | `/api/pricing/sync` | Clear all synced pricing data. |
### Other cost-relevant endpoints
| Method | Endpoint | Purpose |
| ------ | ----------------------------- | ----------------------------------------------------------------------- |
| `GET` | `/api/free-tier/summary` | Free-model token totals, used-this-month, and remaining free allowance. |
| `GET` | `/api/quota/pools/[id]/usage` | Usage for a shared-quota pool. |
---
## CLI
OmniRoute's CLI exposes cost, usage, and pricing commands (registered in
[`bin/cli/commands/registry.mjs`](../../bin/cli/commands/registry.mjs)).
### `omniroute cost`
A cost report aggregated from `/api/usage/analytics`.
```bash
omniroute cost # last 30d, grouped by provider
omniroute cost --period 7d # last 7 days
omniroute cost --group-by model # group by provider | model | combo | api-key | day
omniroute cost --since 2026-06-01 --until 2026-06-13
omniroute cost --api-key <key> --limit 50
```
Columns: group, requests, tokens in/out, cost (USD), and % of total. A grand total line
is printed at the end (suppressed with `--quiet` or `--output json`).
### `omniroute usage`
```bash
omniroute usage analytics --period 30d [--provider <id>] # per-provider cost summary
omniroute usage logs [--limit 100] [--follow] [--api-key <k>] [--search <q>]
omniroute usage quota [--provider <id>] [--check]
omniroute usage utilization [--api-key <k>]
omniroute usage history [--limit 100]
omniroute usage proxy-logs [--limit 100]
# Budgets
omniroute usage budget list
omniroute usage budget get [scope]
omniroute usage budget set <amount> [--scope global] [--period monthly]
omniroute usage budget reset [scope]
```
### `omniroute pricing`
```bash
omniroute pricing list [--provider <p>] [--model <m>] [--limit 200]
omniroute pricing get <model>
omniroute pricing sync [--provider <p>] [--force] # POST /api/pricing/sync
omniroute pricing diff [--model <m>]
omniroute pricing defaults show
omniroute pricing defaults set [--input <p>] [--output <p>] [--cache-read <p>] [--cache-write <p>]
```
> `pricing defaults show` reads `GET /api/pricing/defaults`. To edit individual model
> prices instead, use the **Pricing** dashboard page or `PATCH /api/pricing`.
---
## Troubleshooting
- **All costs show $0 / "Legacy / Free".** The models in use have no pricing entry.
Enable external sync (`PRICING_SYNC_ENABLED=true`) and run `omniroute pricing sync`, or
set prices manually via the Pricing page / `PATCH /api/pricing`.
- **A historical model is mispriced.** Fix the price (override or re-sync) — cost is
recomputed from token counts on every analytics read, so estimates update retroactively.
- **Spend lags behind real time.** Per-key spend is batched; lower
`OMNIROUTE_SPEND_FLUSH_INTERVAL_MS` if you need fresher numbers.
---
For where this fits in the broader dashboard, see the [User Guide](./USER_GUIDE.md) and
the [Features Gallery](./FEATURES.md).
+254
View File
@@ -0,0 +1,254 @@
---
title: "🐳 Docker Guide — OmniRoute"
version: 3.8.40
lastUpdated: 2026-06-28
---
# 🐳 Docker Guide — OmniRoute
> Complete Docker deployment reference. For a quick start, see the [README Docker section](../README.md#-docker).
## Table of Contents
- [Quick Run](#quick-run)
- [With Environment File](#with-environment-file)
- [Docker Compose](#docker-compose)
- [Available Profiles](#available-profiles)
- [Redis Sidecar](#redis-sidecar)
- [Production Compose](#production-compose)
- [Dockerfile Stages](#dockerfile-stages)
- [Critical Environment Variables](#critical-environment-variables)
- [Docker Compose with Caddy (HTTPS)](#docker-compose-with-caddy-https-auto-tls)
- [Cloudflare Quick Tunnel](#cloudflare-quick-tunnel)
- [Image Tags](#image-tags)
- [Important Notes](#important-notes)
---
## Quick Run
```bash
docker run -d \
--name omniroute \
--restart unless-stopped \
--stop-timeout 40 \
-p 20128:20128 \
-v omniroute-data:/app/data \
diegosouzapw/omniroute:latest
```
## With Environment File
```bash
# Copy and edit .env first
cp .env.example .env
docker run -d \
--name omniroute \
--restart unless-stopped \
--stop-timeout 40 \
--env-file .env \
-p 20128:20128 \
-v omniroute-data:/app/data \
diegosouzapw/omniroute:latest
```
## Docker Compose
```bash
# Base profile (no CLI tools)
docker compose --profile base up -d
# CLI profile (Claude Code, Codex, OpenClaw built-in)
docker compose --profile cli up -d
# Host profile (Linux-first; mounts host CLI binaries read-only)
docker compose --profile host up -d
# Combine CLI + CLIProxyAPI sidecar
docker compose --profile cli --profile cliproxyapi up -d
```
## Available Profiles
OmniRoute ships four Compose profiles. Pick the one that matches your environment.
| Profile | Service | When to use | Command |
| ---------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| `base` (default) | `omniroute-base` | Headless server / minimal runtime, no provider CLIs bundled | `docker compose --profile base up -d` |
| `cli` | `omniroute-cli` | Agentic workflows that call `omniroute providers/setup/doctor` and bundled CLIs (Codex, Claude Code, Droid, OpenClaw) | `docker compose --profile cli up -d` |
| `host` | `omniroute-host` | Linux hosts that want `network_mode`-like access to host CLIs by mounting `~/.local/bin`, `~/.codex`, `~/.claude`, etc. read-only | `docker compose --profile host up -d` |
| `cliproxyapi` | `cliproxyapi` | Run the [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI) sidecar on port `8317` for upstream CLI proxying | `docker compose --profile cliproxyapi up -d` |
> Multiple profiles can be combined: `docker compose --profile cli --profile cliproxyapi up -d`.
## Redis Sidecar
OmniRoute relies on Redis to back the distributed rate limiter and shared cache. The `redis` service is **always defined** in `docker-compose.yml` (it has no profile gate) and starts alongside any other profile.
| Detail | Value |
| -------------------- | --------------------------------- |
| Image | `redis:7-alpine` |
| Container name | `omniroute-redis` |
| Internal port | `6379` |
| Host port (override) | `REDIS_PORT` (defaults to `6379`) |
| Volume | `omniroute-redis-data``/data` |
| Healthcheck | `redis-cli ping` (10s interval) |
Related environment variables:
- `REDIS_URL` — connection string injected into the app (`redis://redis:6379` by default).
- `REDIS_PORT` — host-side port mapping for the Redis container.
**Disabling Redis** is not recommended (rate limiter will degrade to in-memory fallback). If you must, either remove/comment the `redis:` service block in `docker-compose.yml` or scale it to zero:
```bash
docker compose up -d --scale redis=0
```
## Production Compose
For an isolated production snapshot running alongside dev, use `docker-compose.prod.yml`.
| Detail | Value |
| ---------------------- | ---------------------------------------------------------------------------------- |
| File | `docker-compose.prod.yml` |
| Default dashboard port | `PROD_DASHBOARD_PORT=20130` (mapped to internal `${DASHBOARD_PORT:-20128}`) |
| Default API port | `PROD_API_PORT=20131` |
| Image | `omniroute:prod` (built from `runner-cli` target) |
| Redis container | `omniroute-redis-prod` (`redis:8.6.2`, dedicated `redis-prod-data` volume) |
| Data volume | `omniroute-prod-data` (named, persisted across rebuilds) |
| Healthchecks | `node healthcheck.mjs` + `redis-cli ping`, with `depends_on` gated on Redis health |
How to use:
```bash
# Build & start the production stack
docker compose -f docker-compose.prod.yml up -d --build
# Stream logs
docker compose -f docker-compose.prod.yml logs -f
# Tear down (keep volumes)
docker compose -f docker-compose.prod.yml down
```
The prod stack runs in parallel with the dev compose (different container names, ports, and volumes), so you can keep iterating locally while production stays up.
## Dockerfile Stages
The repository ships a multi-stage Dockerfile (`Dockerfile`). Three stages are exposed; pick the right `target` for your use case.
| Stage | Base image | Purpose |
| ------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `builder` | `node:24.15.0-trixie-slim` | Installs deps (`npm ci --legacy-peer-deps`) and runs `npm run build -- --webpack` |
| `runner-base` | `node:24.15.0-trixie-slim` | Production runtime with the Next.js standalone output. **No provider CLIs bundled.** |
| `runner-cli` | `runner-base` | Adds `git`, `docker.io`, `docker-compose` and global CLIs: `@openai/codex`, `@anthropic-ai/claude-code`, `droid`, `openclaw`. **Pick this for agentic workflows.** |
Build a specific target manually:
```bash
docker build --target runner-base -t omniroute:base .
docker build --target runner-cli -t omniroute:cli .
```
Defaults exported by `runner-base`: `PORT=20128`, `HOSTNAME=0.0.0.0`, `NODE_OPTIONS=--max-old-space-size=512`, `DATA_DIR=/app/data`, `OMNIROUTE_MIGRATIONS_DIR=/app/migrations`.
Memory behavior in Docker:
- `NODE_OPTIONS=--max-old-space-size=512` is baked into the image as a fallback.
- The actual server process is started by the standalone launcher, which reads `OMNIROUTE_MEMORY_MB` and appends `--max-old-space-size=<OMNIROUTE_MEMORY_MB>`.
- Node uses the last repeated `--max-old-space-size` value, so setting `OMNIROUTE_MEMORY_MB` controls the effective Docker heap limit.
- If `OMNIROUTE_MEMORY_MB` is unset, the launcher uses `512`.
## Critical Environment Variables
Beyond the defaults documented in [ENVIRONMENT.md](../reference/ENVIRONMENT.md), the following variables matter most when running under Docker:
| Variable | Purpose | Default |
| ----------------------------- | --------------------------------------------------------------------------------------------------- | ------------------------ |
| `OMNIROUTE_WS_BRIDGE_SECRET` | Shared secret for the WebSocket bridge. **Required in production** — set to a strong random string. | unset (must be provided) |
| `REDIS_URL` | Connection string for the rate limiter / cache backend | `redis://redis:6379` |
| `REDIS_PORT` | Host-side port for the bundled Redis container | `6379` |
| `AUTO_UPDATE_HOST_REPO_DIR` | Host path mounted into `cli` profile at `/workspace/omniroute` for self-update workflows | `.` (current directory) |
| `OMNIROUTE_MEMORY_MB` | Runtime Node heap ceiling for the Docker standalone server; overrides the image fallback above | `512` |
| `DASHBOARD_PORT` / `API_PORT` | Override exposed ports for dashboard (20128) and API (20129) | `20128` / `20129` |
| `PROD_DASHBOARD_PORT` | Host-side dashboard port for `docker-compose.prod.yml` | `20130` |
| `CLIPROXYAPI_PORT` | Host-side port for the `cliproxyapi` sidecar | `8317` |
## Docker Compose with Caddy (HTTPS Auto-TLS)
OmniRoute can be securely exposed using Caddy's automatic SSL provisioning. Ensure your domain's DNS A record points to your server's IP.
```yaml
services:
omniroute:
image: diegosouzapw/omniroute:latest
container_name: omniroute
restart: unless-stopped
volumes:
- omniroute-data:/app/data
environment:
- PORT=20128
# Browser-facing origin for OAuth callbacks, dashboard links, and generated public URLs.
- NEXT_PUBLIC_BASE_URL=https://your-domain.com
# Internal server-to-server URL for scheduled jobs / self-fetches.
- BASE_URL=http://omniroute:20128
- AUTH_COOKIE_SECURE=true
caddy:
image: caddy:latest
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
command: caddy reverse-proxy --from https://your-domain.com --to http://omniroute:20128
volumes:
omniroute-data:
```
Caddy sets the standard forwarding headers for the upstream container. OmniRoute uses
`NEXT_PUBLIC_BASE_URL` as the canonical public origin for OAuth callbacks and generated public
links; authenticated dashboard writes use same-origin requests plus session-bound CSRF
protection. Only enable `OMNIROUTE_TRUST_PROXY` for advanced deployments where you intentionally
want OmniRoute to derive the public origin from trusted forwarded headers instead of explicit
configuration.
## Cloudflare Quick Tunnel
Dashboard support for Docker deployments includes a one-click **Cloudflare Quick Tunnel** on `Dashboard → Endpoints`. The first enable downloads `cloudflared` only when needed, starts a temporary tunnel to your current `/v1` endpoint, and shows the generated `https://*.trycloudflare.com/v1` URL directly below your normal public URL.
Endpoint tunnel panels (Cloudflare, Tailscale, ngrok) can be shown or hidden from `Settings → Appearance` without changing active tunnel state.
### Tunnel Notes
- Quick Tunnel URLs are temporary and change after every restart.
- Quick Tunnels are not auto-restored after an OmniRoute or container restart. Re-enable them from the dashboard when needed.
- Managed install currently supports Linux, macOS, and Windows on `x64` / `arm64`.
- Managed Quick Tunnels default to HTTP/2 transport to avoid noisy QUIC UDP buffer warnings in constrained container environments. Set `CLOUDFLARED_PROTOCOL=quic` or `auto` if you want a different transport.
- Docker images bundle system CA roots and pass them to managed `cloudflared`, which avoids TLS trust failures when the tunnel bootstraps inside the container.
- Set `CLOUDFLARED_BIN=/absolute/path/to/cloudflared` if you want OmniRoute to use an existing binary instead of downloading one.
## Image Tags
| Image | Tag | Size | Description |
| ------------------------ | -------- | ------ | --------------------- |
| `diegosouzapw/omniroute` | `latest` | ~250MB | Latest stable release |
| `diegosouzapw/omniroute` | `3.8.0` | ~250MB | Current version |
Multi-platform manifest: `linux/amd64` + `linux/arm64` native (Apple Silicon, AWS Graviton, Raspberry Pi). Docker selects the matching architecture automatically; pass `--platform linux/amd64` if you need to force AMD64 emulation on ARM hosts.
## Important Notes
- **SQLite WAL Mode:** `docker stop` should be allowed to finish so OmniRoute can checkpoint the latest changes back into `storage.sqlite`. The bundled Compose files already set a 40s stop grace period. If you run the image directly, keep `--stop-timeout 40`.
- **`DISABLE_SQLITE_AUTO_BACKUP`:** Set to `true` if backups are managed externally.
- **Data Persistence:** Always mount a volume to `/app/data` to persist your database, keys, and configurations across container restarts.
- **Port Configuration:** Override `PORT` environment variable to change the default `20128` port.
## See Also
- [VM Deployment Guide](../ops/VM_DEPLOYMENT_GUIDE.md) — VM + nginx + Cloudflare setup
- [Fly.io Deployment Guide](../ops/FLY_IO_DEPLOYMENT_GUIDE.md) — Deploy to Fly.io
- [Environment Config](../reference/ENVIRONMENT.md) — Complete `.env` reference
+277
View File
@@ -0,0 +1,277 @@
---
title: "Electron Desktop Guide"
version: 3.8.40
lastUpdated: 2026-06-28
---
# Electron Desktop Guide
> **Source of truth:** `electron/` workspace
> **Last updated:** 2026-06-28 — v3.8.40
OmniRoute ships a cross-platform desktop app (Windows / macOS / Linux) built on
**Electron 41** + **electron-builder 26.10**. The desktop app spawns the Next.js
standalone server as a child process, points a `BrowserWindow` at it, and adds a
system tray, auto-updater, IPC bridge, and zero-config secret bootstrap.
## Architecture
```
┌──────────────────────────────────────────────┐
│ Electron main process (electron/main.js) │
│ ├─ Single-instance lock │
│ ├─ Child process: Next.js standalone server │
│ │ (spawned with Electron's Node runtime) │
│ ├─ BrowserWindow → http://localhost:PORT │
│ ├─ System tray + context menu │
│ ├─ Auto-update via electron-updater │
│ ├─ Content Security Policy (session headers) │
│ └─ Secret bootstrap (JWT / API_KEY_SECRET) │
└──────────────────────────────────────────────┘
↕ IPC bridge (electron/preload.js)
┌──────────────────────────────────────────────┐
│ Renderer (Next.js dashboard) │
│ window.electronAPI.* (contextIsolation) │
└──────────────────────────────────────────────┘
```
## Versions
Confirmed from `electron/package.json`:
| Package | Version |
| ------------------ | -------------------------- |
| `electron` | `^41.5.1` |
| `electron-builder` | `^26.10.0` |
| `electron-updater` | `^6.8.5` |
| `better-sqlite3` | `^12.9.0` |
| App version | `3.8.0` |
| App id | `online.omniroute.desktop` |
| Product name | `OmniRoute` |
## Scripts (root `package.json`)
| Script | Purpose |
| --------------------------------- | -------------------------------------------------------------------------- |
| `npm run electron:dev` | Starts `npm run dev` + waits for `localhost:20128` + launches Electron |
| `npm run electron:build` | Builds Next.js then runs `electron-builder` for the current OS |
| `npm run electron:build:win` | Builds Windows NSIS installer + portable (x64) |
| `npm run electron:build:mac` | Builds macOS DMG (Intel + Apple Silicon) |
| `npm run electron:build:linux` | Builds Linux AppImage + DEB (x64 + arm64) |
| `npm run electron:smoke:packaged` | Launches packaged binary and probes `/login` for HTTP 200, then shuts down |
The `electron/` workspace also exposes:
- `npm run prepare:bundle` — runs `scripts/build/prepare-electron-standalone.mjs`
- `npm run build:mac-x64` / `build:mac-arm64` — single-arch macOS builds
- `npm run pack` — directory-only build for local testing (no installer)
## Directory Layout
```
electron/
├── package.json # Electron deps + electron-builder config
├── main.js # Main process (24 KB — see annotations below)
├── preload.js # contextBridge IPC bridge
├── types.d.ts # AppInfo / ServerStatus / ElectronAPI types
├── README.md # In-workspace notes
├── assets/ # icon.png, icon.ico, icon.icns, tray-icon.png
└── dist-electron/ # electron-builder output (gitignored)
scripts/
├── build/
│ └── prepare-electron-standalone.mjs # Stages .next/electron-standalone bundle
└── dev/
└── smoke-electron-packaged.mjs # Post-build smoke test
```
Both `main.js` and `preload.js` are **CommonJS `.js` files**, not TypeScript. The
renderer-side typings live in `electron/types.d.ts`.
## IPC Bridge (`preload.js`)
The preload exposes a whitelisted API on `window.electronAPI` using `contextBridge`
with `contextIsolation: true` and `nodeIntegration: false`.
```javascript
const VALID_CHANNELS = {
invoke: [
"get-app-info",
"open-external",
"get-data-dir",
"restart-server",
"check-for-updates",
"download-update",
"install-update",
"get-app-version",
],
send: ["window-minimize", "window-maximize", "window-close"],
receive: ["server-status", "port-changed", "update-status"],
};
```
Exposed methods:
| Renderer call | Type |
| ----------------------------------------------------------------- | -------------------------- |
| `getAppInfo()``{ name, version, platform, isDev, port }` | invoke |
| `openExternal(url)` | invoke |
| `getDataDir()` | invoke |
| `restartServer()` | invoke |
| `getAppVersion()` | invoke |
| `checkForUpdates()` / `downloadUpdate()` / `installUpdate()` | invoke |
| `minimizeWindow()` / `maximizeWindow()` / `closeWindow()` | send |
| `onServerStatus(cb)` / `onPortChanged(cb)` / `onUpdateStatus(cb)` | receive (returns disposer) |
The receive helpers return a **disposer function** rather than relying on
`removeAllListeners` — this prevents listener accumulation when React components
remount.
## Server Lifecycle
`main.js` spawns the Next.js standalone bundle directly with the Electron Node
runtime to avoid native-module ABI mismatch with system Node:
```js
spawn(process.execPath, [serverScript], {
cwd: NEXT_SERVER_PATH,
env: { ...serverEnv, PORT, NODE_ENV: "production", ELECTRON_RUN_AS_NODE: "1", NODE_PATH },
stdio: "pipe",
});
```
Highlights:
- `waitForServer()` polls the URL up to 30 s before showing the window (no blank screen on cold start).
- `stdio: "pipe"` captures stdout/stderr; ready phrases (`Ready` / `listening`) emit `server-status: running` over IPC.
- `before-quit` waits up to 5 s for graceful SIGTERM (WAL checkpoint) then sends SIGKILL.
- Port switcher in the tray (`20128`, `3000`, `8080`) stops and restarts the server, then reloads the BrowserWindow.
## Zero-config Secret Bootstrap
On first launch, the main process auto-generates and persists missing secrets:
| Secret | Source |
| ------------------------ | ----------------------------------------------------------------------------------- |
| `JWT_SECRET` | `crypto.randomBytes(64).toString("hex")` |
| `STORAGE_ENCRYPTION_KEY` | `crypto.randomBytes(32).toString("hex")` (refuses if encrypted creds already exist) |
| `API_KEY_SECRET` | `crypto.randomBytes(32).toString("hex")` |
Persisted to `<DATA_DIR>/server.env`. `DATA_DIR` resolves to:
- Windows: `%APPDATA%\omniroute`
- Linux: `$XDG_CONFIG_HOME/omniroute` or `~/.omniroute`
- macOS: `~/.omniroute`
## Window & Tray
- `BrowserWindow`: 1400×900 (min 1024×700), `backgroundColor: "#0a0a0a"`.
- macOS: `titleBarStyle: "hiddenInset"`, traffic-light at `{ x: 16, y: 16 }`.
- Windows/Linux: native title bar.
- Close button minimizes to tray; the tray menu has **Open OmniRoute**, **Open Dashboard** (external browser), **Server Port** submenu, **Check for Updates**, **Quit**.
## Content Security Policy
Set via `session.defaultSession.webRequest.onHeadersReceived`. Notable directives:
- `frame-ancestors 'none'`, `object-src 'none'`, `child-src 'none'`
- `connect-src 'self' http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:* https://*.omniroute.online https://*.omniroute.dev`
- Dev mode adds `'unsafe-eval'` to `script-src` only
## Auto-update
Uses `electron-updater` with the GitHub provider (`diegosouzapw/OmniRoute`).
- `autoDownload = false`, `autoInstallOnAppQuit = true`
- Events forwarded to renderer via `update-status` IPC:
`checking`, `available`, `not-available`, `downloading` (with `percent`), `downloaded`, `error`
- `installUpdate()` kills the server then calls `autoUpdater.quitAndInstall()`
- Skipped in dev mode (`!app.isPackaged`)
## Build Pipeline
1. `npm run build` → Next.js standalone in `.next/standalone`.
2. `prepare-electron-standalone.mjs` → re-stages into `.next/electron-standalone` and rewrites absolute paths inside `server.js` + `required-server-files.json` so the bundle is relocatable.
3. `electron-builder` packages `main.js`, `preload.js`, `node_modules`, and `extraResources: { ../.next/electron-standalone → app }`.
### Build targets
| OS | Targets |
| ------- | ----------------------------------------- |
| Windows | NSIS installer + portable (x64) |
| macOS | DMG (Intel + arm64, drag-to-Applications) |
| Linux | AppImage + DEB (x64 + arm64) |
NSIS settings: `oneClick: false`, lets the user choose the install directory, creates Desktop and Start-Menu shortcuts.
## Smoke Testing Packaged Build
```bash
npm run electron:smoke:packaged
```
`scripts/dev/smoke-electron-packaged.mjs`:
- Auto-discovers the packaged binary in `electron/dist-electron/` for the current platform.
- Launches with isolated `HOME`/`APPDATA`/`XDG_*` directories so it doesn't touch developer data.
- Polls `http://127.0.0.1:20128/login` for HTTP 200 within 45 s.
- Watches stderr/stdout for fatal patterns (`Cannot find module`, `MODULE_NOT_FOUND`, `ERR_DLOPEN_FAILED`, `Failed to start server`, etc.).
- Waits 2 s of stable runtime after readiness, then issues SIGTERM and waits for the port to free.
- In CI, automatically passes `--no-sandbox --disable-gpu` (and `--disable-dev-shm-usage` on Linux).
Env overrides: `ELECTRON_SMOKE_APP_EXECUTABLE`, `ELECTRON_SMOKE_URL`, `ELECTRON_SMOKE_TIMEOUT_MS`, `ELECTRON_SMOKE_SETTLE_MS`, `ELECTRON_SMOKE_DATA_DIR`, `ELECTRON_SMOKE_KEEP_DATA`, `ELECTRON_SMOKE_STREAM_LOGS`.
## Code Signing
`electron/package.json` does **not** wire signing credentials directly. Pass them via env vars to `electron-builder`:
### macOS
```bash
export APPLE_ID=<email>
export APPLE_APP_SPECIFIC_PASSWORD=<password>
export APPLE_TEAM_ID=<id>
export CSC_LINK=path/to/cert.p12
export CSC_KEY_PASSWORD=<cert-password>
npm run electron:build:mac
```
### Windows
```bash
export CSC_LINK=path/to/cert.pfx
export CSC_KEY_PASSWORD=<cert-password>
npm run electron:build:win
```
### Linux
AppImage signing is optional — set `LINUX_GPG_KEY` if signing.
## Distribution
Artifacts land in `electron/dist-electron/`:
- `OmniRoute Setup X.Y.Z.exe`, `OmniRoute-X.Y.Z-portable.exe` (Windows)
- `OmniRoute-X.Y.Z-mac.dmg`, `OmniRoute-X.Y.Z-arm64-mac.dmg` (macOS)
- `OmniRoute-X.Y.Z.AppImage`, `omniroute-desktop_X.Y.Z_amd64.deb` (Linux)
Releases are published to GitHub Releases (`diegosouzapw/OmniRoute`), which is also where `electron-updater` checks for new versions.
## Troubleshooting
| Symptom | Fix |
| --------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `Cannot find module 'better-sqlite3'` after Electron major bump | `cd electron && npm rebuild` |
| `ERR_DLOPEN_FAILED` for native module | Re-run `prepare:bundle` and verify ABI matches Electron's Node |
| Window appears blank on Linux | Confirm Next.js server actually bound to PORT (check `[Server]` logs) |
| macOS notarization stalls | Ensure `APPLE_*` vars are exported, not just in `.env` |
| Windows SmartScreen warning | Sign with EV cert, or users right-click → "Run anyway" |
| Smoke test fails with port-in-use | Stop any local dev server on 20128 before running `electron:smoke:packaged` |
## See Also
- [SETUP_GUIDE.md](./SETUP_GUIDE.md)
- [RELEASE_CHECKLIST.md](../ops/RELEASE_CHECKLIST.md)
- Source: `electron/main.js`, `electron/preload.js`, `electron/package.json`
- Helpers: `scripts/build/prepare-electron-standalone.mjs`, `scripts/dev/smoke-electron-packaged.mjs`
+329
View File
@@ -0,0 +1,329 @@
---
title: "OmniRoute — Dashboard Features Gallery"
version: 3.8.40
lastUpdated: 2026-06-28
---
# OmniRoute — Dashboard Features Gallery
🌐 **Main README translations:** 🇺🇸 [English](../README.md) | 🇧🇷 [Português (Brasil)](../i18n/pt-BR/README.md) | 🇪🇸 [Español](../i18n/es/README.md) | 🇫🇷 [Français](../i18n/fr/README.md) | 🇮🇹 [Italiano](../i18n/it/README.md) | 🇷🇺 [Русский](../i18n/ru/README.md) | 🇨🇳 [中文 (简体)](../i18n/zh-CN/README.md) | 🇩🇪 [Deutsch](../i18n/de/README.md) | 🇮🇳 [हिन्दी](../i18n/in/README.md) | 🇹🇭 [ไทย](../i18n/th/README.md) | 🇺🇦 [Українська](../i18n/uk-UA/README.md) | 🇸🇦 [العربية](../i18n/ar/README.md) | 🇯🇵 [日本語](../i18n/ja/README.md) | 🇻🇳 [Tiếng Việt](../i18n/vi/README.md) | 🇧🇬 [Български](../i18n/bg/README.md) | 🇩🇰 [Dansk](../i18n/da/README.md) | 🇫🇮 [Suomi](../i18n/fi/README.md) | 🇮🇱 [עברית](../i18n/he/README.md) | 🇭🇺 [Magyar](../i18n/hu/README.md) | 🇮🇩 [Bahasa Indonesia](../i18n/id/README.md) | 🇰🇷 [한국어](../i18n/ko/README.md) | 🇲🇾 [Bahasa Melayu](../i18n/ms/README.md) | 🇳🇱 [Nederlands](../i18n/nl/README.md) | 🇳🇴 [Norsk](../i18n/no/README.md) | 🇵🇹 [Português (Portugal)](../i18n/pt/README.md) | 🇷🇴 [Română](../i18n/ro/README.md) | 🇵🇱 [Polski](../i18n/pl/README.md) | 🇸🇰 [Slovenčina](../i18n/sk/README.md) | 🇸🇪 [Svenska](../i18n/sv/README.md) | 🇵🇭 [Filipino](../i18n/phi/README.md) | 🇨🇿 [Čeština](../i18n/cs/README.md)
Visual guide to every section of the OmniRoute dashboard.
> 📅 **Last updated:** 2026-06-28 — **v3.8.40**
---
## ✨ v3.8.0 Highlights
The v3.7.x → v3.8.0 cycle added zero-config auto routing, new providers, OAuth flows, deeper resilience, and a much richer CLI experience. Headline features below — full details further in the document and in linked specs.
- 🤖 **Auto Combo / Zero-config auto-routing** — use prefixes `auto/coding`, `auto/fast`, `auto/cheap`, `auto/offline`, `auto/smart`, `auto/lkgp`. Backed by a 9-factor scoring engine and 4 curated **mode packs** (ship-fast, cost-saver, quality-first, offline-friendly)
- 🆕 **Command Code provider** (#2199) — first-class registration with model catalog and quota tracking
- 🆕 **Z.AI provider** — new free-tier provider with quota labels
- 🎬 **KIE media expansion** — extended catalog including video generation models
- 🔐 **Windsurf + Devin CLI OAuth flows** (#2168) — end-to-end browser-based login
- 🆓 **9 new free providers** — LLM7, Lepton, Kluster, UncloseAI, BazaarLink, Completions, Enally, FreeTheAi, Command Code
- 🎯 **Manifest-aware tier routing W1W4** — provider manifests drive weighted tier selection
- 🎨 **Cursor full OpenAI parity** — tool calls, streaming, session management end-to-end
- 📊 **Cursor Pro plan usage** — quota & cycle data surfaced in the provider-limits dashboard
-**Service tier breakdown / Codex fast tier analytics** — per-tier consumption visibility
- 📌 **Per-session sticky routing** — Codex sessions pin to the same account between turns
- 🔊 **Inworld TTS enhancements** — voice catalogs, streaming, and latency improvements
- 🔑 **Kiro headless auth** — login via local `kiro-cli` SQLite store, no browser required
- 📉 **DeepSeek quota and limit monitoring** — daily/monthly usage exposed via dashboard
- 🔄 **Reset-aware routing strategy** — combos now prefer accounts whose quota window resets soonest
- ⏱️ **`fallbackDelayMs`** and **dynamic tool limit detection** — finer fallback timing + per-provider tool-count limits
- 🔧 **Background mode degradation (Responses API)** — falls back to synchronous mode with a structured warning when an upstream lacks background polling
- 🚦 **Per-provider 429 classification** + `useUpstream429BreakerHints` toggle — finer breaker behavior using upstream rate-limit hints
- 🩺 **Model cooldowns dashboard** — observe per-model lockouts and manually re-enable from the UI
- 🔒 **MITM dynamic Linux cert detection** — works across Debian/Ubuntu, Fedora/RHEL, Arch, and other distros
- 💻 **CLI enhancement suite** — 20+ commands including `omniroute providers`, `omniroute combos`, `omniroute doctor`, `omniroute setup`
- 🔍 **Qdrant embedding model discovery** — automatic vector-store model probe
- 🔑 **API Keys / Bearer keys with `manage` scope** — perform admin operations programmatically via API
- 🏥 **Combo target health analytics** + **structured combo builder** — per-target health & UI builder for assembling `(provider, model, connection)` steps
- 🤝 **GitLab Duo OAuth provider** — login with GitLab credentials
- 🧠 **Reasoning Replay Cache** — hybrid in-memory + SQLite persistence of reasoning traces
📚 **Related docs:** [Skills Framework](../frameworks/SKILLS.md) · [Memory System](../frameworks/MEMORY.md) · [Cloud Agents](../frameworks/CLOUD_AGENT.md) · [Webhooks](../frameworks/WEBHOOKS.md) · [Reasoning Replay Cache](../routing/REASONING_REPLAY.md)
---
## 🔌 Providers
Manage AI provider connections: OAuth providers (Claude Code, Codex), API key providers (Groq, DeepSeek, OpenRouter), and free providers (Qoder, Qwen, Kiro). Kiro accounts include credit balance tracking — remaining credits, total allowance, and renewal date visible in Dashboard → Usage.
OpenRouter connections can store a per-connection `preset` in Advanced Settings. When set, OmniRoute sends it as the OpenRouter top-level request field, for example `"preset": "email-copywriter"`, unless the client request already supplied its own `preset`.
![Providers Dashboard](../screenshots/01-providers.png)
---
## 🎨 Combos
Create model routing combos with 17 strategies: priority, weighted, fill-first, round-robin, p2c (power-of-two-choices), random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp (last-known-good-provider), context-optimized, context-relay, and **fusion** (fan out to a panel of models in parallel, then synthesize one answer via a judge). Each combo chains multiple models with automatic fallback and includes quick templates and readiness checks.
Recent combo improvements:
- **Structured combo builder** — create each step by selecting provider, model, and exact account/connection
- **Repeated provider support** — reuse the same provider many times in one combo as long as the `(provider, model, connection)` tuple is unique
- **Combo target health** — analytics and health surfaces now distinguish individual combo targets/steps instead of collapsing everything into model strings
- **Composite tier ordering** — `defaultTier -> fallbackTier` now influences runtime execution/fallback order for top-level combo steps
![Combos Dashboard](../screenshots/02-combos.png)
---
## 📊 Analytics
Comprehensive usage analytics with token consumption, cost estimates, activity heatmaps, weekly distribution charts, and per-provider breakdowns.
![Analytics Dashboard](../screenshots/03-analytics.png)
---
## 🏥 System Health
Real-time monitoring: uptime, memory, version, latency percentiles (p50/p95/p99), cache statistics, provider circuit breaker states, active quota-monitored sessions, and combo target health.
![Health Dashboard](../screenshots/04-health.png)
---
## 🔧 Translator Playground
Four modes for debugging API translations: **Playground** (format converter), **Chat Tester** (live requests), **Test Bench** (batch tests), and **Live Monitor** (real-time stream).
![Translator Playground](../screenshots/05-translator.png)
---
## 🎮 Model Playground _(v2.0.9+)_
Test any model directly from the dashboard. Select provider, model, and endpoint, write prompts with Monaco Editor, stream responses in real-time, abort mid-stream, and view timing metrics.
---
## 🎨 Themes _(v2.0.5+)_
Customizable color themes for the entire dashboard. Choose from 7 preset colors (Coral, Blue, Red, Green, Violet, Orange, Cyan) or create a custom theme by picking any hex color. Supports light, dark, and system mode.
---
## ⚙️ Settings
Comprehensive settings panel with **7 tabs**:
- **General** — System storage, backup management (export/import database)
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item and group separator visibility controls, Endpoint tunnel visibility controls
- **AI** — AI assistant features, default routing presets (Auto Combo `auto/coding`, `auto/fast`, `auto/cheap`, `auto/smart`), reasoning replay cache, and skill/memory toggles
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation, manifest-aware tier routing (W1W4), `fallbackDelayMs`, per-session sticky routing
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring, **Context Relay** handoff threshold and summary model configuration, per-provider 429 classification & `useUpstream429BreakerHints` toggle, model cooldowns
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode, background mode degradation for Responses API
![Settings Dashboard](../screenshots/06-settings.png)
---
## 🔧 CLI Tools
One-click configuration for AI coding tools: Claude Code, Codex CLI, OpenClaw, Kilo Code, Antigravity, Cline, Continue, Cursor, and Factory Droid. Features automated config apply/reset, connection profiles, and model mapping.
![CLI Tools Dashboard](../screenshots/07-cli-tools.png)
---
## 🤖 CLI Agents _(v2.0.11+)_
Dashboard for discovering and managing CLI agents. Shows a grid of 17 built-in agents (Codex, Claude, Goose, OpenClaw, Aider, OpenCode, Cline, Qwen Code, ForgeCode, Amazon Q, Open Interpreter, Cursor CLI, Warp, **Windsurf**, **Devin CLI**, **Kimi Coding**, **Command Code**) with:
- **Installation status** — Installed / Not Found with version detection
- **Protocol badges** — stdio, HTTP, etc.
- **Custom agents** — Register any CLI tool via form (name, binary, version command, spawn args)
- **CLI Fingerprint Matching** — Per-provider toggle to match native CLI request signatures, reducing ban risk while preserving proxy IP
- **OAuth-backed agents** — Windsurf & Devin CLI now use browser OAuth flows for authentication (v3.8.0+)
---
## 🔗 Context Relay _(v3.5.5+)_
A combo strategy that preserves session continuity when account rotation happens mid-conversation. Before the active account is exhausted, OmniRoute generates a structured handoff summary in the background. After the next request resolves to a different account, the summary is injected as a system message so the new account continues with full context.
Configurable via combo-level or global settings:
- **Handoff Threshold** — Quota usage percentage that triggers summary generation (default 85%)
- **Max Messages For Summary** — How much recent history to condense
- **Summary Model** — Optional override model for generating the handoff summary
Currently supports Codex account rotation. See [Context Relay documentation](../architecture/ARCHITECTURE.md).
---
## 🗜️ Prompt Compression _(v3.7.9+)_
Context & Cache now exposes dedicated pages for Caveman, RTK, and Compression Combos:
- **Caveman** — language-aware rule packs, preview, output-mode controls, and analytics
- **RTK** — command-aware compression for shell, git, test, build, package, Docker, infra, JSON, and stack-trace output
- **Compression Combos** — named pipelines such as `rtk -> caveman` assigned to routing combos; the default stacked math reaches `~89%` average and `78-95%` eligible-context savings when both engines apply
- **Raw-output recovery** — optional redacted RTK raw-output pointers for debugging compressed failures
See [Compression Guide](../compression/COMPRESSION_GUIDE.md), [RTK Compression](../compression/RTK_COMPRESSION.md), and
[Compression Engines](../compression/COMPRESSION_ENGINES.md).
---
## 🛡️ Proxy Hardening _(v3.5.5+)_
Comprehensive proxy configuration enforcement across the entire request pipeline:
- **Token Health Check** — Background OAuth refresh now resolves proxy config per connection, preventing failures in proxy-required environments
- **API Key Validation** — Provider key validation (`POST /api/providers/validate`) routes through `runWithProxyContext`, honoring provider-level and global proxy settings
- **undici Dispatcher Fix** — Proxy dispatchers use undici's own fetch implementation instead of Node's built-in fetch, resolving `invalid onRequestStart method` errors on Node.js 22
- **Node.js Version Detection** — Login page proactively detects incompatible Node.js versions (24+) and displays a warning banner with instructions to use Node 22 LTS
---
## 📧 Email Privacy Masking _(v3.5.6+)_
OAuth account emails are masked by default (e.g. `di*****@g****.com`) to prevent accidental exposure when sharing screenshots or recording demos. Use Settings → Appearance → Account email visibility to reveal or mask full account emails globally across providers, combos, logs, quota, and playground screens.
---
## 👁️ Model Visibility Toggle _(v3.5.6+)_
The provider page model list now includes:
- **Real-time search/filter bar** — Quickly find specific models
- **Per-model visibility toggle** (👁 icon) — Hidden models are grayed out and excluded from the `/v1/models` catalog
- **Active-count badge** (`N/M active`) — Shows at a glance how many models are enabled vs total
---
## 🔧 OAuth Env Repair _(v3.6.1+)_
One-click "Repair env" action for OAuth providers that restores missing environment variables and fixes broken auth state. Accessible from `Dashboard → Providers → [OAuth Provider] → Repair env`. Automatically detects and repairs:
- Missing OAuth client credentials
- Corrupted env file entries
- Backup path sanitization
---
## 🗑️ Uninstall / Full Uninstall _(v3.6.2+)_
Clean removal scripts for all installation methods:
| Command | Action |
| ------------------------ | ----------------------------------------------------------------------------------- |
| `npm run uninstall` | Removes the system app but **keeps your DB and configurations** in `~/.omniroute`. |
| `npm run uninstall:full` | Removes the app AND permanently **erases all configurations, keys, and databases**. |
---
## 🖼️ Media _(v2.0.3+)_
Generate images, videos, and music from the dashboard. Supports OpenAI, xAI, Together, Hyperbolic, SD WebUI, ComfyUI, AnimateDiff, Stable Audio Open, and MusicGen.
---
## 📝 Request Logs
Real-time request logging with filtering by provider, model, account, and API key. Shows status codes, token usage, latency, and response details.
![Usage Logs](../screenshots/08-usage.png)
---
## 🌐 API Endpoint
Your unified API endpoint with capability breakdown: Chat Completions, Responses API, Embeddings, Image Generation, Reranking, Audio Transcription, Text-to-Speech, Moderations, and registered API keys. Cloudflare Quick Tunnel, Tailscale Funnel, ngrok Tunnel, and cloud proxy support are available for remote access.
![Endpoint Dashboard](../screenshots/09-endpoint.png)
---
## 🔑 API Key Management
Create, scope, and revoke API keys. Each key can be restricted to specific models/providers with full access or read-only permissions. Visual key management with usage tracking.
---
## 📋 Audit Log
Administrative action tracking with filtering by action type, actor, target, IP address, and timestamp. Full security event history.
---
## 🖥️ Desktop Application
Native Electron desktop app for Windows, macOS, and Linux. Run OmniRoute as a standalone application with system tray integration, offline support, auto-update, and one-click install.
Key features:
- Server readiness polling (no blank screen on cold start)
- System tray with port management
- Content Security Policy
- Single-instance lock
- Auto-update on restart
- Platform-conditional UI (macOS traffic lights, Windows/Linux default titlebar)
- Hardened Electron build packaging — symlinked `node_modules` in the standalone bundle is detected and rejected before packaging, preventing runtime dependency on the build machine (v2.5.5+)
- **Graceful shutdown** — Electron `before-quit` shuts down Next.js cleanly, preventing SQLite WAL database locks (v3.6.2+)
📖 See [`electron/README.md`](../../electron/README.md) for full documentation.
---
## 🌐 V1 WebSocket Bridge _(v3.6.6+)_
OmniRoute now supports **OpenAI-compatible WebSocket clients** via the `/v1/ws` upgrade endpoint. The custom `scripts/dev/v1-ws-bridge.mjs` server wraps Next.js and upgrades WS connections to full bidirectional streaming sessions. Authentication uses the same API key or session cookie as HTTP requests.
Key behaviours:
- WS upgrade validated by `src/lib/ws/handshake.ts` before the connection is established
- Streams terminated cleanly on session close or upstream error
- Works alongside the existing HTTP+SSE streaming path simultaneously
---
## 🔑 Sync Tokens & Config Bundle _(v3.6.6+)_
Multi-device and external operator access is now possible via **scoped sync tokens**:
- **`POST /api/sync/tokens`** — Issue a new sync token (scoped, with optional expiry)
- **`DELETE /api/sync/tokens/:id`** — Revoke a token
- **`GET /api/sync/bundle`** — Download a versioned, ETag-keyed JSON snapshot of all non-sensitive settings (passwords redacted)
The config bundle is built by `src/lib/sync/bundle.ts`. Consumers compare the `ETag` response header to detect changes without re-downloading the full payload.
---
## 🧠 GLM Thinking Preset _(v3.6.6+)_
**GLM Thinking (`glmt`)** is now a registered first-class provider: 65 536 max output tokens, 24 576 thinking budget, 900 s default timeout, Claude-compatible API format, and shared usage sync with the GLM family.
**Hybrid token counting** also lands in v3.6.6: when a Claude-compatible provider exposes `/messages/count_tokens`, OmniRoute calls it before large requests with graceful estimation fallback.
---
## 🛡️ Safe Outbound Fetch & SSRF Guard _(v3.6.6+)_
All provider validation and model discovery calls now go through a two-layer outbound guard:
1. **URL guard** (`src/shared/network/outboundUrlGuard.ts`) — Blocks private/loopback/link-local IP ranges before the socket is opened.
2. **Safe fetch wrapper** (`src/shared/network/safeOutboundFetch.ts`) — Applies the URL guard, normalises timeouts, and retries transient errors with exponential backoff.
Guard violations surface as HTTP 422 (`URL_GUARD_BLOCKED`) and are written to the compliance audit log via `providerAudit.ts`.
---
## 🔄 Cooldown-Aware Retries _(v3.6.6+)_
Chat requests now **automatically retry** when an upstream provider returns a model-scoped cooldown. Configurable via `REQUEST_RETRY` (default: 2) and `MAX_RETRY_INTERVAL_SEC` (default: 30 s). Rate-limit header learning improved across `x-ratelimit-reset-requests`, `x-ratelimit-reset-tokens`, and `Retry-After` — per-model cooldown state is visible in the Resilience dashboard.
---
## 📋 Compliance Audit v2 _(v3.6.6+)_
The audit log has been expanded with cursor-based pagination, request context enrichment (request ID, user agent, IP), structured auth events, provider CRUD events with diff context, and SSRF-blocked validation logging. New events emitted by `src/lib/compliance/providerAudit.ts`.
+253
View File
@@ -0,0 +1,253 @@
---
title: "Free Provider Rankings (Arena ELO)"
version: 3.8.40
lastUpdated: 2026-06-28
---
# Free Provider Rankings (Arena ELO)
> **TL;DR**: OmniRoute ranks its **free** providers by model quality using **Arena AI
> (LMArena-style) ELO scores**. Open the **Free Provider Rankings** page in the
> dashboard to see which free providers ship the strongest models for your task —
> overall, or filtered by category (coding, review, documentation, debugging).
---
## What It Is
OmniRoute aggregates 160+ providers, many of which expose a **free tier** (no-auth,
free-tier OAuth, or free-tier API key — see the
[Free Tiers Guide](../getting-started/FREE-TIERS-GUIDE.md) and the full
[Free Tiers directory](../reference/FREE_TIERS.md)). The catch: free providers vary
wildly in model quality. A no-auth provider serving a frontier model is far more useful
than one serving a small legacy model.
**Free Provider Rankings** answers "**which free provider gives me the best model?**" by
joining each free provider's catalog with **crowd-sourced quality scores** from the
**Arena AI leaderboard** (human-preference ELO, the same idea behind the LMArena
chatbot arena). Providers are then ranked by the strength of their **best free model**.
The ranking is computed from three real sources:
1. The free-provider lists — `NOAUTH_PROVIDERS`, plus `OAUTH_PROVIDERS` /
`APIKEY_PROVIDERS` entries flagged `hasFree`
(`src/shared/constants/providers.ts`).
2. Each provider's model catalog from the provider registry
(`open-sse/config/providerRegistry.ts`).
3. ELO-derived task-fit scores stored in the `model_intelligence` DB table by the
Arena ELO sync engine (`src/lib/arenaEloSync.ts`).
The join logic lives in `src/lib/freeProviderRankings.ts`.
---
## How to Access
### Dashboard page
Open the dashboard and go to **Costs → Free Provider Rankings**, or navigate directly to:
```
/dashboard/free-provider-rankings
```
The page (`src/app/(dashboard)/dashboard/free-provider-rankings/page.tsx`) shows:
- A **top-3 podium** (🥇 🥈 🥉) of the best-ranked free providers.
- A full **ranking table** with columns: **Rank**, **Provider**, **Top Model**,
**Score**, **Avg Score**, **Models**, **Type**.
- **Category filter buttons**: _All Categories_, _Default_, _Coding_, _Review_,
_Documentation_, _Debugging_.
Each provider's **Type** badge tells you how it is free:
| Badge | Meaning |
| -------- | --------------------------------------------- |
| `NOAUTH` | Always free, no credentials needed |
| `OAUTH` | OAuth provider with a free tier (`hasFree`) |
| `APIKEY` | API-key provider with a free tier (`hasFree`) |
Scores are shown as human-readable labels (e.g. _Elite_, _Excellent_, _Very Good_,
_Good_, _Average_) rather than raw numbers, because the underlying value is a relative
ranking quality, not a percentage.
### API endpoint
The page is backed by a public read endpoint
(`src/app/api/free-provider-rankings/route.ts`):
```
GET /api/free-provider-rankings
GET /api/free-provider-rankings?category=coding
GET /api/free-provider-rankings?category=coding&limit=20
```
Query parameters (validated with Zod):
| Param | Type | Default | Notes |
| ---------- | ------ | ------- | -------------------------------------------------------------------------------------------------- |
| `category` | string | (none) | One of `default`, `coding`, `review`, `documentation`, `debugging`. Omit for the combined ranking. |
| `limit` | number | `50` | Clamped to the range `1100`. |
Response shape:
```json
{
"rankings": [
{
"id": "<provider-id>",
"name": "<provider name>",
"icon": "<icon>",
"color": "<hex color>",
"textIcon": "<short label>",
"category": "noauth | oauth | apikey",
"topModel": {
"modelId": "<registry model id>",
"modelName": "<model display name>",
"score": 0.0,
"eloRaw": 0,
"confidence": "high | medium | low",
"category": "<task category>"
},
"averageScore": 0.0,
"modelCount": 0
}
]
}
```
`eloRaw` is the original Arena ELO value; `score` is the normalized task-fit value
(see below). Providers with no scored models are omitted from the response.
---
## How the Scores Work
### Source: Arena AI leaderboard
The Arena ELO sync engine (`src/lib/arenaEloSync.ts`) fetches two leaderboards — `text`
and `code` — from the Arena AI leaderboard API
(`https://api.wulong.dev/arena-ai-leaderboards/v1/leaderboard`). Each leaderboard entry
carries a model name, vendor, ELO `score`, confidence interval, and vote count.
Leaderboard categories map to OmniRoute task categories:
| Arena leaderboard | OmniRoute task categories |
| ----------------- | ------------------------------------------------- |
| `text` | `default`, `review`, `documentation`, `debugging` |
| `code` | `coding` |
### Normalization (task-fit score)
Raw ELO scores are normalized per leaderboard into a **task-fit value in `[0.4, 0.98]`**:
```
taskFit = 0.4 + 0.58 * ((elo - minElo) / (maxElo - minElo))
```
The score never reaches `0` or `1`, leaving headroom for user overrides. This is the
`score` field you see in the API response and the label shown on the dashboard.
### Confidence
Each entry gets a confidence level based on Arena vote count:
| Confidence | Votes |
| ---------- | ------- |
| `high` | ≥ 5,000 |
| `medium` | ≥ 1,000 |
| `low` | < 1,000 |
### Storage and freshness
Normalized entries are written to the `model_intelligence` DB table with
`source = "arena_elo"` (`src/lib/db/modelIntelligence.ts`). Entries **expire after
7 days**, so a provider that stops syncing eventually drops out rather than serving
stale data.
The sync runs **on by default**:
- It runs once at server startup and then on a periodic timer
(`src/lib/arenaEloSync.ts`, wired from `src/server-init.ts`).
- It is **non-blocking and never fatal** — if the upstream fetch fails, OmniRoute keeps
running and the rankings simply show the last good data (or an empty state).
Two environment variables control it (documented in
[`docs/reference/ENVIRONMENT.md`](../reference/ENVIRONMENT.md)):
| Variable | Default | Purpose |
| ------------------------- | ------------- | ----------------------------------------------- |
| `ARENA_ELO_SYNC_ENABLED` | `true` | Set to `false` to opt out of the outbound sync. |
| `ARENA_ELO_SYNC_INTERVAL` | `86400` (24h) | Sync interval, in seconds. |
### Manual sync / status / clear
For operators, an authenticated management endpoint exposes manual control
(`src/app/api/intelligence/sync/route.ts` — requires management auth):
```
GET /api/intelligence/sync # current sync status (enabled, lastSync, nextSync, intervalMs)
POST /api/intelligence/sync # trigger a manual sync; body: { "dryRun": true } to preview without writing
DELETE /api/intelligence/sync # clear all synced arena_elo intelligence entries
```
If the rankings page is empty, a manual `POST /api/intelligence/sync` (or simply
restarting the server) repopulates it.
### Matching models to the leaderboard
Registry model IDs and Arena model names don't always match exactly. The ranking uses
flexible matching (`findMatchingIntelligence` in `src/lib/freeProviderRankings.ts`):
1. Exact match on the normalized model ID.
2. Match after stripping a trailing version suffix (e.g. `kimi-k2.6``kimi-k2`).
3. Prefix match (a leaderboard model name is a prefix of the registry ID).
On the sync side, known vendor prefixes (`anthropic/`, `openai/`, `google/`, …) are
stripped and a small alias map expands canonical names into the variants OmniRoute uses
internally, so models stay findable under any name.
### How a provider is ranked
For each free provider, the engine scores every model in its catalog, then:
- **Top Model** = the provider's highest-scoring model.
- **Avg Score** = the mean score across all of that provider's scored models.
- **Models** = how many of the provider's models had an Arena score.
Providers are sorted by **top-model score** first, then by average score. This rewards a
provider that ships at least one strong free model.
---
## Using It to Choose Free Providers
1. **Pick the right category.** Use the **Coding** filter for agentic/code workloads, or
leave it on **All Categories** / **Default** for general chat. The same provider can
rank differently across categories because its top model differs per leaderboard.
2. **Prefer the podium for one-shot setups.** If you only want to connect one or two free
providers, start with the top-ranked ones for your category.
3. **Check the Type badge.** `NOAUTH` providers are the fastest to connect (no
credentials). `OAUTH` / `APIKEY` free tiers need a quick sign-up but often expose
stronger models. See [Free Tiers Guide](../getting-started/FREE-TIERS-GUIDE.md) for
connection steps.
4. **Connect several and let Auto-Combo decide.** The same Arena ELO data that powers
this page also feeds the **task-fitness factor** of the Auto-Combo scoring engine
(`open-sse/services/autoCombo/taskFitness.ts`, resolution order
`user_override → arena_elo → models_dev_tier → static table`). So after you connect
the top free providers, routing with `model: "auto"` (e.g. `auto/coding`) will
automatically prefer the higher-quality free models per request. See
[Auto-Combo](../routing/AUTO-COMBO.md) for the full 9-factor scoring.
---
## Related Documentation
- [Free Tiers Guide](../getting-started/FREE-TIERS-GUIDE.md) — how to connect free
providers, no credit card required.
- [Free Tiers directory](../reference/FREE_TIERS.md) — full catalog of free providers
and their limits.
- [Auto-Combo](../routing/AUTO-COMBO.md) — the 9-factor routing engine that consumes the
same Arena ELO task-fitness data.
- [Environment variables](../reference/ENVIRONMENT.md) — `ARENA_ELO_SYNC_ENABLED` /
`ARENA_ELO_SYNC_INTERVAL` reference.
+578
View File
@@ -0,0 +1,578 @@
---
title: "i18n — Internationalization Guide"
version: 3.8.40
lastUpdated: 2026-06-28
---
# i18n — Internationalization Guide
OmniRoute supports **42 languages** with full dashboard UI translation, translated documentation, and RTL support for Arabic and Hebrew.
🌐 **Languages:** 🇺🇸 [English](./I18N.md) | 🇧🇷 [Português (Brasil)](../i18n/pt-BR/docs/guides/I18N.md) | 🇪🇸 [Español](../i18n/es/docs/guides/I18N.md) | 🇫🇷 [Français](../i18n/fr/docs/guides/I18N.md) | 🇩🇪 [Deutsch](../i18n/de/docs/guides/I18N.md) | 🇮🇹 [Italiano](../i18n/it/docs/guides/I18N.md) | 🇷🇺 [Русский](../i18n/ru/docs/guides/I18N.md) | 🇨🇳 [中文 (简体)](../i18n/zh-CN/docs/guides/I18N.md) | 🇯🇵 [日本語](../i18n/ja/docs/guides/I18N.md) | 🇰🇷 [한국어](../i18n/ko/docs/guides/I18N.md) | 🇸🇦 [العربية](../i18n/ar/docs/guides/I18N.md) | 🇮🇳 [हिन्दी](../i18n/hi/docs/guides/I18N.md) | 🇹🇭 [ไทย](../i18n/th/docs/guides/I18N.md) | 🇹🇷 [Türkçe](../i18n/tr/docs/guides/I18N.md) | 🇺🇦 [Українська](../i18n/uk-UA/docs/guides/I18N.md) | 🇻🇳 [Tiếng Việt](../i18n/vi/docs/guides/I18N.md) | 🇧🇬 [Български](../i18n/bg/docs/guides/I18N.md) | 🇩🇰 [Dansk](../i18n/da/docs/guides/I18N.md) | 🇫🇮 [Suomi](../i18n/fi/docs/guides/I18N.md) | 🇮🇱 [עברית](../i18n/he/docs/guides/I18N.md) | 🇭🇺 [Magyar](../i18n/hu/docs/guides/I18N.md) | 🇮🇩 [Bahasa Indonesia](../i18n/id/docs/guides/I18N.md) | 🇲🇾 [Bahasa Melayu](../i18n/ms/docs/guides/I18N.md) | 🇳🇱 [Nederlands](../i18n/nl/docs/guides/I18N.md) | 🇳🇴 [Norsk](../i18n/no/docs/guides/I18N.md) | 🇵🇹 [Português (Portugal)](../i18n/pt/docs/guides/I18N.md) | 🇷🇴 [Română](../i18n/ro/docs/guides/I18N.md) | 🇵🇱 [Polski](../i18n/pl/docs/guides/I18N.md) | 🇸🇰 [Slovenčina](../i18n/sk/docs/guides/I18N.md) | 🇸🇪 [Svenska](../i18n/sv/docs/guides/I18N.md) | 🇵🇭 [Filipino](../i18n/phi/docs/guides/I18N.md) | 🇨🇿 [Čeština](../i18n/cs/docs/guides/I18N.md)
## Translation pipeline (recommended — v3.8.0)
OmniRoute uses a hash-based incremental translator for docs, backed by an
OpenAI-compatible LLM endpoint (typically `cx/gpt-5.4-mini` through OmniRoute
Cloud):
```bash
# Run translations (incremental — only touches changed sources)
npm run i18n:run
# Limit to one locale
npm run i18n:run -- --locale=pt-BR
# Specific files (comma-separated, repo-relative paths)
npm run i18n:run -- --files=CLAUDE.md,docs/architecture/ARCHITECTURE.md
# Force retranslate everything (expensive)
npm run i18n:run -- --force
# Preview what would happen (no API calls, no writes)
npm run i18n:run:dry
# CI gate — exits non-zero if state is drifting
npm run i18n:check
```
**Source of truth.** `config/i18n.json` lists every locale (UI + docs) plus
the RTL set and the `docsExcluded` codes. The runtime config in
`src/i18n/config.ts` is a thin adapter over that JSON.
**Backend.** Configured via env (set in `.env`, never committed):
| Variable | Purpose |
| ----------------------------------- | --------------------------------------- |
| `OMNIROUTE_TRANSLATION_API_URL` | OpenAI-compatible base URL, e.g. `…/v1` |
| `OMNIROUTE_TRANSLATION_API_KEY` | bearer token (kept out of logs) |
| `OMNIROUTE_TRANSLATION_MODEL` | model id, e.g. `cx/gpt-5.4-mini` |
| `OMNIROUTE_TRANSLATION_TIMEOUT_MS` | optional, default `60000` |
| `OMNIROUTE_TRANSLATION_CONCURRENCY` | optional, default `4` |
**State tracking.** `.i18n-state.json` (committed) keeps SHA-256 hashes per
source + per locale. Drift detection is automatic and deterministic — no API
calls in `i18n:check`.
**Output shape.** Each translated file gets a top-level `# <heading>
(<native>)` line, a `🌐 Languages: …` bar, an `---` separator, and the
translated body. That layout matches what `scripts/check/check-docs-sync.mjs`
already enforces for `llm.txt` and `CHANGELOG.md` mirrors.
### Legacy scripts (deprecated)
The older Python script (`scripts/i18n/i18n_autotranslate.py`) and the
Google-Translate-backed generator (`scripts/i18n/generate-multilang.mjs`)
still exist with a deprecation banner. They will be removed in v3.10. The
`messages` and `readme` modes of `generate-multilang.mjs` (UI strings + root
README variants) are not yet handled by the new pipeline and are still used.
## Quick Reference
| Task | Command |
| ----------------------- | ---------------------------------------------------------- |
| Translate docs (LLM) | `npm run i18n:run` (preferred — incremental, hash-based) |
| Translate UI strings | `node scripts/i18n/generate-multilang.mjs messages` |
| Check translation drift | `npm run i18n:check` |
| Validate a locale | `python3 scripts/i18n/validate_translation.py quick -l cs` |
| Check code keys | `python3 scripts/i18n/check_translations.py` |
| Generate QA report | `node scripts/i18n/generate-qa-checklist.mjs` |
| Visual QA (Playwright) | `node scripts/i18n/run-visual-qa.mjs` |
## Architecture
![Incremental hash-based i18n pipeline](../diagrams/exported/i18n-flow.svg)
> Source: [diagrams/i18n-flow.mmd](../diagrams/i18n-flow.mmd)
### Source of Truth
- **UI strings**: `src/i18n/messages/en.json` (English source, ~2800 keys)
- **Locale files**: `src/i18n/messages/{locale}.json` (30 translations)
- **Framework**: `next-intl` with cookie-based locale resolution
- **Config**: `src/i18n/config.ts` — defines all 30 locales, language names, flags
### Runtime Flow
1. User selects language → `NEXT_LOCALE` cookie set
2. `src/i18n/request.ts` resolves locale: cookie → `Accept-Language` header → fallback `en`
3. Dynamic import loads `messages/{locale}.json`
4. Components use `useTranslations("namespace")` and `t("key")`
### Supported Locales
| Code | Language | RTL | Google Translate Code |
| ------- | -------------------- | --- | --------------------- |
| `ar` | العربية | Yes | `ar` |
| `bg` | Български | No | `bg` |
| `cs` | Čeština | No | `cs` |
| `da` | Dansk | No | `da` |
| `de` | Deutsch | No | `de` |
| `es` | Español | No | `es` |
| `fi` | Suomi | No | `fi` |
| `fr` | Français | No | `fr` |
| `he` | עברית | Yes | `iw` |
| `hi` | हिन्दी | No | `hi` |
| `hu` | Magyar | No | `hu` |
| `id` | Bahasa Indonesia | No | `id` |
| `it` | Italiano | No | `it` |
| `ja` | 日本語 | No | `ja` |
| `ko` | 한국어 | No | `ko` |
| `ms` | Bahasa Melayu | No | `ms` |
| `nl` | Nederlands | No | `nl` |
| `no` | Norsk | No | `no` |
| `phi` | Filipino | No | `tl` |
| `pl` | Polski | No | `pl` |
| `pt` | Português (Portugal) | No | `pt` |
| `pt-BR` | Português (Brasil) | No | `pt` |
| `ro` | Română | No | `ro` |
| `ru` | Русский | No | `ru` |
| `sk` | Slovenčina | No | `sk` |
| `sv` | Svenska | No | `sv` |
| `th` | ไทย | No | `th` |
| `tr` | Türkçe | No | `tr` |
| `uk-UA` | Українська | No | `uk` |
| `vi` | Tiếng Việt | No | `vi` |
| `zh-CN` | 中文 (简体) | No | `zh-CN` |
## Adding a New Language
### 1. Register the Locale
Edit `src/i18n/config.ts`:
```ts
// Add to LOCALES array
"xx",
// Add to LANGUAGES array
{ code: "xx", label: "XX", name: "Language Name", flag: "🏳️" },
```
### 2. Add to Generator
Edit `scripts/i18n/generate-multilang.mjs` — add entry to `LOCALE_SPECS`:
```js
{
code: "xx",
googleTl: "xx",
label: "XX",
flag: "🏳️",
languageName: "Language Name",
readmeName: "Language Name",
docsName: "Language Name",
},
```
### 3. Generate Initial Translation
```bash
node scripts/i18n/generate-multilang.mjs messages
```
This creates `src/i18n/messages/xx.json` auto-translated from `en.json` via Google Translate.
### 4. Review & Fix Auto-Translations
Auto-translations are a starting point. Review manually for:
- Technical accuracy
- Context-appropriate terminology
- Proper handling of placeholders (`{count}`, `{value}`, etc.)
### 5. Validate
```bash
python3 scripts/i18n/validate_translation.py quick -l xx
python3 scripts/i18n/validate_translation.py diff common -l xx
```
### 6. Generate Translated Documentation
```bash
node scripts/i18n/generate-multilang.mjs docs
```
## Auto-Translation Pipeline
### generate-multilang.mjs (Google Translate)
**Primary auto-translation engine** — uses Google Translate free API to generate translations for UI strings, READMEs, and documentation.
```bash
node scripts/i18n/generate-multilang.mjs [messages|readme|docs|all]
```
| Mode | What it does |
| ---------- | ----------------------------------------------------------------------------- |
| `messages` | Translates missing keys in `src/i18n/messages/{locale}.json` from `en.json` |
| `readme` | Translates `README.md` into all locales as `README.{code}.md` in project root |
| `docs` | Translates `DOC_SOURCE_FILES` into `docs/i18n/{locale}/{docName}` |
| `all` | Runs all three modes |
**Features:**
- **Text protection**: Masks code blocks (` ``` `), inline code (`` ` ``), markdown links/images (`[text](url)`), HTML tags, tables, and ICU placeholders (`{count}`, `{value}`, `{total}`, etc.) before translation, then restores them
- **Chunked batching**: Joins multiple strings with `__OMNIROUTE_I18N_SEPARATOR__` delimiters to minimize API calls (max 1800 chars per request)
- **In-memory cache**: Avoids redundant API calls for repeated strings within a session
- **Retry logic**: Exponential backoff (up to 5 attempts with 300ms × attempt delay) for 429/5xx errors
- **Timeout**: 20 seconds per request
- **Skip existing**: If target file already exists, it is NOT overwritten
**Important behaviors:**
- `docs/i18n/README.md` is **regenerated** each run — it's an auto-generated index of all docs
- Root `README.{code}.md` files are only created if they don't exist (skips locales in `EXISTING_README_CODES`)
- Language bars (`🌐 **Languages:** ...`) are automatically inserted/updated in all translated docs
### i18n_autotranslate.py (LLM-based)
**Secondary translator** — uses any OpenAI-compatible LLM API (including OmniRoute itself) to translate existing `docs/i18n/` markdown files. Best for polishing or re-translating docs with better quality than Google Translate.
```bash
python3 scripts/i18n/i18n_autotranslate.py \
--api-url http://localhost:20128/v1 \
--api-key sk-your-key \
--model gpt-4o
```
**Features:**
- Scans `docs/i18n/` markdown files for English paragraphs
- Skips code blocks, tables, and already-translated content
- Sends paragraphs to LLM with technical translation system prompt
- Supports all 42 languages
## CLI i18n
The `omniroute` CLI has its own i18n layer separate from the Next.js dashboard.
### How it works
- Every user-facing string in CLI commands goes through `t("module.key", vars)` from `bin/cli/i18n.mjs`.
- Catalogs are JSON files in `bin/cli/locales/` — 42 ship out-of-the-box.
- Locale falls back to `en` for any missing key, so partial translations are valid.
- The source of truth for available locales is `config/i18n.json` (shared with the dashboard).
### Locale selection
Detection order (first match wins):
| Priority | Source | Example |
| -------- | ------------------------ | --------------------------------------- |
| 1 | `--lang` flag | `omniroute --lang de status` |
| 2 | `OMNIROUTE_LANG` env var | `OMNIROUTE_LANG=ja omniroute providers` |
| 3 | `LC_ALL` system env | auto-detected from terminal locale |
| 4 | `LC_MESSAGES` system env | auto-detected from terminal locale |
| 5 | `LANG` system env | auto-detected from terminal locale |
| 6 | Fallback | `en` |
Locale codes with underscores (`pt_BR`) are normalized to hyphen form (`pt-BR`).
Locale codes are validated against `/^[a-zA-Z0-9-]+$/` — path traversal is rejected.
### Saving a language preference
```bash
# Set language and save to ~/.omniroute/.env (persists across sessions)
omniroute config lang set pt-BR
# View current language
omniroute config lang get
# List all 42 available languages
omniroute config lang list
# JSON output
omniroute config lang list --output json
```
The saved preference is written atomically to `~/.omniroute/.env` and is loaded by the
CLI bootstrap before any command runs.
### One-time override
```bash
# Override for one command only (not persisted)
omniroute --lang de providers list
```
Note: the `--lang` flag does NOT write to the env file — it only affects the current
invocation. Use `config lang set` to persist.
### Available locales
42 locale files ship in `bin/cli/locales/`. Full translations: `en`, `pt-BR`.
Scaffold-only (all keys fall back to `en`): `bn`, `gu`, `he`, `in`, `mr`, `ms`, `phi`, `sw`, `ta`, `te`, `ur`.
All other 29 locales have `common` + `program` keys translated.
### Adding a new CLI locale
1. Add the locale entry to `config/i18n.json`.
2. Run `node bin/cli/scripts/generate-locales.mjs` — creates the locale file.
3. Translate the keys (or leave as `{}` for en-fallback scaffold).
4. PRs must add strings to `en.json` and `pt-BR.json`; other files are best-effort.
## Validation & QA
### validate_translation.py
**Translation validator** — compares any locale JSON against `en.json` and reports issues.
```bash
# Quick check (counts only)
python3 scripts/i18n/validate_translation.py quick -l cs
# Output:
# Missing: 0
# Untranslated: 0
# Ignored (UNTRANSLATABLE_KEYS): 236
# Detailed diff by category
python3 scripts/i18n/validate_translation.py diff common -l cs
python3 scripts/i18n/validate_translation.py diff settings -l cs
# Export to CSV
python3 scripts/i18n/validate_translation.py csv -l cs > report.csv
# Export to Markdown
python3 scripts/i18n/validate_translation.py md -l cs > report.md
# Full report (default)
python3 scripts/i18n/validate_translation.py -l cs
```
**Detects:**
- **Missing keys** — keys in `en.json` but not in locale file
- **Extra keys** — keys in locale file but not in `en.json`
- **Untranslated keys** — keys where locale value equals English source (excluding allowlist)
- **Placeholder mismatches** — ICU placeholders that don't match between source and translation
**Exit codes:**
| Code | Meaning |
|------|---------|
| 0 | OK |
| 1 | Generic error |
| 2 | Missing strings (hard error) |
| 3 | Untranslated warning (soft) |
**Environment:** Set `TRANSLATION_LANG=cs` or use `-l cs` flag.
### check_translations.py
**Code-to-JSON key checker** — scans `src/**/*.tsx` and `src/**/*.ts` for `useTranslations()` calls and verifies all referenced keys exist in `en.json`.
```bash
# Basic check
python3 scripts/i18n/check_translations.py
# Verbose output
python3 scripts/i18n/check_translations.py --verbose
# Auto-fix (adds missing keys to en.json)
python3 scripts/i18n/check_translations.py --fix
```
### generate-qa-checklist.mjs
**Static analysis QA** — scans Next.js page files for i18n risk metrics and generates a Markdown report.
```bash
node scripts/i18n/generate-qa-checklist.mjs
```
**Checks:**
- Fixed-width class usage (overflow risk)
- Directional left/right classes (RTL risk)
- Clipping-prone patterns
- Locale parity (missing/extra keys vs `en.json`)
- README language selector bars in priority locales (`es`, `fr`, `de`, `ja`, `ar`)
**Output:** `docs/reports/i18n-qa-checklist-{date}.md`
### run-visual-qa.mjs
**Visual QA via Playwright** — takes screenshots of all dashboard routes in multiple locales and viewports, then evaluates page health.
```bash
# Default: es, fr, de, ja, ar on localhost:20128
node scripts/i18n/run-visual-qa.mjs
# Custom base URL and locales
QA_BASE_URL=http://staging.example.com QA_LOCALES=de,fr node scripts/i18n/run-visual-qa.mjs
# Custom routes
QA_ROUTES=/dashboard/settings,/dashboard/providers node scripts/i18n/run-visual-qa.mjs
```
**Detects:**
- Text overflow
- Element clipping
- RTL layout mismatches
**Output:** `docs/reports/i18n-visual-qa-{date}.md` + JSON report
## Managing Untranslatable Keys
### untranslatable-keys.json
**File:** `scripts/i18n/untranslatable-keys.json`
Allowlist of keys that should remain identical to English source. Used by `validate_translation.py` to avoid false-positive "untranslated" warnings.
```json
{
"description": "Keys that should remain untranslated...",
"keys": [
"common.model",
"common.oauth",
"health.cpu",
...
]
}
```
**What belongs here:**
- Brand/product names: `landing.brandName`, `common.social-github`
- Technical terms/acronyms: `health.cpu`, `mcpDashboard.pid`, `settings.ai`
- ICU/format strings: `apiManager.modelsCount`, `health.millisecondsShort`
- Placeholder values: `providers.openaiBaseUrlPlaceholder`, `cliTools.baseUrlPlaceholder`
- Protocol names: `common.http`, `common.oauth`, `providers.oauth2Label`
- Navigation sections: `sidebar.primarySection`, `sidebar.cliSection`
**To add a key:** Edit the `keys` array in `scripts/i18n/untranslatable-keys.json` and re-run validation.
## CI Integration
### GitHub Actions (`.github/workflows/ci.yml`)
The CI pipeline validates all locales on every push and PR:
1. **`i18n-matrix` job** — dynamically discovers all locale files (excluding `en.json`)
2. **`i18n` job** — runs `validate_translation.py quick -l '<lang>'` for each locale in parallel
3. **`ci-summary` job** — aggregates results into a dashboard summary
```yaml
# i18n-matrix: discovers languages
LANGS=$(ls src/i18n/messages/*.json | xargs -n1 basename | sed 's/.json$//' | grep -v '^en$')
# i18n: validates each language
python3 scripts/i18n/validate_translation.py quick -l '${{ matrix.lang }}'
```
**Dashboard output:**
```
## 🌍 Translations
| Metric | Value |
|--------|------|
| Languages checked | 30 |
| Total untranslated | 0 |
✅ All translations complete
```
## File Structure
```
src/i18n/
├── config.ts # Locale definitions (30 locales, RTL config)
├── request.ts # Runtime locale resolution
└── messages/
├── en.json # Source of truth (~2800 keys)
├── cs.json # Czech translation
├── de.json # German translation
└── ... # 30 locale files total
scripts/
├── i18n/
│ ├── generate-multilang.mjs # Auto-translation engine (Google Translate, 888 lines)
│ ├── generate-qa-checklist.mjs # Static analysis QA
│ ├── run-visual-qa.mjs # Playwright visual QA
│ └── untranslatable-keys.json # Allowlist for validation (236 keys)
├── validate_translation.py # Translation validator
├── check_translations.py # Code-to-JSON key checker
└── i18n_autotranslate.py # LLM-based doc translator
.github/workflows/
└── ci.yml # i18n validation in CI matrix
docs/
├── I18N.md # This file — i18n toolchain documentation
├── i18n/
│ ├── README.md # Auto-generated language index
│ ├── cs/ # Czech docs
│ │ └── docs/
│ │ ├── I18N.md # Czech translation of this file
│ │ └── ...
│ ├── de/ # German docs
│ └── ... # 30 locale directories
└── reports/
├── i18n-qa-checklist-*.md # Static analysis reports
└── i18n-visual-qa-*.md # Visual QA reports
```
## Best Practices
### When Editing Translations
1. **Always edit `en.json` first** — it's the source of truth
2. **Run `generate-multilang.mjs messages`** to propagate new keys to all locales
3. **Review auto-translations** — Google Translate is a starting point, not final
4. **Validate before committing**`python3 scripts/i18n/validate_translation.py quick -l <lang>`
5. **Update `untranslatable-keys.json`** if a key should remain in English
### Placeholder Safety
- ICU placeholders (`{count}`, `{value}`, `{total}`, `{seconds}`) must be preserved exactly
- Plural formats (`{count, plural, one {# model} other {# models}}`) must maintain structure
- The validator detects placeholder mismatches automatically
### Adding New Translation Keys in Code
```tsx
// Use namespaced keys
const t = useTranslations("settings");
t("cacheSettings"); // maps to settings.cacheSettings in JSON
// Run check_translations.py to verify keys exist
python3 scripts/i18n/check_translations.py --verbose
```
### RTL Considerations
- Arabic (`ar`) and Hebrew (`he`) are RTL locales
- Avoid hardcoded `left`/`right` CSS — use `start`/`end` logical properties
- Visual QA catches RTL layout mismatches via `run-visual-qa.mjs`
## Known Issues & History
### `in.json` → `hi.json` Fix
The generator originally used `code: "in"` (deprecated Google Translate code) for Hindi instead of the correct ISO 639-1 `hi`. This created an orphaned `in.json` duplicate of `hi.json`. Fixed by changing `code: "in"` to `code: "hi"` in `generate-multilang.mjs` and removing the orphaned file.
> ⚠️ **Audit (2026-05-13):** The `docs/i18n/in/` directory still exists on disk (full duplicate of `hi/`). Translation generator no longer writes to it, but the historical tree was not pruned. Safe to delete with `rm -rf docs/i18n/in/` after confirming no external links reference the old path.
### `docs/i18n/README.md` Is Auto-Generated
The `docs/i18n/README.md` file is completely regenerated by `generate-multilang.mjs docs`. Any manual edits will be lost. Use `docs/guides/I18N.md` (this file) for hand-written documentation that should persist.
### External Untranslatable Keys List
The `untranslatable-keys.json` allowlist was moved from an inline Python set in `validate_translation.py` to an external JSON file for easier maintenance. The validator loads it at runtime.
### `generate-multilang.mjs` Hindi Code Fix
The generator originally used `code: "in"` (deprecated Google Translate code) for Hindi instead of the correct ISO 639-1 `hi`. This was introduced in upstream commit `952b0b22c` by `diegosouzapw`. Fixed by changing `code: "in"` to `code: "hi"` in the `LOCALE_SPECS` array and removing the orphaned `in.json` file.
### `validate_translation.py` Ignored Count Output
The `quick` check now displays the count of ignored keys from `untranslatable-keys.json`:
```
Missing: 0
Untranslated: 0
Ignored (UNTRANSLATABLE_KEYS): <varies per release>
```
+140
View File
@@ -0,0 +1,140 @@
---
title: "Kiro Setup Guide"
---
# Kiro Setup Guide
This guide covers adding Kiro (AWS-hosted AI coding assistant) accounts to OmniRoute,
with a focus on running multiple accounts simultaneously without session conflicts.
---
## Background: Why Kiro Accounts Can Conflict
Kiro's backend uses AWS SSO OIDC client registrations to track active sessions.
The critical constraint: **each OIDC client registration supports only one active
session at a time**. When a second device or user authenticates using the same
registered client, the backend invalidates the first account's refresh token.
This is the same mechanism that causes problems when running `kiro-cli login` on a
machine where another Kiro account is already signed in — the new login revokes the
first account's token.
---
## How OmniRoute Solves This (v3.8.0+)
Starting with v3.8.0, OmniRoute calls `registerClient()` (AWS SSO OIDC) during every
Kiro connection import. This gives each OmniRoute connection its own dedicated OIDC
client registration. Because each client registration is independent, refreshing or
re-authenticating one account does not affect any other account's refresh token.
The isolation applies to all three import methods:
| Import method | Isolation status |
| --------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| AWS Builder ID / IDC device-code flow | Isolated since the device-code flow was introduced |
| **Import Token** (manual refresh token paste) | Isolated from v3.8.0 |
| **Google / GitHub social login** | Isolated from v3.8.0 |
| **Auto-Import** (kiro-cli SQLite) | Isolated from v3.8.0 (SQLite path was already isolated; SSO-cache fallback is now also isolated) |
---
## Migration Note for Connections Created Before v3.8.0
Connections imported before v3.8.0 do not have a dedicated OIDC client registration
stored in `providerSpecificData`. These connections continue to work but use the shared
social-auth refresh endpoint, which means two such connections can still invalidate each
other.
**To gain isolation:** delete the old connection from **Dashboard → Providers** and
re-import it using any of the supported import flows. All newly created connections will
receive their own client registration automatically.
---
## Adding Two Kiro Accounts Side by Side
### Prerequisites
- OmniRoute v3.8.0 or later.
- A working Kiro account (email + password, Google, or GitHub login).
- Optionally a second Kiro account.
### Step 1: Import the first account
1. Open **Dashboard → Providers → Add Provider → Kiro**.
2. Choose one of:
- **Import Token** — paste a refresh token starting with `aorAAAAAG`.
- **Google / GitHub login** — complete the OAuth flow in the browser.
- **Auto-Import** — click the button; OmniRoute reads credentials from the
local kiro-cli database or `~/.aws/sso/cache`.
3. The connection is saved. OmniRoute automatically registers a dedicated OIDC client for it.
### Step 2: Import the second account
Repeat step 1 for the second account. Because each import creates a separate OIDC
client registration, the two connections are fully isolated.
### Step 3: Verify both connections are active
1. **Dashboard → Providers** — both Kiro connections should show **Active** status.
2. **Dashboard → Health** — both connections should pass their token health check.
### Step 4: Use a combo to route between accounts
Create a combo with both connections as targets to load-balance or fall back between them:
```
kiro/kiro-dev → kiro/kiro-pro
```
See [FEATURES.md](./FEATURES.md) and the routing documentation for combo configuration.
---
## Enterprise / IDC Users
For AWS IAM Identity Center (IDC) accounts, use the **AWS Builder ID / IDC device-code**
flow from **Dashboard → Providers → Kiro → Device Code**. The device-code flow has
always been fully isolated. No re-import is needed for these connections.
Enterprise users who operate in a non-default AWS region can specify the region when
importing via the Import Token API:
```bash
curl -X POST http://localhost:20128/api/oauth/kiro/import \
-H "Content-Type: application/json" \
-d '{"refreshToken": "aorAAAAAG...", "region": "eu-west-1"}'
```
The `region` field defaults to `us-east-1` when omitted.
---
## OIDC Client Expiry
AWS SSO OIDC public clients typically expire after 90 days
(`clientSecretExpiresAt`). OmniRoute stores this timestamp in `providerSpecificData`
for observability. If a connection stops refreshing after ~90 days, re-import the
connection to obtain a fresh OIDC client registration. Automatic re-registration on
expiry is tracked as a future improvement.
---
## Troubleshooting
### Second account keeps getting logged out
- Check both connections in **Dashboard → Providers** and confirm each shows a non-null
`clientId` in its raw JSON (visible via the info icon). If either connection is missing
`clientId`, it was imported before v3.8.0 — re-import it.
### Import fails with "Token validation failed"
- Ensure the refresh token starts with `aorAAAAAG`.
- Ensure OmniRoute can reach `https://oidc.us-east-1.amazonaws.com` (or the configured
region). If you are behind a corporate proxy, set a provider-level proxy in
**Dashboard → Settings → Proxies**.
For other issues, see the main [TROUBLESHOOTING.md](./TROUBLESHOOTING.md).
+192
View File
@@ -0,0 +1,192 @@
---
title: "Progressive Web App (PWA) Guide"
version: 3.8.40
lastUpdated: 2026-06-28
---
# Progressive Web App (PWA) Guide
OmniRoute ships as a fully installable Progressive Web App. When you access the dashboard from any mobile browser — Android (Chrome) or iOS (Safari) — you can "Add to Home Screen" and get a native app-like experience with no app store required.
## What Is a PWA?
A Progressive Web App turns the OmniRoute web dashboard into something that looks and feels like a native mobile app. Once installed, it:
- Launches from your home screen with its own icon
- Opens fullscreen — no browser address bar or tab UI
- Works offline with a dedicated connectivity page
- Caches static assets for faster loading
- Supports both portrait and landscape orientations
## Installation
### Android (Chrome)
1. Open the OmniRoute dashboard in Chrome: `http://YOUR_IP:20128`
2. Chrome will show an **"Add OmniRoute to Home screen"** banner automatically, or:
- Tap the **⋮** menu (three dots) → **"Add to Home screen"** or **"Install app"**
3. Confirm the prompt
4. OmniRoute appears on your home screen as a standalone app
### iOS (Safari)
1. Open the OmniRoute dashboard in Safari: `http://YOUR_IP:20128`
2. Tap the **Share** button (box with arrow)
3. Scroll down and tap **"Add to Home Screen"**
4. Name it (defaults to "OmniRoute") and tap **Add**
5. OmniRoute appears on your home screen with the app icon
### Desktop (Chrome / Edge)
1. Open the OmniRoute dashboard
2. Click the **install icon** in the address bar (or ⋮ → "Install OmniRoute...")
3. Confirm the prompt
4. OmniRoute opens as a standalone window — no tabs, no address bar
## Features
### Fullscreen Experience
The manifest is configured with `display: "fullscreen"`, which means the installed app uses the entire screen — no browser chrome, no status bar overlap. This makes the dashboard feel truly native.
### Offline Support
OmniRoute includes a service worker (`sw.js`) that provides intelligent caching:
| Asset Type | Strategy | Behavior |
| ------------------------------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------- |
| **App Shell** | Cache-first | `/`, `/offline`, manifest, and icons are pre-cached on install |
| **Static assets** (CSS, JS, images, fonts) | Network-first with cache fallback | Fetches fresh from the network; falls back to cache if offline |
| **Next.js bundles** (`/_next/`) | Network-first with cache update | Fetches from network and updates cache; serves cached version if offline |
| **Navigation requests** | Network-only with offline fallback | Always fetches from network; shows `/offline` page if network is unavailable |
| **API routes** (`/api/`, `/a2a`, `/dashboard/endpoint`) | Bypass (never cached) | Always goes directly to the server — never intercepted by the service worker |
### Offline Page
When the network is unavailable and a user navigates to a new page, the service worker serves a dedicated `/offline` page that:
- Displays a clear **"Connectivity Issue"** message
- Shows a live **online/offline status indicator** that updates in real time
- Provides a **"Retry Connection"** button to reload when connectivity returns
- Links to the **Status Page** for diagnostics
### App Icons
OmniRoute provides icons optimized for each platform:
| File | Size | Used By |
| ---------------------- | ---------------- | ------------------------------------- |
| `icon-512.png` | 512×512 | Android install prompt, splash screen |
| `apple-touch-icon.png` | 180×180 | iOS home screen icon |
| `icon-192.svg` | 192×192 (vector) | Android adaptive icon |
| `apple-touch-icon.svg` | 180×180 (vector) | Apple fallback |
| `favicon.svg` | Vector | Browser tabs |
| `favicon.ico` | Multi-size | Legacy browsers |
### Automatic Registration
The service worker is registered automatically via the `<PwaRegister />` component in the root layout. No user action is needed — the app becomes installable as soon as the browser detects the valid manifest and service worker.
## Technical Architecture
### Web App Manifest (`manifest.webmanifest`)
Generated by Next.js via `src/app/manifest.ts`:
```json
{
"name": "OmniRoute",
"short_name": "OmniRoute",
"description": "OmniRoute is an AI gateway for multi-provider LLMs. One endpoint for all your AI providers.",
"start_url": "/",
"scope": "/",
"display": "fullscreen",
"orientation": "any",
"background_color": "#0b0f1a",
"theme_color": "#0b0f1a",
"icons": [
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" },
{ "src": "/apple-touch-icon.png", "sizes": "180x180", "type": "image/png" }
]
}
```
### Service Worker (`public/sw.js`)
A vanilla service worker (no framework dependencies) with:
- **Install phase**: Pre-caches the app shell (root, offline page, manifest, icons)
- **Activate phase**: Cleans up old cache versions and claims all clients
- **Fetch phase**: Intelligent routing based on request type (navigation, static asset, API)
- **Cache versioning**: `omniroute-pwa-v2` — bump this to force a fresh cache on update
### Layout Metadata (`src/app/layout.tsx`)
The root layout provides all the meta tags required for PWA compliance:
- `manifest` link to `/manifest.webmanifest`
- `apple-web-app-capable: true` for iOS standalone mode
- `apple-web-app-status-bar-style: black-translucent`
- `mobile-web-app-capable: yes` for Android Chrome
- `theme-color: #0b0f1a`
- `viewport-fit: cover` for edge-to-edge rendering
### Component: `PwaRegister`
Located at `src/shared/components/PwaRegister.tsx`, this client component:
1. Runs on mount (client-side only)
2. Checks for `serviceWorker` support in the browser
3. Registers `/sw.js` silently (errors are swallowed to avoid blocking the app)
4. Renders nothing (`return null`) — it's a side-effect-only component
## Use With Termux (Android)
When running OmniRoute on Android via Termux, the PWA works seamlessly:
1. Start OmniRoute in Termux: `npx omniroute`
2. Open Chrome on the same phone: `http://localhost:20128`
3. Install the PWA via "Add to Home Screen"
4. The PWA connects to the local Termux server — everything runs on-device
This combination means your Android phone is both the **server** (Termux) and the **client** (PWA) — a complete self-contained AI gateway.
## Use From Other Devices
Install the PWA on any device that has browser access to your OmniRoute server:
- **Another phone/tablet**: Navigate to `http://PHONE_IP:20128` and install the PWA
- **Laptop**: Open Chrome/Edge and install it as a desktop PWA
- **Smart TV with browser**: Access the dashboard fullscreen
## Customization
### Instance Name
The PWA title respects the **Instance Name** setting from `Dashboard → Settings`. If you rename your instance to "My AI Gateway", the installed PWA will show that name.
### Custom Favicon
If you upload a custom favicon via `Dashboard → Settings`, the PWA icon on desktop will reflect the custom icon. Mobile home screen icons use the pre-built `icon-512.png` and `apple-touch-icon.png` files.
## Limitations
- **No push notifications** — The service worker does not implement the Push API. Notifications are handled by the Electron app instead.
- **No background sync** — Offline actions are not queued for replay. The PWA is primarily a dashboard viewer.
- **iOS restrictions** — Safari on iOS does not support all PWA features (e.g., install prompts are manual, and background service workers are limited).
- **Cache size** — The service worker caches static assets only. Large response payloads from `/api/` routes are never cached.
- **Custom icons on mobile** — Changing the favicon in settings does not update the home screen icon on mobile (this requires regenerating the PWA icons).
## Files Reference
| File | Purpose |
| --------------------------------------- | ---------------------------------------------------------------- |
| `src/app/manifest.ts` | Next.js manifest route (generates `manifest.webmanifest`) |
| `public/sw.js` | Service worker with caching logic |
| `src/shared/components/PwaRegister.tsx` | Client component that registers the service worker |
| `src/app/offline/page.tsx` | Offline fallback page with live status indicator |
| `src/app/layout.tsx` | Root layout with PWA metadata (apple-web-app, theme-color, etc.) |
| `public/icon-512.png` | 512×512 PNG icon (Android, splash screen) |
| `public/apple-touch-icon.png` | 180×180 PNG icon (iOS home screen) |
| `public/icon-192.svg` | 192×192 SVG icon (Android adaptive) |
| `public/apple-touch-icon.svg` | 180×180 SVG icon (Apple fallback) |
+347
View File
@@ -0,0 +1,347 @@
---
title: "Remote Mode — Drive a remote OmniRoute from your laptop"
version: 3.8.40
lastUpdated: 2026-06-28
---
# Remote Mode
Run the `omniroute` CLI on your laptop while OmniRoute itself runs somewhere else
(a VPS, a home server, another machine on your Tailnet). You log in once with
`omniroute connect`, and from then on **every** CLI command targets that remote
server — same commands, same output, just executed against the remote.
There is no second tool to install: remote mode is the regular `omniroute` CLI
plus scoped **access tokens**.
```bash
npm install -g omniroute # the normal CLI
omniroute connect 192.168.0.15 # log in (password → scoped token)
omniroute models list # ← now lists the REMOTE server's models
omniroute configure codex # ← writes a local Codex profile from the remote catalog
```
---
## How it works
```
your laptop remote OmniRoute (VPS)
┌────────────────────┐ ┌───────────────────────────────┐
│ omniroute CLI │ POST /api/cli/connect (password → token) │
│ context: vps │ ───────────────► │ mints a scoped access token │
│ baseUrl, token │ Authorization: Bearer oma_live_… │
│ │ ───────────────► │ every management route, scope- │
│ writes configs │ ◄─────────────── │ checked per the token's scope │
│ LOCALLY │ └───────────────────────────────┘
└────────────────────┘
```
- **Contexts** store one server each (`~/.omniroute/config.json`, `chmod 600`).
`omniroute contexts use <name>` switches the active server; `default` is local.
- **Access tokens** (`oma_live_…`) authorize management commands. They are
distinct from inference API keys (`sk-…`, used for `/v1/chat/completions`).
- Only the SHA-256 hash of a token is stored server-side. The plaintext is shown
**once**, at creation.
---
## Connecting
### With the management password (bootstrap)
```bash
omniroute connect 192.168.0.15
# Management password for http://192.168.0.15:20128: ********
# ✔ Connected to http://192.168.0.15:20128 — context '192.168.0.15' (scope: admin)
```
The password flow mints an **admin** token by default (you hold the password, so
you already have full control). Downscope with `--scope`:
```bash
omniroute connect 192.168.0.15 --scope write
```
Options: `--port <p>` (when the host has none), `--name <ctx>` (context name),
`--scope read|write|admin`. A full URL is honoured as-is:
`omniroute connect https://omni.example.com`.
### With a pre-generated token
Generate a scoped token in the dashboard (or with `omniroute tokens create`) and
paste it — no password needed:
```bash
omniroute connect 192.168.0.15 --key oma_live_xxxxxxxx
```
The CLI validates it via `GET /api/cli/whoami` and saves it as the active context.
---
## Scopes
Three levels, hierarchical (`admin ⊃ write ⊃ read`):
| Scope | Can do |
| ------- | ---------------------------------------------------------------------------- |
| `read` | list/inspect — `models list`, `providers status`, `logs`, `usage`, `cost` |
| `write` | read **+** configure/apply — `setup-codex`, `keys add`, `config set`, combos |
| `admin` | write **+** manage — `tokens` CRUD, add providers, services, policy, oauth |
The server infers the scope each route requires from the HTTP method
(`GET`→read, mutations→write) plus an admin allowlist for sensitive surfaces
(`/api/cli/tokens`, `/api/providers` mutations, `/api/oauth`, `/api/services`, …).
A token with insufficient scope gets `403` with a clear message.
> Routes that spawn processes (`/api/services/*`, `/api/mcp/*`, …) stay
> **loopback-only** — a remote token can never reach them, regardless of scope.
---
## Connecting Antigravity on a remote install
Antigravity uses Google's firstparty/nativeapp consent screen. Google only
releases the authorization code when the **loopback redirect**
(`http://127.0.0.1:<port>/callback`) is **reachable from the browser that
approves the sign-in**. On a remote VPS install that loopback lives on the
server, not on your machine, so the consent screen **hangs forever and never
emits a code** — the normal "paste the callback URL" fallback has nothing to
paste. (This is a Google-side constraint: the same hang happens in any proxy
that uses the bundled Antigravity desktop client, not just OmniRoute.)
There are two supported ways to connect Antigravity to a remote OmniRoute.
### Option A — local login helper (recommended)
Run the OAuth on **your own computer**, where `127.0.0.1` is reachable, and paste
the result into the remote dashboard. The helper talks only to Google — it does
**not** need network access to your VPS, so it works even behind firewalls.
```bash
# On your LOCAL machine (needs Node.js + a browser):
npx omniroute login antigravity
# ↳ opens the Google consent in your browser, captures the callback on a local
# loopback port, exchanges it, and prints a one-line credential blob:
#
# omniroute-cred-v1.eyJ2IjoxLCJ...
```
Then, in the **remote** dashboard: **Providers → Antigravity → Connect**, and
paste the `omniroute-cred-v1.…` blob into the **Step 2** field (it accepts either
a callback URL or a credential blob). OmniRoute decodes it, runs the Cloud Code
onboarding server-side, and persists the connection.
> The blob contains a refresh token — treat it like a password. It is sent once
> over your dashboard connection and stored encrypted at rest.
Flags: `--no-browser` (print the URL instead of auto-opening), `--port <n>`
(pin the loopback port), `--timeout <ms>`.
### Option B — SSH local-forward tunnel
If you have SSH access to the VPS, forward the dashboard port so that the
loopback callback resolves back to the server through the tunnel:
```bash
# On your LOCAL machine:
ssh -L 20128:localhost:20128 user@your-vps
# then open http://localhost:20128 in your LOCAL browser and connect Antigravity
# normally — the 127.0.0.1:20128/callback redirect now reaches the VPS via SSH.
```
Because you reach the dashboard as `localhost:20128`, the Google consent
completes and the callback is delivered to the server through the same tunnel —
no blob needed. Keep the tunnel open until the connection shows as active.
> A fully headless alternative (no helper, no tunnel) is to configure your **own**
> Google OAuth web credentials + a public base URL; see the provider's OAuth
> environment variables. The two options above need no extra Google setup.
---
## Managing tokens
```bash
omniroute tokens create --name "laptop" --scope write [--expires 30]
# ↳ prints the secret ONCE — copy it now
omniroute tokens list # masked: id, name, scope, prefix, status, expiry
omniroute tokens revoke <id|prefix> # revoke immediately
omniroute tokens scopes # explain the three scopes
```
`tokens` commands require an **admin** credential. You can also manage tokens in
the dashboard under **Settings → Access Tokens** (create, revoke, copy-once).
---
## Configuring a coding CLI from the remote catalog
`omniroute configure` reads the **active server's** live model catalog and writes
a config on **your** machine.
```bash
omniroute configure codex
# Providers: glm, kmc, ollamacloud, opencode-go, …
# Provider: glm
# Model id: glm/glm-5.2
# ✔ Wrote ~/.codex/glm52.config.toml
# Use it: codex --profile glm52
# non-interactive
omniroute configure codex --provider glm --model glm/glm-5.2 --name glm52
```
The written profile references the inference key by env var
(`OMNIROUTE_API_KEY`) — the secret is never written to disk. For the one-time
base Codex setup (the `[model_providers.omniroute]` block), see
[CODEX-CLI-CONFIGURATION.md](./CODEX-CLI-CONFIGURATION.md).
### Per-CLI setup commands
Each supported CLI has a remote-aware setup command (all honour the active
context, or `--remote <url> --api-key <key>`):
| CLI | Command | What it writes |
|-----|---------|----------------|
| Codex | `omniroute setup-codex` | `~/.codex/<name>.config.toml` profiles (per model) |
| Claude Code | `omniroute setup-claude` | `~/.claude/profiles/<name>/settings.json` (per model) |
| OpenCode | `omniroute setup-opencode` | `~/.config/opencode/opencode.json` — the `omniroute` openai-compatible provider with every catalog model (run `opencode -m omniroute/<model>`) |
| Cline | `omniroute setup-cline` | `~/.cline/data/{globalState,secrets}.json` (CLI mode) + prints the VS Code extension settings to paste (OpenAI-compatible, Base URL **without** `/v1`) |
| Kilo Code | `omniroute setup-kilo` | `~/.local/share/kilo/auth.json` (CLI) + VS Code `kilocode.*` settings — OpenAI-compatible, Base URL **with** `/v1` |
| Continue | `omniroute setup-continue` | `~/.continue/config.yaml` (VS Code/JetBrains + `cn` CLI) — `provider: openai`, `apiBase` **with** `/v1`, key via `${{ secrets.OMNIROUTE_API_KEY }}` |
| Cursor | `omniroute setup-cursor` | prints the in-app steps (Settings → Models → Override OpenAI Base URL **with** `/v1` + key + model). Cursor config is opaque SQLite — chat panel only |
| Roo Code | `omniroute setup-roo` | writes a Roo import JSON (`~/.omniroute/roo-settings.json`) + sets `roo-cline.autoImportSettingsPath` + prints UI steps (OpenAI-compatible, Base URL **with** `/v1`) |
| Crush | `omniroute setup-crush` | `~/.config/crush/crush.json``openai-compat` provider, `base_url` **with** `/v1`, key via `$OMNIROUTE_API_KEY` |
| Goose | `omniroute setup-goose` | `~/.config/goose/config.yaml` (`GOOSE_PROVIDER=openai` + `OPENAI_HOST` **without** `/v1` + `GOOSE_MODEL`) + env recipe |
| Qwen Code | `omniroute setup-qwen` | `~/.qwen/settings.json` — openai `modelProvider`, `baseUrl` **with** `/v1`, key via `envKey` (OMNIROUTE_API_KEY) |
| Aider | `omniroute setup-aider` | `~/.aider.conf.yml` (`openai-api-base` **without** `/v1` + `model: openai/<id>`) + env recipe (`aider --message --yes`) |
```bash
# OpenCode (openai-compatible provider, all catalog models, remote VPS)
omniroute setup-opencode --remote http://192.168.0.15:20128 --api-key oma_live_xxx
omniroute setup-opencode --only glm,kimi # keep only matching models
opencode -m omniroute/glm/glm-5.2 "..." # export OMNIROUTE_API_KEY first
```
> OpenCode also has a richer **plugin** integration: `omniroute setup opencode`
> (now remote-aware via `--remote`) installs `@omniroute/opencode-plugin`.
> `setup-opencode` is the lightweight openai-compatible alternative. The API key
> is referenced via `{env:OMNIROUTE_API_KEY}` — never written to disk.
---
## Managing contexts (switch between servers)
A **context** is a saved server (baseUrl + credential + scope). `omniroute connect`
creates one and makes it active; from then on every command targets it. Manage and
switch between them with `omniroute contexts`:
```bash
omniroute contexts list # all contexts; the active one is marked ●
omniroute contexts current # the active server, auth status, scope
```
```text
| Name | Base URL | Auth | Scope | Description
● | vps | http://100.67.86.91:20128 | token | admin | Remote OmniRoute (…)
| default | http://localhost:20128 | ✗ | |
```
**Switch servers** — every subsequent command follows the active context:
```bash
omniroute contexts use vps # → all commands now hit the remote VPS
omniroute tokens list # (runs against the VPS)
omniroute contexts use default # → back to localhost
omniroute tokens list # (runs against the local server)
```
**Add a context manually** (instead of `connect`), inspect, or rename:
```bash
omniroute contexts add staging --url https://staging.example.com:20128 \
--access-token oma_live_xxxx --scope write --description "staging box"
omniroute contexts show staging # full details for one context
omniroute contexts rename staging stg
```
**Remove a context** — prompts for confirmation; pass `--yes` to skip it
(required for scripts / non-interactive shells, which otherwise decline safely):
```bash
omniroute contexts remove stg --yes
```
> `default` (localhost) cannot be removed. Removing the active context falls back
> to `default`. Tip: removing a context only drops the **local** saved credential —
> revoke the token on the server with `omniroute tokens revoke <id>` to actually
> kill access.
**Export / import** contexts (e.g. to move them between machines — secrets included,
so handle the file carefully):
```bash
omniroute contexts export --out contexts.json # default: stdout
omniroute contexts import contexts.json # overwrite; --merge to keep existing
```
---
## Quick end-to-end check
A copy-paste lifecycle to verify a remote setup from scratch — connect, mint a
scoped token, route a command, switch back, and tear down. Replace
`192.168.0.15` with your server's host/IP (Tailscale, LAN, or a public
`https://…` URL).
```bash
# 1. Connect (password → admin token, saved as a context that becomes active)
omniroute connect 192.168.0.15 # or: --key oma_live_xxxx (no password)
omniroute contexts current # shows the remote server + scope
# 2. Use it — management commands now run against the remote
omniroute tokens create --name laptop --scope read # mint a narrower token
omniroute tokens list # masked list, from the remote
# 3. Switch back and forth
omniroute contexts use default # → local
omniroute contexts use 192-168-0-15 # → remote again (name from `contexts list`)
# 4. Tear down. NOTE: `contexts remove` only deletes the LOCAL credential —
# it does NOT revoke the token on the server. Revoke server-side first if you
# want to actually kill access.
omniroute tokens revoke <id|prefix> # kills access on the server
omniroute contexts remove 192-168-0-15 --yes # drop the local context (even if active → falls back to default), no prompt
```
> `--yes` makes `contexts remove` non-interactive (required in scripts/CI; without
> it, a non-interactive shell declines safely instead of hanging). Removing the
> **active** context falls back to `default` automatically.
---
## Security notes
- Token plaintext is shown once; only the SHA-256 hash is persisted (same as API keys).
- `omniroute connect` reuses the login brute-force lockout + audit logging.
- Prefer HTTPS or a Tailnet for the transport; a bare host defaults to `http://`
for LAN/Tailscale convenience — pass a full `https://…` URL for TLS.
- The local context file is `~/.omniroute/config.json` (`chmod 600`); tokens are
never printed in logs (masked to a prefix).
---
## API endpoints (reference)
| Method | Route | Auth | Scope |
| ------ | --------------------- | ------------------- | -------------------------- |
| POST | `/api/cli/connect` | management password | — (public, password-gated) |
| GET | `/api/cli/whoami` | access token | read |
| GET | `/api/cli/tokens` | access token | admin |
| POST | `/api/cli/tokens` | access token | admin |
| DELETE | `/api/cli/tokens/:id` | access token | admin |
See [openapi.yaml](../openapi.yaml) for full schemas.
+418
View File
@@ -0,0 +1,418 @@
---
title: "📖 Setup Guide — OmniRoute"
version: 3.8.40
lastUpdated: 2026-06-28
---
# 📖 Setup Guide — OmniRoute
> Complete setup reference for OmniRoute. For the quick version, see the [Quick Start in README](../README.md#-quick-start).
## Table of Contents
- [Install Methods](#install-methods)
- [CLI Tool Configuration](#cli-tool-configuration)
- [Protocol Setup (MCP + A2A)](#protocol-setup-mcp--a2a)
- [Timeout Configuration](#timeout-configuration)
- [Split-Port Mode](#split-port-mode)
- [Void Linux (xbps-src)](#void-linux-xbps-src-template)
- [Uninstalling](#uninstalling)
---
## Install Methods
### npm (recommended)
```bash
npm install -g omniroute
omniroute
```
Dashboard opens at `http://localhost:20128` and API base URL is `http://localhost:20128/v1`.
### pnpm
```bash
pnpm add -g omniroute@latest --allow-build=better-sqlite3 --allow-build=@swc/core
omniroute
```
> **pnpm users:** the `--allow-build` flag is required to enable native build scripts for `better-sqlite3` and `@swc/core`. The `pnpm approve-builds -g` command is not supported for global installs on pnpm v11.
### Arch Linux (AUR)
```bash
yay -S omniroute-bin
systemctl --user enable --now omniroute.service
```
The [AUR package](https://aur.archlinux.org/packages/omniroute-bin) installs OmniRoute and provides a systemd user service.
### From Source
```bash
npm install
PORT=20128 DASHBOARD_PORT=20129 NEXT_PUBLIC_BASE_URL=http://localhost:20129 npm run dev
```
> **Note:** `npm install` auto-generates `.env` from `.env.example` on first run. Subsequent installs will not overwrite an existing `.env`, so customizations are preserved. To re-seed, delete `.env` before re-running.
### Docker
See the [Docker Guide](./DOCKER_GUIDE.md) for complete Docker setup including Compose profiles and Caddy HTTPS.
### Desktop App (Electron)
OmniRoute ships a desktop wrapper built on Electron 41 + electron-builder 26.10. Available scripts (workspace root):
```bash
npm run electron:dev # Run desktop with hot-reload
npm run electron:build # Build for current OS (auto-detected)
npm run electron:build:win # Windows installer (NSIS + portable)
npm run electron:build:mac # macOS (dmg + zip, arm64+x64)
npm run electron:build:linux # Linux (AppImage + deb + rpm)
npm run electron:smoke:packaged # Smoke-test packaged build
```
Releases of the desktop installers are attached to GitHub Releases. For the full Electron deep-dive (signing, IPC bridge, distros), see [`ELECTRON_GUIDE.md`](./ELECTRON_GUIDE.md) _(criado em fase posterior)_.
### Headless server (CI/automation)
For unattended setups (Docker, Kubernetes, CI), use:
```bash
omniroute setup --non-interactive
omniroute providers test-batch
```
Combined with env vars (`INITIAL_PASSWORD`, `OMNIROUTE_WS_BRIDGE_SECRET`, etc.), this lets you spin up an OmniRoute instance fully scriptable.
### CLI Options
| Command | Description |
| ----------------------- | -------------------------------------------------------------- |
| `omniroute` | Start server (`PORT=20128`, API and dashboard on same port) |
| `omniroute setup` | Guided CLI onboarding for password and first provider |
| `omniroute doctor` | Run local health checks without starting the server |
| `omniroute providers` | Discover, list, validate, and test providers from CLI |
| `omniroute config` | CLI tool configuration — list, get, set, validate configs |
| `omniroute status` | Offline status dashboard — version, DB, tools, config |
| `omniroute logs` | Stream usage logs from the API (supports `--follow`) |
| `omniroute update` | Check for or apply OmniRoute updates |
| `omniroute provider` | Manage provider connections — add, list, remove, test, default |
| `omniroute --port 3000` | Set canonical/API port to 3000 |
| `omniroute --mcp` | Start MCP server (stdio transport) |
| `omniroute --no-open` | Don't auto-open browser |
| `omniroute --help` | Show help |
Headless setup can be scripted with flags or environment variables:
```bash
omniroute setup --non-interactive --password "$OMNIROUTE_PASSWORD"
omniroute setup --non-interactive --add-provider --provider openai --api-key "$OPENAI_API_KEY"
omniroute setup --non-interactive --add-provider --provider openai --api-key "$OPENAI_API_KEY" --test-provider
```
Run local diagnostics without opening the dashboard:
```bash
omniroute doctor
omniroute doctor --json
omniroute doctor --no-liveness
```
Manage providers from SSH or scripts without opening the dashboard:
```bash
omniroute providers available
omniroute providers available --search openai
omniroute providers available --category api-key
omniroute providers list
omniroute providers test <id-or-name>
omniroute providers test-all
omniroute providers validate
```
---
## CLI Tool Configuration
### 1) Connect Providers and Create API Key
1. Open Dashboard → `Providers` and connect at least one provider (OAuth or API key).
2. Open Dashboard → `Endpoints` and create an API key.
3. (Optional) Open Dashboard → `Combos` and set your fallback chain.
### 2) Point Your Coding Tool
```txt
Base URL: http://localhost:20128/v1
API Key: [copy from Endpoint page]
Model: if/kimi-k2-thinking (or any provider/model prefix)
```
If your editor cannot send `Authorization: Bearer ...`, use the tokenized compatibility base instead:
```txt
Base URL: http://localhost:20128/api/v1/vscode/YOUR_KEY/
Models URL: http://localhost:20128/api/v1/vscode/YOUR_KEY/models
Chat URL: http://localhost:20128/api/v1/vscode/YOUR_KEY/chat/completions
Ollama Tags URL: http://localhost:20128/api/v1/vscode/YOUR_KEY/api/tags
```
Works with Claude Code, Codex CLI, Cursor, Cline, OpenClaw, OpenCode, and OpenAI-compatible SDKs.
#### Auto-configure with `setup-*`
Instead of pasting the base URL and key by hand, let OmniRoute write each tool's
own config from the live model catalog. One command per tool:
```bash
omniroute setup-codex # ~/.codex/<name>.config.toml profiles
omniroute setup-claude # ~/.claude/profiles/<name>/settings.json
omniroute setup-opencode # ~/.config/opencode/opencode.json (openai-compatible)
omniroute setup-cline # Cline CLI + VS Code extension settings
omniroute setup-kilo # Kilo Code
omniroute setup-continue # ~/.continue/config.yaml (Continue / cn)
omniroute setup-cursor # prints Cursor's in-app steps
omniroute setup-roo # Roo Code import + autoImport pointer
omniroute setup-crush # ~/.config/crush/crush.json
omniroute setup-goose # ~/.config/goose/config.yaml
omniroute setup-qwen # ~/.qwen/settings.json
omniroute setup-aider # ~/.aider.conf.yml
```
Each accepts `--remote <url> --api-key <key>` to configure a local tool against a
**remote** OmniRoute, plus `--dry-run` to preview. The launchers
`omniroute launch` (Claude Code) and `omniroute launch-codex` (Codex) spawn the CLI
with the right env injected, writing no config at all.
For the full table (what each command writes, every flag, local vs remote, base-URL
`/v1` conventions), see **[CLI Integrations](./CLI-INTEGRATIONS.md)**.
For detailed per-tool configuration (Claude Code, Codex CLI, Cursor, Cline, OpenClaw, Kilo Code, Copilot, and more), see the dedicated **[CLI Tools Guide](../reference/CLI-TOOLS.md)**.
---
## Protocol Setup (MCP + A2A)
### MCP Setup (Model Context Protocol)
Start MCP transport in stdio mode:
```bash
omniroute --mcp
```
Recommended validation flow:
```bash
# 1. Start MCP server
omniroute --mcp
# 2. From your MCP client, call:
omniroute_get_health # Should return system health
omniroute_list_combos # Should return active combos
# 3. Or run the full E2E suite:
npm run test:protocols:e2e
```
#### MCP Client Configuration
**Claude Code:**
```bash
claude mcp add-server omniroute --type http --url http://localhost:20128/api/mcp/stream
```
**Cursor / Cline:**
Add to your MCP settings:
```json
{
"mcpServers": {
"omniroute": {
"command": "omniroute",
"args": ["--mcp"],
"env": {}
}
}
}
```
**Full MCP documentation:** [MCP Server README](../../open-sse/mcp-server/README.md) — 87 tools, IDE configs, Python/TS/Go clients.
### A2A Setup (Agent-to-Agent Protocol)
Verify the Agent Card:
```bash
curl http://localhost:20128/.well-known/agent.json
```
Send a task:
```bash
curl -X POST http://localhost:20128/a2a \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":"quickstart","method":"message/send","params":{"skill":"quota-management","messages":[{"role":"user","content":"Give me a short quota summary."}]}}'
```
**Full A2A documentation:** [A2A Server README](../../src/lib/a2a/README.md) — JSON-RPC 2.0, skills, streaming, task lifecycle.
---
## Timeout Configuration
### Basic Timeouts
For most deployments, you only need these two variables:
| Variable | Default | Purpose |
| ------------------------ | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `REQUEST_TIMEOUT_MS` | `600000` | Shared baseline for upstream response-start timeout, hidden Undici timeouts, TLS fingerprint requests, and API bridge request/proxy timeouts |
| `STREAM_IDLE_TIMEOUT_MS` | inherits `REQUEST_TIMEOUT_MS` | Maximum gap between streaming chunks before OmniRoute aborts the SSE stream |
Backward compatibility is preserved: existing `FETCH_TIMEOUT_MS`, `API_BRIDGE_PROXY_TIMEOUT_MS`, and other per-layer timeout vars still work and override the shared baseline.
### Provider-Specific Notes
For Claude Code-compatible upstreams (`anthropic-compatible-cc-*`), OmniRoute derives the outbound `X-Stainless-Timeout` header from the resolved fetch timeout so provider-side read timeouts stay aligned with your env configuration.
For third-party Claude Code-compatible reverse proxies, OmniRoute keeps the default `anthropic-beta` set conservative and, when `Client Cache Control` is left on `Auto`, only forwards client-provided `cache_control` markers. Enable the per-connection "Enable redact-thinking beta" toggle only when the upstream specifically requires redacted Claude thinking streams.
### Advanced Timeout Overrides
| Variable | Default | Purpose |
| ---------------------------------------- | ------------------------------------------ | -------------------------------------------------------------------- |
| `FETCH_TIMEOUT_MS` | inherits `REQUEST_TIMEOUT_MS` | Upstream response-start timeout used until response headers arrive |
| `FETCH_HEADERS_TIMEOUT_MS` | inherits `FETCH_TIMEOUT_MS` | Undici time limit for receiving upstream response headers |
| `FETCH_BODY_TIMEOUT_MS` | inherits `FETCH_TIMEOUT_MS` | Undici time limit between upstream body chunks (`0` disables it) |
| `FETCH_CONNECT_TIMEOUT_MS` | `30000` | Undici TCP connect timeout |
| `FETCH_KEEPALIVE_TIMEOUT_MS` | `4000` | Undici idle keep-alive socket timeout |
| `TLS_CLIENT_TIMEOUT_MS` | inherits `FETCH_TIMEOUT_MS` | Timeout for TLS fingerprint requests made through `wreq-js` |
| `API_BRIDGE_PROXY_TIMEOUT_MS` | inherits `REQUEST_TIMEOUT_MS` or `600000` | Timeout for `/v1` proxy forwarding from API port to dashboard port |
| `API_BRIDGE_SERVER_REQUEST_TIMEOUT_MS` | `max(API_BRIDGE_PROXY_TIMEOUT_MS, 300000)` | Incoming request timeout on the API bridge server |
| `API_BRIDGE_SERVER_HEADERS_TIMEOUT_MS` | `60000` | Incoming header timeout on the API bridge server |
| `API_BRIDGE_SERVER_KEEPALIVE_TIMEOUT_MS` | `5000` | Keep-alive timeout on the API bridge server |
| `API_BRIDGE_SERVER_SOCKET_TIMEOUT_MS` | `0` | Socket inactivity timeout on the API bridge server (`0` disables it) |
> **Note:** For streaming requests, `FETCH_TIMEOUT_MS` only covers connection setup / waiting for the first upstream response. Once the stream is active, OmniRoute will only abort on an actual stall (`STREAM_IDLE_TIMEOUT_MS`) or Undici body inactivity (`FETCH_BODY_TIMEOUT_MS`).
### Reverse Proxy Compatibility
If you run OmniRoute behind Nginx, Caddy, Cloudflare, or another reverse proxy, make sure the proxy timeouts are also higher than your OmniRoute stream/fetch timeouts.
---
## Split-Port Mode
Run API and Dashboard on separate ports for advanced scenarios (reverse proxy, container networking):
```bash
PORT=20128 DASHBOARD_PORT=20129 omniroute
# API: http://localhost:20128/v1
# Dashboard: http://localhost:20129
```
---
## Void Linux (xbps-src) Template
For Void Linux users, you can build a native package using `xbps-src`. Save this block as `srcpkgs/omniroute/template`:
```bash
# Template file for 'omniroute'
pkgname=omniroute
version=3.8.0
revision=1
hostmakedepends="nodejs python3 make"
depends="openssl"
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
maintainer="zenobit <zenobit@disroot.org>"
license="MIT"
homepage="https://github.com/diegosouzapw/OmniRoute"
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
# Regenerate the checksum for each release with:
# curl -L -o /tmp/omniroute.tar.gz "https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz" && sha256sum /tmp/omniroute.tar.gz
checksum=PLACEHOLDER_REGENERATE_PER_RELEASE
system_accounts="_omniroute"
omniroute_homedir="/var/lib/omniroute"
export NODE_ENV=production
export npm_config_engine_strict=false
export npm_config_loglevel=error
export npm_config_fund=false
export npm_config_audit=false
do_build() {
local _gyp_arch
case "$XBPS_TARGET_MACHINE" in
aarch64*) _gyp_arch=arm64 ;;
armv7*|armv6*) _gyp_arch=arm ;;
i686*) _gyp_arch=ia32 ;;
*) _gyp_arch=x64 ;;
esac
NODE_ENV=development npm ci --ignore-scripts
npm run build
cp -r .next/static .next/standalone/.next/static
[ -d public ] && cp -r public .next/standalone/public || true
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
mkdir -p "$_bs3_release"
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
rm -rf .next/standalone/node_modules/@img
for _mod in pino-abstract-transport split2 process-warning; do
cp -r "node_modules/$_mod" .next/standalone/node_modules/
done
}
do_check() {
npm run test:unit
}
do_install() {
vmkdir usr/lib/omniroute/.next
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
for _d in \
.next/standalone/.next/server/app/dashboard \
.next/standalone/.next/server/app/dashboard/settings \
.next/standalone/.next/server/app/dashboard/providers; do
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
done
cat > "${WRKDIR}/omniroute" <<'EOF'
#!/bin/sh
export PORT="${PORT:-20128}"
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
export APP_LOG_TO_FILE="${APP_LOG_TO_FILE:-false}"
mkdir -p "${DATA_DIR}"
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
EOF
vbin "${WRKDIR}/omniroute"
}
post_install() {
vlicense LICENSE
}
```
---
## Uninstalling
| Command | Action |
| ------------------------ | ----------------------------------------------------------------------------------- |
| `npm run uninstall` | Removes the system app but **keeps your DB and configurations** in `~/.omniroute`. |
| `npm run uninstall:full` | Removes the app AND permanently **erases all configurations, keys, and databases**. |
> For detailed uninstall instructions across all methods, see [UNINSTALL.md](./UNINSTALL.md).
+168
View File
@@ -0,0 +1,168 @@
---
title: "Termux Headless Setup"
version: 3.8.40
lastUpdated: 2026-06-28
---
# Termux Headless Setup
OmniRoute can run as a headless server on Android through Termux. The Electron desktop app is not supported in Termux, but the web dashboard and OpenAI-compatible API work from the local browser or from other devices on the same network.
## Prerequisites
Install Termux from F-Droid or GitHub releases, then update packages and install the build tools required by native dependencies such as `better-sqlite3`.
```bash
pkg update
pkg upgrade
pkg install nodejs python build-essential git
```
> **Node.js version:** OmniRoute requires Node `>=22.22.2 <23 || >=24.0.0 <27` (matches `engines` in `package.json` / `SUPPORTED_NODE_RANGE`). Termux's `nodejs-lts` typically ships Node 20 LTS, which is **no longer supported** — install `pkg install nodejs` (current) instead and verify `node --version` reports a 22.x/24.x+ line.
If native package compilation fails, rerun the `pkg install` command above and then retry the OmniRoute install.
## Install
Run the latest published package directly:
```bash
npx -y omniroute@latest
```
You can also install it globally:
```bash
npm install -g omniroute
omniroute
```
## Run
Start OmniRoute in headless server mode:
```bash
omniroute
```
or:
```bash
npx omniroute
```
The dashboard listens on:
```text
http://localhost:20128
```
Open that URL in the Android browser. If you run clients inside Termux, use the same host and port as the OpenAI-compatible base URL.
## Background Execution
For a simple background process:
```bash
nohup omniroute > omniroute.log 2>&1 &
```
To stop it:
```bash
pkill -f omniroute
```
For automatic startup after device boot, install the Termux:Boot add-on and create a boot script:
```bash
mkdir -p ~/.termux/boot
cat > ~/.termux/boot/omniroute.sh <<'EOF'
#!/data/data/com.termux/files/usr/bin/sh
cd "$HOME"
nohup omniroute > "$HOME/omniroute.log" 2>&1 &
EOF
chmod +x ~/.termux/boot/omniroute.sh
```
Android battery optimization can stop long-running background processes. Disable battery optimization for Termux if the server is expected to stay online.
## Access From Other Devices
Find the phone IP address on the WiFi network:
```bash
ip addr show wlan0
```
Then open the dashboard from another device:
```text
http://PHONE_IP:20128
```
For example:
```text
http://192.168.1.50:20128
```
Keep the phone and client on the same trusted network. If you expose OmniRoute outside the phone, enable API keys and dashboard authentication.
## Data Directory
By default OmniRoute stores data under the Termux home directory, following the same server-side data path behavior used on Linux. To place the database somewhere explicit:
```bash
export DATA_DIR="$HOME/.omniroute"
omniroute
```
## Limitations
- Electron does not run in Termux.
- There is no system tray or desktop integration.
- This setup is server-only: use the browser dashboard.
- Native dependencies may need local compilation.
- Low-memory Android devices may need fewer concurrent requests.
- MITM/system certificate features may require Android-level trust-store work outside Termux.
## Troubleshooting
### better-sqlite3 Build Errors
Install the Termux build toolchain:
```bash
pkg install nodejs python build-essential
```
Then rerun:
```bash
npx -y omniroute@latest
```
### Port Already In Use
Check what is listening on the default port:
```bash
ss -ltnp | grep 20128
```
Stop the old process:
```bash
pkill -f omniroute
```
### Dashboard Not Reachable From Another Device
Verify both devices are on the same WiFi network, then test from Termux:
```bash
curl http://localhost:20128
```
If local access works but LAN access does not, check Android hotspot/WiFi isolation and any firewall or VPN profile on the phone.
+106
View File
@@ -0,0 +1,106 @@
---
title: "OmniRoute Tiers — User Guide"
version: 3.8.40
lastUpdated: 2026-06-28
---
# OmniRoute Tiers — User Guide
OmniRoute organizes the 207+ supported providers into 3 economic tiers. Each
request travels through them in order until one returns successfully — you
get the cheapest viable response without ever writing fallback code.
## Tier 1 — Subscription
**Providers you already pay for.** OmniRoute uses every drop of quota before
it expires.
| Provider | Why Tier 1 |
| ----------------------------------- | -------------------------------------------- |
| Claude Code OAuth | Anthropic Pro/Team — flat-rate, often unused |
| OpenAI Codex (ChatGPT subscription) | Plus/Team includes Codex quota |
| GitHub Copilot | Per-seat — quota resets monthly |
| Cursor IDE | Pro plan quota |
| Antigravity / Windsurf | Built-in quotas |
**Strategy**: route here first for every request that fits the model's
strengths. Quota tracker monitors approaching reset; combo strategies
`reset-aware` and `subscription` prioritize accordingly.
## Tier 2 — Cheap
**Pay-per-token providers under $1/1M tokens.** Reserved for high-volume work
or after Tier 1 quotas hit limits.
| Provider | Price (input/output) | Strengths |
| ---------------------------- | -------------------- | -------------------- |
| DeepSeek V4 Pro | $0.27 / $1.10 per 1M | Code, reasoning |
| GLM-4.5 | $0.60 / $2.20 per 1M | Long context |
| MiniMax M1 | $0.20 / $1.10 per 1M | Speed |
| Qwen Coder | $0.30 / $1.20 per 1M | Code |
| OpenRouter (price-optimized) | varies | 100+ models, dynamic |
**Strategy**: combo `cost-optimized` picks lowest $/token model that meets
the task's capability filter (vision, JSON mode, tools, max-context).
## Tier 3 — Free
**Zero-cost providers** — free tiers, credit programs, OAuth daily quotas.
| Provider | Free quota / credits |
| ---------------- | ------------------------------------ |
| Kiro AI | Free Claude tier (generous fair-use) |
| OpenCode Free | No auth, generous rate limits |
| Qoder | Free OAuth |
| Google Vertex AI | $300 new-account credits |
| Amazon Q | Free tier for AWS users |
| Pollinations | Open public API |
| Cloudflare AI | Workers AI free tier |
**Strategy**: combo `auto` with budget cap routes here when Tier 1+2 fail
or when `useFreeOnly=true` is set. Free providers often have weaker
rate limits — circuit breaker recovers them on backoff.
## Configuring tiers
Dashboard → **Tiers** → assign your providers. Defaults (from `tierDefaults.json`) are
sensible; edit when you have specific subscriptions to prioritize or providers to exclude.
Auto-Combo's 9-factor scoring also considers tier. See
[`docs/routing/AUTO-COMBO.md`](../routing/AUTO-COMBO.md).
## Telemetry
Dashboard → **Usage** shows tokens spent per tier per day. Use this to:
- Confirm Tier 1 is utilized fully (otherwise you're wasting subscription value)
- Identify which Tier 2 models are picked most (consolidate to 1-2)
- Verify Tier 3 saves money on test/exploration workloads
## Common patterns
### Pure-free workload
```json
{
"strategy": "auto",
"config": { "auto": { "weights": { "costInv": 0.5, "tierPriority": 0.3 } } }
}
```
Forces strongly towards Tier 3; only uses Tier 2 if Tier 3 is unavailable.
### Subscription-first with cheap fallback
```json
{
"strategy": "priority",
"targets": [
{ "provider": "claude-code-oauth", "weight": 1 },
{ "provider": "deepseek", "weight": 1 },
{ "provider": "kiro", "weight": 1 }
]
}
```
Explicit ordered list matching Tier 1 → Tier 2 → Tier 3.
+514
View File
@@ -0,0 +1,514 @@
---
title: "Troubleshooting"
version: 3.8.40
lastUpdated: 2026-06-28
---
# Troubleshooting
> **For Users**: Looking for quick fixes? See the [Quick Reference](#quick-reference) below.
🌐 **Languages:** 🇺🇸 [English](./TROUBLESHOOTING.md) | 🇧🇷 [Português (Brasil)](../i18n/pt-BR/docs/guides/TROUBLESHOOTING.md) | 🇪🇸 [Español](../i18n/es/docs/guides/TROUBLESHOOTING.md) | 🇫🇷 [Français](../i18n/fr/docs/guides/TROUBLESHOOTING.md) | 🇮🇹 [Italiano](../i18n/it/docs/guides/TROUBLESHOOTING.md) | 🇷🇺 [Русский](../i18n/ru/docs/guides/TROUBLESHOOTING.md) | 🇨🇳 [中文 (简体)](../i18n/zh-CN/docs/guides/TROUBLESHOOTING.md) | 🇩🇪 [Deutsch](../i18n/de/docs/guides/TROUBLESHOOTING.md) | 🇮🇳 [हिन्दी](../i18n/in/docs/guides/TROUBLESHOOTING.md) | 🇹🇭 [ไทย](../i18n/th/docs/guides/TROUBLESHOOTING.md) | 🇺🇦 [Українська](../i18n/uk-UA/docs/guides/TROUBLESHOOTING.md) | 🇸🇦 [العربية](../i18n/ar/docs/guides/TROUBLESHOOTING.md) | 🇯🇵 [日本語](../i18n/ja/docs/guides/TROUBLESHOOTING.md) | 🇻🇳 [Tiếng Việt](../i18n/vi/docs/guides/TROUBLESHOOTING.md) | 🇧🇬 [Български](../i18n/bg/docs/guides/TROUBLESHOOTING.md) | 🇩🇰 [Dansk](../i18n/da/docs/guides/TROUBLESHOOTING.md) | 🇫🇮 [Suomi](../i18n/fi/docs/guides/TROUBLESHOOTING.md) | 🇮🇱 [עברית](../i18n/he/docs/guides/TROUBLESHOOTING.md) | 🇭🇺 [Magyar](../i18n/hu/docs/guides/TROUBLESHOOTING.md) | 🇮🇩 [Bahasa Indonesia](../i18n/id/docs/guides/TROUBLESHOOTING.md) | 🇰🇷 [한국어](../i18n/ko/docs/guides/TROUBLESHOOTING.md) | 🇲🇾 [Bahasa Melayu](../i18n/ms/docs/guides/TROUBLESHOOTING.md) | 🇳🇱 [Nederlands](../i18n/nl/docs/guides/TROUBLESHOOTING.md) | 🇳🇴 [Norsk](../i18n/no/docs/guides/TROUBLESHOOTING.md) | 🇵🇹 [Português (Portugal)](../i18n/pt/docs/guides/TROUBLESHOOTING.md) | 🇷🇴 [Română](../i18n/ro/docs/guides/TROUBLESHOOTING.md) | 🇵🇱 [Polski](../i18n/pl/docs/guides/TROUBLESHOOTING.md) | 🇸🇰 [Slovenčina](../i18n/sk/docs/guides/TROUBLESHOOTING.md) | 🇸🇪 [Svenska](../i18n/sv/docs/guides/TROUBLESHOOTING.md) | 🇵🇭 [Filipino](../i18n/phi/docs/guides/TROUBLESHOOTING.md) | 🇨🇿 [Čeština](../i18n/cs/docs/guides/TROUBLESHOOTING.md)
Common problems and solutions for OmniRoute.
---
## Quick Reference
**New to OmniRoute?** Start here — these solve 90% of problems:
| I see this | What it means | What to do |
| ----------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------- |
| "Can't connect" | OmniRoute isn't running | Run `omniroute` or `docker restart omniroute` |
| "Invalid API key" | Your key is wrong or expired | Re-copy the key from the provider's website |
| "Rate limit exceeded" | You're sending too many requests | Wait 1 minute, or use `model: "auto"` for automatic fallback |
| "Quota exceeded" | You've used up your free/paid quota | Connect more providers, or use free providers (Kiro, Pollinations) |
| "Slow responses" | Provider is busy or far away | Use `model: "auto/fast"` or connect a faster provider (Groq, Cerebras) |
| "Wrong provider used" | `auto` picked a different provider | That's normal! `auto` picks the best one. Force a specific provider with `model: "openai/gpt-4o"` |
| "502 Bad Gateway" | Provider is down | Wait and retry, or use `model: "auto"` to switch providers |
| "401 Unauthorized" | Your credentials are wrong | Check your API key or re-authenticate with OAuth |
| "429 Too Many Requests" | Rate limited | Wait 1 minute, or connect more providers |
**Still stuck?** See the [detailed troubleshooting](#detailed-troubleshooting) below, or ask on [Discord](https://discord.gg/EkzRkpzKYt).
---
## Detailed Troubleshooting
---
## Quick Fixes
| Problem | Solution |
| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| First login not working | Set `INITIAL_PASSWORD` in `.env` (no hardcoded default) |
| Dashboard opens on wrong port | Set `PORT=20128` and `NEXT_PUBLIC_BASE_URL=http://localhost:20128` |
| No logs written to disk | Set `APP_LOG_TO_FILE=true` and verify call log capture is enabled |
| EACCES: permission denied | Set `DATA_DIR=/path/to/writable/dir` to override `~/.omniroute` |
| Routing strategy not saving | Update to the latest v3.x release (Zod schema fix for settings persistence shipped in earlier versions) |
| Login crash / blank page | Check Node.js version — see [Node.js Compatibility](#nodejs-compatibility) below |
| `dlopen` / `slice is not valid mach-o file` (macOS) | Run `cd $(npm root -g)/omniroute/app && npm rebuild better-sqlite3 && omniroute` — see [macOS native module rebuild](#macos-native-module-rebuild) below |
| Proxy "fetch failed" | Ensure proxy config is set at the correct level — see [Proxy Issues](#proxy-issues) below |
---
## Node.js Compatibility
<a name="nodejs-compatibility"></a>
### Login page crashes or shows "Module self-registration" error
**Cause:** You are running a Node.js version outside OmniRoute's approved secure runtime floor. The most common case is running an older Node 22 or 24 patch level that falls below the patched security floor OmniRoute requires.
**Symptoms:**
- Login page shows a blank screen or a server error
- Console shows `Error: Module did not self-register` or similar native binding errors
- The login page shows an **orange warning banner** with your Node version if the runtime is outside the supported secure policy
**Fix:**
1. Install a supported Node.js LTS release (recommended: Node.js 24.x):
```bash
nvm install 24
nvm use 24
```
2. Verify your version: `node --version` should show `v24.0.0` or newer on the 24.x LTS line
3. Reinstall OmniRoute: `npm install -g omniroute`
4. Restart: `omniroute`
> **Supported secure versions:** `>=22.22.2 <23` or `>=24.0.0 <27`. Node.js 24.x LTS (Krypton) and Node.js 26 are fully supported.
### macOS: `dlopen` / "slice is not valid mach-o file"
<a name="macos-native-module-rebuild"></a>
**Cause:** After a global `npm install -g omniroute`, the `better-sqlite3` native binary inside the package may have been compiled for a different architecture or Node.js ABI than what is running locally. This is common on macOS (both Apple Silicon and Intel) when the pre-built binary does not match your environment.
**Symptoms:**
- Server fails immediately on startup with a `dlopen` error
- Error contains `slice is not valid mach-o file`
- Full example:
```
dlopen(/Users/<user>/.nvm/versions/node/v24.14.1/lib/node_modules/omniroute/app/node_modules/better-sqlite3/build/Release/better_sqlite3.node, 0x0001): tried: '...' (slice is not valid mach-o file)
```
**Fix — rebuild for your local environment (no Node.js downgrade required):**
```bash
cd $(npm root -g)/omniroute/app
npm rebuild better-sqlite3
omniroute
```
> **Note:** This recompiles the native binding against your local Node.js version and CPU architecture, resolving the binary mismatch. The officially supported runtime range is **`>=22.22.2 <23` or `>=24.0.0 <27`** (`SUPPORTED_NODE_RANGE` in `src/shared/utils/nodeRuntimeSupport.ts`, aligned with the `package.json` `engines` field). Node.js 24.x LTS (Krypton) and Node.js 26 are fully supported with `better-sqlite3` v12.x.
---
## Proxy Issues
<a name="proxy-issues"></a>
### Provider validation shows "fetch failed"
**Cause:** The API key validation endpoint (`POST /api/providers/validate`) was previously bypassing proxy configuration, causing failures in environments that require proxy routing.
**Fix (v3.5.5+):** This is now fixed. Provider validation routes through `runWithProxyContext`, honoring provider-level and global proxy settings automatically.
### Token health check fails with "fetch failed"
**Cause:** Background OAuth token refresh was not resolving proxy configuration per connection.
**Fix (v3.5.5+):** The token health check scheduler now resolves proxy config per connection before attempting refresh. Update to v3.5.5+.
### SOCKS5 proxy returns "invalid onRequestStart method"
**Cause:** On Node.js 22, the undici@8 dispatcher is incompatible with Node's built-in `fetch()` implementation.
**Fix (v3.5.5+):** OmniRoute now uses undici's own `fetch()` function when a proxy dispatcher is active, ensuring consistent behavior. Update to v3.5.5+.
### MITM proxy under WSL: desktop apps on the Windows host are not intercepted
**Cause:** The MITM proxy and its CA certificate install into the environment where OmniRoute runs. Under WSL that environment is the Linux guest, while the AI desktop apps (Kiro, Trae, Copilot, Zed, …) run on the Windows host. The host apps do not trust the guest's certificate store and do not route through the guest's system proxy, so desktop interception does not engage there.
**Recommendation:** Run OmniRoute natively on the same OS as the desktop apps you want to intercept (Windows for Windows apps; macOS/Linux likewise). Keeping OmniRoute inside WSL while targeting host apps requires manually trusting the generated CA certificate on the Windows host and pointing each host app's network/proxy settings at the WSL proxy endpoint — an unsupported, fragile setup.
---
## Provider Issues
### "Language model did not provide messages"
**Cause:** Provider quota exhausted.
**Fix:**
1. Check dashboard quota tracker
2. Use a combo with fallback tiers
3. Switch to cheaper/free tier
### Rate Limiting
**Cause:** Subscription quota exhausted.
**Fix:**
- Add fallback: `cc/claude-opus-4-6 → glm/glm-4.7 → if/kimi-k2-thinking`
- Use GLM/MiniMax as cheap backup
### OAuth Token Expired
OmniRoute auto-refreshes tokens. If issues persist:
1. Dashboard → Provider → Reconnect
2. Delete and re-add the provider connection
### Kiro multi-account: second account invalidates the first
**Cause:** Kiro's backend enforces a single active session per OIDC client registration.
When two accounts share the same registered client (connections imported before v3.8.0),
refreshing one account's token invalidates the other's refresh token.
**Fix (v3.8.0+):** Re-import affected connections.
Starting with v3.8.0, every new Kiro connection created via **Import Token**,
**Google/GitHub social login**, or **Auto-Import** automatically registers its own
dedicated OIDC client. The connection is therefore fully isolated and refreshing one
account has no effect on any other account.
Connections that were imported _before_ v3.8.0 do not carry a per-connection client
registration. Those connections continue to use the shared social-auth refresh endpoint.
To gain isolation, delete the old connection from Dashboard → Providers and re-add it
via any of the three import flows.
For full details and step-by-step instructions for adding two Kiro accounts side by side,
see [`docs/guides/KIRO_SETUP.md`](./KIRO_SETUP.md).
---
## Cloud Issues
### Cloud Sync Errors
1. Verify `BASE_URL` points to your running instance (e.g., `http://localhost:20128`)
2. Verify `CLOUD_URL` points to your cloud endpoint (e.g., `https://omniroute.dev`)
3. Keep `NEXT_PUBLIC_*` values aligned with server-side values
### Cloud `stream=false` Returns 500
**Symptom:** `Unexpected token 'd'...` on cloud endpoint for non-streaming calls.
**Cause:** Upstream returns SSE payload while client expects JSON.
**Workaround:** Use `stream=true` for cloud direct calls. Local runtime includes SSE→JSON fallback.
### Cloud Says Connected but "Invalid API key"
1. Create a fresh key from local dashboard (`/api/keys`)
2. Run cloud sync: Enable Cloud → Sync Now
3. Old/non-synced keys can still return `401` on cloud
---
## Docker Issues
### CLI Tool Shows Not Installed
1. Check runtime fields: `curl http://localhost:20128/api/cli-tools/runtime/codex | jq`
2. For portable mode: use image target `runner-cli` (bundled CLIs)
3. For host mount mode: set `CLI_EXTRA_PATHS` and mount host bin directory as read-only
4. If `installed=true` and `runnable=false`: binary was found but failed healthcheck
### Quick Runtime Validation
```bash
curl -s http://localhost:20128/api/cli-tools/codex-settings | jq '{installed,runnable,commandPath,runtimeMode,reason}'
curl -s http://localhost:20128/api/cli-tools/claude-settings | jq '{installed,runnable,commandPath,runtimeMode,reason}'
curl -s http://localhost:20128/api/cli-tools/openclaw-settings | jq '{installed,runnable,commandPath,runtimeMode,reason}'
```
---
## Cost Issues
### High Costs
1. Check usage stats in Dashboard → Usage
2. Switch primary model to GLM/MiniMax
3. Use free tier (Qoder, Kiro) for non-critical tasks
4. Set cost budgets per API key: Dashboard → API Keys → Budget
---
## Debugging
### Enable Log Files
Set `APP_LOG_TO_FILE=true` in your `.env` file. Application logs are written under `logs/`.
Request artifacts are stored under `${DATA_DIR}/call_logs/` when the call log pipeline is
enabled in settings.
When pipeline capture is enabled, set `CALL_LOG_PIPELINE_CAPTURE_STREAM_CHUNKS=false` to omit
stream chunk payloads, or tune `CALL_LOG_PIPELINE_MAX_SIZE_KB` to change the artifact cap in KB.
### Check Provider Health
```bash
# Health dashboard
http://localhost:20128/dashboard/health
# API health check
curl http://localhost:20128/api/monitoring/health
```
### Runtime Storage
- Main state: `${DATA_DIR}/storage.sqlite` (providers, combos, aliases, keys, settings)
- Usage: SQLite tables in `storage.sqlite` (`usage_history`, `call_logs`, `proxy_logs`) + optional `${DATA_DIR}/call_logs/`
- Application logs: `<repo>/logs/...` (when `APP_LOG_TO_FILE=true`)
- Call log artifacts: `${DATA_DIR}/call_logs/YYYY-MM-DD/...` when the call log pipeline is enabled
The Request Logs page's **Clean history** action clears `call_logs`, legacy
`request_detail_logs`, and the local `${DATA_DIR}/call_logs/` artifact directory.
---
## Circuit Breaker Issues
### Provider stuck in OPEN state
When a provider's circuit breaker is OPEN, requests are blocked until the cooldown expires.
**Fix:**
1. Go to **Dashboard → Settings → Resilience**
2. Check the circuit breaker card for the affected provider
3. Click **Reset All** to clear all breakers, or wait for the cooldown to expire
4. Verify the provider is actually available before resetting
### Provider keeps tripping the circuit breaker
If a provider repeatedly enters OPEN state:
1. Check **Dashboard → Health → Provider Health** for the failure pattern
2. Go to **Settings → Resilience → Provider Profiles** and increase the failure threshold
3. Check if the provider has changed API limits or requires re-authentication
4. Review latency telemetry — high latency may cause timeout-based failures
---
## Audio Transcription Issues
### "Unsupported model" error
- Ensure you're using the correct prefix: `deepgram/nova-3` or `assemblyai/best`
- Verify the provider is connected in **Dashboard → Providers**
### Transcription returns empty or fails
- Check supported audio formats: `mp3`, `wav`, `m4a`, `flac`, `ogg`, `webm`
- Verify file size is within provider limits (typically < 25MB)
- Check provider API key validity in the provider card
---
## Translator Debugging
Use **Dashboard → Translator** to debug format translation issues:
| Mode | When to Use |
| ---------------- | -------------------------------------------------------------------------------------------- |
| **Playground** | Compare input/output formats side by side — paste a failing request to see how it translates |
| **Chat Tester** | Send live messages and inspect the full request/response payload including headers |
| **Test Bench** | Run batch tests across format combinations to find which translations are broken |
| **Live Monitor** | Watch real-time request flow to catch intermittent translation issues |
### Common format issues
- **Thinking tags not appearing** — Check if the target provider supports thinking and the thinking budget setting
- **Tool calls dropping** — Some format translations may strip unsupported fields; verify in Playground mode
- **System prompt missing** — Claude and Gemini handle system prompts differently; check translation output
- **SDK returns raw string instead of object** — Resolved in v1.x; response sanitizer strips non-standard fields (`x_groq`, `usage_breakdown`, etc.) that cause OpenAI SDK Pydantic validation failures. If you still see this on v3.x+, please file an issue.
- **GLM/ERNIE rejects `system` role** — Resolved in v1.x; role normalizer automatically merges system messages into user messages for incompatible models. If you still see this on v3.x+, please file an issue.
- **`developer` role not recognized** — Resolved in v1.x; automatically converted to `system` for non-OpenAI providers. If you still see this on v3.x+, please file an issue.
- **`json_schema` not working with Gemini** — Resolved in v1.x; `response_format` is now converted to Gemini's `responseMimeType` + `responseSchema`. If you still see this on v3.x+, please file an issue.
---
## Resilience Settings
### Auto rate-limit not triggering
- Auto rate-limit only applies to API key providers (not OAuth/subscription)
- Verify **Settings → Resilience → Provider Profiles** has auto-rate-limit enabled
- Check if the provider returns `429` status codes or `Retry-After` headers
### Tuning exponential backoff
Provider profiles support these settings:
- **Base delay** — Initial wait time after first failure (default: 1s)
- **Max delay** — Maximum wait time cap (default: 30s)
- **Multiplier** — How much to increase delay per consecutive failure (default: 2x)
### Anti-thundering herd
When many concurrent requests hit a rate-limited provider, OmniRoute uses mutex + auto rate-limiting to serialize requests and prevent cascading failures. This is automatic for API key providers.
---
## Optional RAG / LLM failure taxonomy (16 problems)
Some OmniRoute users place the gateway in front of RAG or agent stacks. In those setups it is common to see a strange pattern: OmniRoute looks healthy (providers up, routing profiles ok, no rate limit alerts) but the final answer is still wrong.
In practice these incidents usually come from the downstream RAG pipeline, not from the gateway itself.
If you want a shared vocabulary to describe those failures you can use the WFGY ProblemMap, an external MIT license text resource that defines sixteen recurring RAG / LLM failure patterns. At a high level it covers:
- retrieval drift and broken context boundaries
- empty or stale indexes and vector stores
- embedding versus semantic mismatch
- prompt assembly and context window issues
- logic collapse and overconfident answers
- long chain and agent coordination failures
- multi agent memory and role drift
- deployment and bootstrap ordering problems
The idea is simple:
1. When you investigate a bad response, capture:
- user task and request
- route or provider combo in OmniRoute
- any RAG context used downstream (retrieved documents, tool calls, etc)
2. Map the incident to one or two WFGY ProblemMap numbers (`No.1` … `No.16`).
3. Store the number in your own dashboard, runbook, or incident tracker next to the OmniRoute logs.
4. Use the corresponding WFGY page to decide whether you need to change your RAG stack, retriever, or routing strategy.
Full text and concrete recipes live here (MIT license, text only):
[WFGY ProblemMap README](https://github.com/onestardao/WFGY/blob/main/ProblemMap/README.md)
You can ignore this section if you do not run RAG or agent pipelines behind OmniRoute.
---
## v3.8.0 Known Issues
Issues specific to the v3.8.0 release and their current workarounds. If a fix lands in a later patch, the entry will be updated or removed.
### Windsurf OAuth flow fails with 401
**Symptoms:**
- "401 unauthorized" while completing the Windsurf OAuth flow from the dashboard
- Windsurf provider card stays in "needs reconnection" state after the callback
**Causes:**
- `WINDSURF_FIREBASE_API_KEY` env var missing or empty
- `WINDSURF_API_KEY` misconfigured or pointing at a stale token
- Local firewall/proxy blocking the OAuth callback
**Fix:**
1. Verify both `WINDSURF_FIREBASE_API_KEY` and `WINDSURF_API_KEY` are set in `.env`
2. Restart OmniRoute so the new env values are picked up
3. Re-run the OAuth flow from **Dashboard → Providers → Windsurf → Reconnect**
### Devin CLI auth failures
**Symptoms:**
- "Devin CLI not found" or "auth failed" when invoking Devin-backed tools
- CLI runtime check reports `installed=false`
**Causes:**
- `CLI_DEVIN_BIN` points to a path that does not exist
- Devin CLI is not installed on the host
**Fix:**
1. Install the Devin CLI for your platform
2. Set `CLI_DEVIN_BIN=/usr/local/bin/devin` (or the real path) in `.env`
3. Restart OmniRoute and re-test from **Dashboard → CLI Tools**
### Model cooldown stuck (manual reset)
**Symptoms:**
- A model stays listed in cooldown even after the expiration time has passed
- Requests still skip the model in combo routing despite the timestamp being in the past
**Manual reset:**
- **Dashboard:** **Settings → Model Cooldowns** → click **Re-enable** on the affected card
- **API:** `DELETE /api/resilience/model-cooldowns` with management auth headers
### Command Code provider connection fails with 403
**Symptoms:**
- 403 when testing the Command Code provider connection
- The provider card shows "unauthorized" after a fresh add
**Cause:** The OAuth flow did not complete (callback not received or token not persisted).
**Fix:**
- Run `omniroute providers` from the CLI to re-trigger the OAuth flow, or
- Re-run OAuth from **Dashboard → Providers → Command Code → Reconnect**
### ModelScope returns aggressive 429 cooldowns
**Symptoms:**
- Very short or immediate cooldowns on ModelScope after a small burst of requests
- Combo routing skips ModelScope earlier than expected
**Cause:** ModelScope emits provider-specific `Retry-After` headers. v3.8.0 ships dedicated handling for those headers, so older versions misread them as generic rate-limit hints.
**Fix:**
- Ensure you are on v3.8.0 or later
- Verify the `useUpstream429BreakerHints` toggle is enabled under **Settings → Resilience**
### OMNIROUTE_WS_BRIDGE_SECRET missing in production
**Symptoms:**
- 401 on every Codex/Responses WebSocket bridge request when running on a remote production host
- WebSocket bridge handshake closes immediately after connect
**Cause:** The `OMNIROUTE_WS_BRIDGE_SECRET` env var is missing from the production environment.
**Fix:**
1. Generate a random secret: `openssl rand -hex 32`
2. Set `OMNIROUTE_WS_BRIDGE_SECRET=<random-secret>` in the production server env (and any client that talks to the bridge)
3. Restart OmniRoute
### Responses API: background mode degraded to synchronous
**Symptoms:**
- Warning logged: `background mode degraded to synchronous`
- A `background: true` request returns a normal synchronous response instead of a background job handle
**Cause:** v3.8.0 intentionally degrades `background: true` on the Responses API to synchronous execution while emitting a warning. Full async background execution is a future deliverable.
**Fix:**
- Adjust the client to call without `background`, or
- Wait for a later release that ships full async background mode (track the changelog)
---
## Still Stuck?
- **GitHub Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
- **Architecture**: See [`docs/architecture/ARCHITECTURE.md`](../architecture/ARCHITECTURE.md) for internal details
- **API Reference**: See [`docs/reference/API_REFERENCE.md`](../reference/API_REFERENCE.md) for all endpoints
- **Health Dashboard**: Check **Dashboard → Health** for real-time system status
- **Translator**: Use **Dashboard → Translator** to debug format issues
+161
View File
@@ -0,0 +1,161 @@
---
title: "OmniRoute — Uninstall Guide"
version: 3.8.40
lastUpdated: 2026-06-28
---
# OmniRoute — Uninstall Guide
🌐 **Languages:** 🇺🇸 [English](./UNINSTALL.md) | 🇧🇷 [Português (Brasil)](../i18n/pt-BR/docs/guides/UNINSTALL.md) | 🇪🇸 [Español](../i18n/es/docs/guides/UNINSTALL.md) | 🇫🇷 [Français](../i18n/fr/docs/guides/UNINSTALL.md) | 🇮🇹 [Italiano](../i18n/it/docs/guides/UNINSTALL.md) | 🇷🇺 [Русский](../i18n/ru/docs/guides/UNINSTALL.md) | 🇨🇳 [中文 (简体)](../i18n/zh-CN/docs/guides/UNINSTALL.md) | 🇩🇪 [Deutsch](../i18n/de/docs/guides/UNINSTALL.md) | 🇮🇳 [हिन्दी](../i18n/in/docs/guides/UNINSTALL.md) | 🇹🇭 [ไทย](../i18n/th/docs/guides/UNINSTALL.md) | 🇺🇦 [Українська](../i18n/uk-UA/docs/guides/UNINSTALL.md) | 🇸🇦 [العربية](../i18n/ar/docs/guides/UNINSTALL.md) | 🇯🇵 [日本語](../i18n/ja/docs/guides/UNINSTALL.md) | 🇻🇳 [Tiếng Việt](../i18n/vi/docs/guides/UNINSTALL.md) | 🇧🇬 [Български](../i18n/bg/docs/guides/UNINSTALL.md) | 🇩🇰 [Dansk](../i18n/da/docs/guides/UNINSTALL.md) | 🇫🇮 [Suomi](../i18n/fi/docs/guides/UNINSTALL.md) | 🇮🇱 [עברית](../i18n/he/docs/guides/UNINSTALL.md) | 🇭🇺 [Magyar](../i18n/hu/docs/guides/UNINSTALL.md) | 🇮🇩 [Bahasa Indonesia](../i18n/id/docs/guides/UNINSTALL.md) | 🇰🇷 [한국어](../i18n/ko/docs/guides/UNINSTALL.md) | 🇲🇾 [Bahasa Melayu](../i18n/ms/docs/guides/UNINSTALL.md) | 🇳🇱 [Nederlands](../i18n/nl/docs/guides/UNINSTALL.md) | 🇳🇴 [Norsk](../i18n/no/docs/guides/UNINSTALL.md) | 🇵🇹 [Português (Portugal)](../i18n/pt/docs/guides/UNINSTALL.md) | 🇷🇴 [Română](../i18n/ro/docs/guides/UNINSTALL.md) | 🇵🇱 [Polski](../i18n/pl/docs/guides/UNINSTALL.md) | 🇸🇰 [Slovenčina](../i18n/sk/docs/guides/UNINSTALL.md) | 🇸🇪 [Svenska](../i18n/sv/docs/guides/UNINSTALL.md) | 🇵🇭 [Filipino](../i18n/phi/docs/guides/UNINSTALL.md) | 🇨🇿 [Čeština](../i18n/cs/docs/guides/UNINSTALL.md)
This guide covers how to cleanly remove OmniRoute from your system.
---
## Quick Uninstall (v3.6.2+)
OmniRoute provides two built-in scripts for clean removal:
### Keep Your Data
```bash
npm run uninstall
```
This removes the OmniRoute application but **preserves** your database, configurations, API keys, and provider settings in `~/.omniroute/`. Use this if you plan to reinstall later and want to keep your setup.
### Full Removal
```bash
npm run uninstall:full
```
This removes the application **and permanently erases** all data:
- Database (`storage.sqlite`)
- Provider configurations and API keys
- Backup files
- Log files
- All files in the `~/.omniroute/` directory
> ⚠️ **Warning:** `npm run uninstall:full` is irreversible. All your provider connections, combos, API keys, and usage history will be permanently deleted.
---
## Manual Uninstall
### NPM Global Install
```bash
# Remove the global package
npm uninstall -g omniroute
# (Optional) Remove data directory
rm -rf ~/.omniroute
```
### pnpm Global Install
```bash
pnpm uninstall -g omniroute
rm -rf ~/.omniroute
```
### Docker
```bash
# Stop and remove the container
docker stop omniroute
docker rm omniroute
# Remove the volume (deletes all data)
docker volume rm omniroute-data
# (Optional) Remove the image
docker rmi diegosouzapw/omniroute:latest
```
### Docker Compose
```bash
# Stop and remove containers
docker compose down
# Also remove volumes (deletes all data)
docker compose down -v
```
### Electron Desktop App
**Windows:**
- Open `Settings → Apps → OmniRoute → Uninstall`
- Or run the NSIS uninstaller from the install directory
**macOS:**
- Drag `OmniRoute.app` from `/Applications` to Trash
- Remove data: `rm -rf ~/Library/Application Support/omniroute`
**Linux:**
- Remove the AppImage file
- Remove data: `rm -rf ~/.omniroute`
### Source Install (git clone)
```bash
# Remove the cloned directory
rm -rf /path/to/omniroute
# (Optional) Remove data directory
rm -rf ~/.omniroute
```
---
## Data Directories
OmniRoute stores data in the following locations by default:
| Platform | Default Path | Override |
| ------------- | ----------------------------- | ------------------------- |
| Linux | `~/.omniroute/` | `DATA_DIR` env var |
| macOS | `~/.omniroute/` | `DATA_DIR` env var |
| Windows | `%APPDATA%/omniroute/` | `DATA_DIR` env var |
| Docker | `/app/data/` (mounted volume) | `DATA_DIR` env var |
| XDG-compliant | `$XDG_CONFIG_HOME/omniroute/` | `XDG_CONFIG_HOME` env var |
### Files in the data directory
| File/Directory | Description |
| -------------------- | ------------------------------------------------- |
| `storage.sqlite` | Main database (providers, combos, settings, keys) |
| `storage.sqlite-wal` | SQLite write-ahead log (temporary) |
| `storage.sqlite-shm` | SQLite shared memory (temporary) |
| `call_logs/` | Request payload archives |
| `backups/` | Automatic database backups |
| `log.txt` | Legacy request log (optional) |
---
## Verify Complete Removal
After uninstalling, verify there are no remaining files:
```bash
# Check for global npm package
npm list -g omniroute 2>/dev/null
# Check for data directory
ls -la ~/.omniroute/ 2>/dev/null
# Check for running processes
pgrep -f omniroute
```
If any process is still running, stop it:
```bash
pkill -f omniroute
```
+433
View File
@@ -0,0 +1,433 @@
---
title: "Usage, Quota & Spend Tracking"
version: 3.8.40
lastUpdated: 2026-06-28
---
# Usage, Quota & Spend Tracking
> **TL;DR**: OmniRoute tracks every request's token usage, computes cost, enforces per-API-key quota, and surfaces analytics in the dashboard. This guide explains how it all works.
**Sources:**
- `open-sse/services/usage.ts` (~70KB) — main usage tracking
- `src/lib/usageAnalytics.ts` (~10KB) — aggregation for dashboard
- `src/lib/db/quotaSnapshots.ts` — historical quota data
- `src/lib/db/usage*.ts` — multiple usage-related DB modules
---
## Overview
Every request that flows through OmniRoute generates a **usage record** that captures:
- **Identity**: which API key, provider, model, combo
- **Tokens**: prompt tokens, completion tokens, cached tokens, total
- **Cost**: USD amount (computed from pricing data)
- **Timing**: latency, start/end timestamps
- **Status**: success, error, rate-limited, etc.
These records are aggregated into **analytics**, persisted as **quota snapshots**, and used to enforce **per-key budget limits**.
```
Request ──▶ chatCore ──▶ usage.record() ──▶ SQLite
┌───────┼───────┐
▼ ▼ ▼
analytics quota billing
(dashboard) (enforce) (export)
```
---
## What Gets Recorded
The `usage.ts` service captures a **usage event** for every request:
| Field | Type | Source |
| ------------------ | ------- | ---------------------------------------------------------- |
| `id` | string | UUID generated on record |
| `apiKeyId` | string | The API key that initiated the request |
| `provider` | string | Provider ID (openai, anthropic, etc.) |
| `model` | string | Model ID (gpt-5, claude-opus-4-6, etc.) |
| `comboId` | string? | Combo ID if routed through a combo |
| `promptTokens` | number | From upstream response |
| `completionTokens` | number | From upstream response |
| `cachedTokens` | number | Cache hit tokens (Anthropic prompt caching, etc.) |
| `totalTokens` | number | prompt + completion |
| `costUsd` | number | Computed from pricing data |
| `latencyMs` | number | End-to-end request duration |
| `status` | enum | `success`, `error`, `rate_limited`, `timeout`, `cancelled` |
| `errorClass` | string? | Error class if status != success |
| `timestamp` | string | ISO 8601 UTC |
| `metadata` | object | Custom plugin-injected data |
### Where Tokens Come From
Tokens are extracted from the upstream provider's response in the **response handler**:
```ts
// From open-sse/handlers/chatCore.ts
const response = await providerExecutor.execute(provider, request);
const usage = response.usage || {
prompt_tokens: 0,
completion_tokens: 0,
cached_tokens: 0,
};
```
For providers that don't return usage (some web-cookie providers), OmniRoute **estimates** tokens using a `~4 chars per token` heuristic (see `open-sse/services/autoCombo/pipelineRouter.ts`).
### Cached Tokens
OmniRoute tracks `cached_tokens` separately from `prompt_tokens` because:
- Anthropic prompt caching charges a reduced rate for cached tokens (10% of normal)
- Some providers return `cache_read_input_tokens` that should be priced differently
- Analytics can show the **cache hit rate** = `cached_tokens / prompt_tokens`
---
## Cost Calculation
Costs are computed from **pricing data** synced from LiteLLM (`src/lib/pricingSync.ts`):
| Model | Input $/1M | Output $/1M | Cached $/1M |
| ----------------- | ---------- | ----------- | ----------- |
| gpt-5 | $2.50 | $10.00 | — |
| claude-opus-4-6 | $15.00 | $75.00 | $1.50 |
| claude-sonnet-4-5 | $3.00 | $15.00 | $0.30 |
| gemini-2.5-pro | $1.25 | $10.00 | — |
The cost formula (`src/lib/usage/costCalculator.ts`):
```ts
cost =
(prompt_tokens - cached_tokens) * input_price +
cached_tokens * cached_price +
completion_tokens * output_price;
```
> **Why subtract cached from prompt?** The cached portion is priced separately; charging input price on the whole prompt would over-count.
### Pricing Sync
Pricing data is auto-synced from LiteLLM via the `/api/pricing/sync` endpoint (triggered by the built-in cron task, not a user-facing env var):
```bash
# Manual trigger
curl -X POST http://localhost:20128/api/pricing/sync
```
For models with no pricing data, OmniRoute falls back to **estimating cost** using internal average rates (sourced from LiteLLM's pricing data).
---
## Date Range Aggregation
The `usageAnalytics.ts` module computes dashboard widgets from raw usage data. It supports 7 time ranges:
| Range | Window | Use case |
| -------- | --------------------------- | --------------------------- |
| `1d` | Last 24 hours | Hourly cost spike detection |
| `7d` | Last 7 days | Weekly review |
| `30d` | Last 30 days | Monthly billing |
| `90d` | Last 90 days | Quarterly analysis |
| `ytd` | Since Jan 1 of current year | Annual budget tracking |
| `all` | All time | Lifetime stats |
| `custom` | User-defined start/end | Audits, ad-hoc queries |
### Dashboard Widgets Computed
For any date range, the analytics layer computes:
| Widget | Description |
| ---------------------- | ------------------------------------------------------ |
| **Summary cards** | Total requests, total cost, total tokens, success rate |
| **Daily trend chart** | Cost + tokens per day, stacked by model |
| **Activity heatmap** | Hour-of-day × day-of-week grid, color = request count |
| **Model breakdown** | Pie chart of cost by model |
| **Provider breakdown** | Bar chart of requests by provider |
| **Top API keys** | Table of top 10 keys by cost |
| **Error analysis** | Error rate over time, top error classes |
### Programmatic Access
````ts
import { computeAnalytics } from "@/lib/usageAnalytics";
const analytics = await computeAnalytics(
history, // usage history records
"7d", // time range: "1d" | "7d" | "30d" | "90d" | "ytd" | "all" | "custom"
connectionMap, // provider connection map (connectionId → account name)
{
startDate: "2025-01-01", // optional: for "custom" range
endDate: "2025-06-01", // optional: for "custom" range
}
);
console.log(analytics.summary.totalCost); // 12.34 (cents)
console.log(analytics.byModel[0]); // { model, cost, requests, promptTokens, completionTokens }
---
## Quota Enforcement
Per-API-key quota is enforced in two places:
1. **Soft limit** (`quotaWarnAt`): dashboard warning when usage exceeds threshold
2. **Hard limit** (`quotaLimit`): request rejected with HTTP 429 when exceeded
### Configuration
```ts
// Per API key
await updateApiKey(keyId, {
quotaWarnAt: 5_00, // $5.00 — show warning
quotaLimit: 10_00, // $10.00 — hard stop
quotaWindow: "month", // "day" | "week" | "month" | "all"
});
````
### Enforcement Flow
```
Request ──▶ quotaCheck()
├── Within limit? ──▶ allow
└── Over limit? ──▶ 429 Too Many Requests
with Retry-After header
```
### Quota Snapshots
`quotaSnapshots` table stores **historical quota state** for trend analysis:
| Field | Description |
| ----------- | -------------------------------- | ------ | ------- |
| `apiKeyId` | The key being tracked |
| `window` | "day" | "week" | "month" |
| `used` | Cost used in this window (cents) |
| `limit` | The limit (cents) |
| `resetAt` | When the window resets |
| `createdAt` | When the snapshot was taken |
Snapshots are taken **on every request** that uses > 0 cost, and used to:
- Render the quota progress bar in the dashboard
- Show 30-day quota trend charts
- Trigger alerts when usage approaches the limit
---
## REST API
### List Usage Records
```bash
GET /api/usage?range=7d&limit=100
GET /api/usage?apiKeyId=key-123&range=30d
GET /api/usage?provider=openai&range=1d
```
Response:
```json
{
"records": [
{
"id": "uuid",
"apiKeyId": "key-123",
"provider": "openai",
"model": "gpt-5",
"promptTokens": 1234,
"completionTokens": 567,
"totalTokens": 1801,
"costUsd": 0.005,
"latencyMs": 1234,
"status": "success",
"timestamp": "2026-06-08T12:00:00Z"
}
],
"total": 1234,
"nextCursor": "..."
}
```
### Get Analytics Summary
```bash
GET /api/usage/analytics?range=7d&groupBy=model
```
Response:
```json
{
"summary": {
"totalCost": 12.34,
"totalRequests": 5678,
"totalTokens": 12345678,
"successRate": 0.987,
"avgLatencyMs": 1234
},
"models": [
{ "model": "gpt-5", "cost": 8.5, "requests": 1234, "tokens": 4567890 },
{ "model": "claude-opus-4-6", "cost": 3.84, "requests": 234, "tokens": 234567 }
],
"daily": [
{ "date": "2026-06-01", "cost": 1.5, "requests": 800 },
{ "date": "2026-06-02", "cost": 2.0, "requests": 1000 }
]
}
```
### Query Usage Analytics
Usage data is accessed via the dashboard or MCP tools, not direct REST export endpoints. Available analytics:
- **`/api/usage/analytics`** — aggregated usage metrics (group by model, provider, key)
- **`/api/usage/quota`** — current quota status per API key
- **`/api/usage/history`** — request history logs
---
## MCP Tools
Two MCP tools expose usage data to agents (see `open-sse/mcp-server/tools/`):
| Tool | Description |
| ----------------------- | -------------------------------------------------- |
| `omniroute_cost_report` | Generates a per-key cost report for a given period |
| `omniroute_check_quota` | Returns current quota status for an API key |
Example agent invocation:
```json
{
"tool": "omniroute_cost_report",
"args": { "period": "week" }
}
```
---
## Retention and Cleanup
Usage data grows ~1-10KB per request. At scale, this can be significant.
### Retention Settings
Usage history retention is configured via the Database Settings in the UI or via `/api/settings/database`.
By default, usage history is retained for **90 days**.
### Cleanup
Old records are cleaned up by `src/lib/db/cleanup.ts`:
- Triggered by the background cron process
- Deletes records from `usage_history` older than the configured `usageHistory` retention setting
### Storage Estimation
| Request rate | 30-day storage | 90-day storage |
| --------------- | -------------- | -------------- |
| 100 req/day | ~3MB | ~9MB |
| 1,000 req/day | ~30MB | ~90MB |
| 10,000 req/day | ~300MB | ~900MB |
| 100,000 req/day | ~3GB | ~9GB |
For very high traffic, consider:
- Reducing the retention period via Database Settings
- Using `aggregated_metrics` instead of raw records (only for analytics)
---
## Cost Optimization Tips
### 1. Use the Right Model
```bash
# Quick answer — use cheap + fast
curl -d '{"model":"auto/fast","messages":[...]}'
# Complex task — use quality
curl -d '{"model":"auto/smart","messages":[...]}'
```
### 2. Enable Caching
Anthropic prompt caching saves **90% on repeated context**:
```ts
// The caching is automatic — just include the same large system prompt
const response = await openai.chat({
model: "claude-sonnet-4-5",
system: longSystemPrompt, // Will be cached automatically
messages: [{ role: "user", content: "..." }],
});
```
### 3. Use Compression
RTK + Caveman compression saves **15-95% on tool-heavy sessions**:
```ts
const config = {
compression: {
engine: "rtk",
intensity: "aggressive",
},
};
```
### 4. Set Per-Key Quotas
Always set `quotaLimit` to prevent runaway costs:
```ts
await updateApiKey(keyId, { quotaLimit: 10_00 }); // $10/month cap
```
### 5. Audit Top Consumers
Use the dashboard or **`/api/usage/analytics`** to group by API key and sort by cost:
```bash
GET /api/usage/analytics?groupBy=apiKey
```
---
## Troubleshooting
### "Cost is higher than expected"
1. Check **`/api/usage/analytics?groupBy=model`** — find the expensive model
2. Check **`/api/usage/analytics?groupBy=apiKey`** — find the heavy consumer
3. Verify pricing data is up to date: `POST /api/pricing/sync`
### "Records missing"
- Check DB retention settings under Dashboard → Database → Cleanup — old records are deleted by the periodic cleanup task (`src/lib/db/cleanup.ts`)
- Check for errors in `src/lib/db/usage*.ts` — DB write failures are logged but not surfaced
- Verify the request actually reached `chatCore` — check combo routing
### "Quota not enforcing"
- Check the key's `quotaLimit` setting
- Verify `quotaWindow` is set correctly
- Look for `quotaSnapshots` records — they should be created on every request
---
## See Also
- [DATABASE_GUIDE.md](../ops/DATABASE_GUIDE.md) — Schema for usage tables
- [ENVIRONMENT.md](../reference/ENVIRONMENT.md#18-pricing-sync) — pricing sync env vars
- [AUTO-COMBO.md](../routing/AUTO-COMBO.md) — How `auto/fast`, `auto/cheap` reduce cost
- [API_REFERENCE.md](../reference/API_REFERENCE.md) — Full `/api/usage/*` reference
- Source: `open-sse/services/usage.ts`, `src/lib/usageAnalytics.ts`, `src/lib/db/usage*.ts`
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
{
"title": "Guides",
"description": "Technical guides for operators and developers",
"pages": [
"SETUP_GUIDE",
"USER_GUIDE",
"DOCKER_GUIDE",
"ELECTRON_GUIDE",
"FEATURES",
"FREE_PROVIDER_RANKINGS",
"COST_TRACKING",
"I18N",
"KIRO_SETUP",
"CODEX-CLI-CONFIGURATION",
"PWA_GUIDE",
"TERMUX_GUIDE",
"TIERS",
"USAGE_QUOTA_GUIDE",
"UNINSTALL"
]
}