chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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>`.
|
||||
@@ -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
|
||||
@@ -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 85–88% 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.138–v0.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 80–85% 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`.
|
||||
@@ -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).
|
||||
@@ -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
|
||||
@@ -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`
|
||||
@@ -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 W1–W4** — 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`.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 🎨 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
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 📊 Analytics
|
||||
|
||||
Comprehensive usage analytics with token consumption, cost estimates, activity heatmaps, weekly distribution charts, and per-provider breakdowns.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 🏥 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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 🔧 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).
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 🎮 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 (W1–W4), `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
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 🔧 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 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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 🌐 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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 🔑 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`.
|
||||
@@ -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 `1–100`. |
|
||||
|
||||
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.
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
> 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>
|
||||
```
|
||||
@@ -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).
|
||||
@@ -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) |
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
@@ -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"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user