mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Release v3.8.40
v3.8.40 cycle integration → main. All test gates green (Unit/Integration/Coverage/Node-compat/Quality-Ratchet). The only red check, 'PR Test Policy', is the test-masking heuristic firing on the cumulative ~57-commit release diff (legitimate assert consolidations already reviewed per-PR — Gemini CLI removal #5246, retired GPT models #5280, provider catalog refreshes); overridden with --admin per the documented release-PR convention. CodeQL/SonarQube advisory scans non-blocking; #5278's code already passed CodeQL on main. Homologated on VPS 192.168.0.15 (v3.8.40 healthy).
This commit is contained in:
committed by
GitHub
parent
1c18be4f8f
commit
7c23dab64d
19
.env.example
19
.env.example
@@ -413,7 +413,6 @@ NEXT_PUBLIC_CLOUD_URL=
|
||||
# debugging or when routing through a corporate mirror. Used by:
|
||||
# open-sse/services/usage.ts.
|
||||
#OMNIROUTE_CROF_USAGE_URL=https://crof.ai/usage_api/
|
||||
#OMNIROUTE_GEMINI_CLI_USAGE_URL=https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist
|
||||
#OMNIROUTE_CODEWHISPERER_BASE_URL=https://codewhisperer.us-east-1.amazonaws.com
|
||||
#OMNIROUTE_OPENCODE_QUOTA_URL=https://opencode.ai/zen/go/v1/quota
|
||||
#OMNIROUTE_OPENCODE_GO_QUOTA_URL=https://api.z.ai/api/monitor/usage/quota/limit
|
||||
@@ -721,7 +720,7 @@ CODEX_OAUTH_CLIENT_ID=app_EMoamEEZ73f0CkXaXp7hrann
|
||||
# Used by: open-sse/executors/theoldllm.ts. Default: 30000 (30s).
|
||||
# THEOLDLLM_NAV_TIMEOUT_MS=30000
|
||||
|
||||
# ── Gemini / Gemini CLI / Antigravity / Windsurf (all Google-based) ──
|
||||
# ── Gemini / Antigravity / Windsurf (all Google-based) ──
|
||||
# These providers ship public OAuth client_id/secret values (or Firebase Web
|
||||
# keys) embedded in their public CLIs/binaries. Defaults are baked into the
|
||||
# code via open-sse/utils/publicCreds.ts — leave the env vars unset to use
|
||||
@@ -730,8 +729,6 @@ CODEX_OAUTH_CLIENT_ID=app_EMoamEEZ73f0CkXaXp7hrann
|
||||
#
|
||||
# GEMINI_OAUTH_CLIENT_ID=
|
||||
# GEMINI_OAUTH_CLIENT_SECRET=
|
||||
# GEMINI_CLI_OAUTH_CLIENT_ID=
|
||||
# GEMINI_CLI_OAUTH_CLIENT_SECRET=
|
||||
# ANTIGRAVITY_OAUTH_CLIENT_ID=
|
||||
# ANTIGRAVITY_OAUTH_CLIENT_SECRET=
|
||||
# WINDSURF_FIREBASE_API_KEY=
|
||||
@@ -819,7 +816,7 @@ GITHUB_OAUTH_CLIENT_ID=Iv1.b507a08c87ecfe98
|
||||
# VISION_BRIDGE_API_KEY=
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# ⚠️ GOOGLE OAUTH (Antigravity, Gemini CLI) & OTHER PROVIDERS — REMOTE SERVERS
|
||||
# ⚠️ GOOGLE OAUTH (Antigravity) & OTHER PROVIDERS — REMOTE SERVERS
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# The default Client IDs above ONLY work when OmniRoute runs on localhost.
|
||||
# For remote/VPS hosting (including Docker containers on remote servers):
|
||||
@@ -847,7 +844,7 @@ GITHUB_OAUTH_CLIENT_ID=Iv1.b507a08c87ecfe98
|
||||
# Used by: open-sse/executors/base.ts — buildHeaders() dynamic lookup.
|
||||
# Update these when providers release new CLI versions to avoid blocks.
|
||||
|
||||
CLAUDE_USER_AGENT="claude-cli/2.1.187 (external, cli)"
|
||||
CLAUDE_USER_AGENT="claude-cli/2.1.195 (external, cli)"
|
||||
|
||||
# Disable the deterministic tool-name cloak applied on both Anthropic-bound paths
|
||||
# (executors/base.ts native OAuth + executors/cliproxyapi.ts CLIProxyAPI) —
|
||||
@@ -857,7 +854,7 @@ CLAUDE_USER_AGENT="claude-cli/2.1.187 (external, cli)"
|
||||
# forward the original names verbatim (debugging only).
|
||||
# CLAUDE_DISABLE_TOOL_NAME_CLOAK=false
|
||||
CODEX_USER_AGENT="codex-cli/0.142.0 (Windows 10.0.26200; x64)"
|
||||
GITHUB_USER_AGENT="GitHubCopilotChat/0.45.1"
|
||||
GITHUB_USER_AGENT="GitHubCopilotChat/0.54.0"
|
||||
ANTIGRAVITY_USER_AGENT="antigravity/2.0.1 linux/arm64 google-api-nodejs-client/10.3.0"
|
||||
KIRO_USER_AGENT="AWS-SDK-JS/3.0.0 kiro-ide/1.0.0"
|
||||
# KIRO_VERIFY_FULL_CRC=false # opt-in: full per-frame message CRC validation on the Kiro event stream (debug corrupted streams; prelude CRC + TLS already protect framing)
|
||||
@@ -872,9 +869,8 @@ KIRO_USER_AGENT="AWS-SDK-JS/3.0.0 kiro-ide/1.0.0"
|
||||
# Used by: open-sse/executors/kiro.ts
|
||||
# KIRO_VERIFY_FULL_CRC=false
|
||||
QODER_USER_AGENT="Qoder-Cli"
|
||||
QWEN_USER_AGENT="QwenCode/0.15.11 (linux; x64)"
|
||||
QWEN_USER_AGENT="QwenCode/0.19.3 (linux; x64)"
|
||||
CURSOR_USER_AGENT="Cursor/3.4"
|
||||
GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0"
|
||||
|
||||
# Override Codex client version sent in headers independently of the
|
||||
# CODEX_USER_AGENT string. Used by: open-sse/config/codexClient.ts.
|
||||
@@ -1362,6 +1358,11 @@ APP_LOG_TO_FILE=true
|
||||
# Health check result cache TTL (ms). Default: 30000 (30s)
|
||||
# PROXY_HEALTH_CACHE_TTL_MS=30000
|
||||
|
||||
# Unhealthy health check result cache TTL (ms). Default: 2000 (2s)
|
||||
# Keeps transient fast-fail timeouts from poisoning a proxy for the full
|
||||
# healthy-result cache window under high concurrency.
|
||||
# PROXY_HEALTH_UNHEALTHY_CACHE_TTL_MS=2000
|
||||
|
||||
# Allow OAuth and provider validation flows to bypass a pinned proxy and connect
|
||||
# directly when proxy reachability pre-checks fail. Default: false.
|
||||
# Also configurable from Dashboard > Settings > Feature Flags.
|
||||
|
||||
12
.github/workflows/docker-publish.yml
vendored
12
.github/workflows/docker-publish.yml
vendored
@@ -98,11 +98,13 @@ jobs:
|
||||
PROMOTE="${PROMOTE_INPUT:-false}"
|
||||
else
|
||||
git fetch --tags --quiet || true
|
||||
HIGHEST=$(git tag -l 'v[0-9]*' | sed 's/^v//' | grep -vE -- '-(rc|alpha|beta|pre|next)' | sort -V | tail -1 || echo "")
|
||||
if [ -n "$HIGHEST" ] && [ "$VERSION" = "$HIGHEST" ]; then
|
||||
PROMOTE="true"
|
||||
else
|
||||
echo "Version $VERSION is not the highest semver tag (highest=${HIGHEST:-<none>}). Not promoting :latest."
|
||||
# Decide via the extracted helper, which folds VERSION into the
|
||||
# candidate set so the result is independent of git-tag sync timing
|
||||
# on `release` events (#5301). Without that, the freshly-created tag
|
||||
# is often not yet visible here and :latest stays a release behind.
|
||||
PROMOTE=$(git tag -l 'v[0-9]*' | bash scripts/ci/should-promote-latest.sh "$VERSION")
|
||||
if [ "$PROMOTE" != "true" ]; then
|
||||
echo "Version $VERSION is not the highest stable semver. Not promoting :latest."
|
||||
fi
|
||||
fi
|
||||
echo "promote_latest=$PROMOTE" >> "$GITHUB_OUTPUT"
|
||||
|
||||
2
.github/workflows/wiki-sync.yml
vendored
2
.github/workflows/wiki-sync.yml
vendored
@@ -9,7 +9,7 @@ name: Wiki Sync
|
||||
# It does NOT overwrite existing wiki pages by default: several docs sources still carry
|
||||
# stale counts (e.g. ARCHITECTURE.md says "177 providers" while the wiki cover is 226),
|
||||
# so blind overwrite would regress the wiki. Full content parity (--update-existing) is
|
||||
# gated on regenerating those sources — see docs/ops/DOCUMENTATION_AUDIT_REPORT.md.
|
||||
# gated on regenerating those sources first.
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"$comment_purpose": "OPT-IN agent-lsp / LSP-in-the-loop (Quality Gates Fase 7 Task 15). Copy this file to `.mcp.json` to enable. It exposes a TypeScript language server to coding agents (Claude Code, etc.) so they get diagnostics / hover / go-to-definition / blast-radius BEFORE writing code — turning 'invented symbol' review-catches into impossible-at-edit-time. Pairs with `npm run typecheck:core` as a compile-before-claim check.",
|
||||
"$comment_safety": "Shipped as `.example` (NOT `.mcp.json`) on purpose so it never auto-loads an unvetted server into everyone's session. Pick an MCP<->LSP bridge you trust and have verified locally, then drop in its package + args below. A broken MCP entry only logs a connection error; it does not break agent sessions. The underlying language server is `typescript-language-server` (npm, mature) — install via `npm i -g typescript-language-server typescript` or rely on npx.",
|
||||
"mcpServers": {
|
||||
"typescript-lsp": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"<your-mcp-lsp-bridge>",
|
||||
"--lsp",
|
||||
"typescript-language-server",
|
||||
"--stdio"
|
||||
],
|
||||
"$note": "Replace <your-mcp-lsp-bridge> with the concrete MCP<->LSP adapter you chose. It must speak MCP on stdio and proxy to `typescript-language-server --stdio`. Scope it to this repo's tsconfig (open-sse/tsconfig.json / tsconfig.json) for accurate diagnostics."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -196,7 +196,7 @@ Every field is optional. Defaults mirror v0.1.0 behaviour so existing `opencode.
|
||||
| `combos` | `boolean` | `true` | Discover `/api/combos` and surface them as pseudo-models with LCD capabilities. Combos are keyed under the `combo/<slug>` namespace and labelled `Combo: <name>` in the model picker so they're distinguishable from raw provider/model pairs. |
|
||||
| `enrichment` | `boolean` | `true` | Pull display names from `/api/pricing/models` AND per-million-token pricing (`input`, `output`, `cached` → `cacheRead`, `cache_creation` → `cacheWrite`) from `/api/pricing`, then overlay both onto the live catalog (so the UI shows `Claude 4.7 Opus` with `cost.input: 5`, `cost.output: 25` instead of raw IDs and zeroed cost). |
|
||||
| `compressionMetadata` | `boolean` | `false` | Pull `/api/context/combos` so combo names get tagged with their compression pipeline, e.g. `Combo: claude-primary [rtk🟡 → caveman🟠]`. Intensity tokens render as traffic-light emoji (🟢 lite/minimal · 🟡 standard · 🟠 aggressive/full · 🔴 ultra) so the picker advertises "how compressed" each combo is at a glance. |
|
||||
| `providerTag` | `boolean` | `true` | Prepend a short upstream-provider label to the enriched display name with `" - "` separator, so `cc/claude-opus-4-7 → Claude - Claude Opus 4.7` differs visibly from `kr/claude-opus-4-7 → Kiro - Claude Opus 4.7` in the OC TUI model picker. Label resolution: use `/api/pricing/models[<alias>].name` verbatim when ≤8 chars (e.g. `Claude`, `Kiro`, `Codex`, `Qwen`), otherwise fall back to `UPPER(alias)` (e.g. `GitHub Models` → `GHM`, `Gemini-cli` → `GEMINI-CLI`). Idempotent. Combos intentionally skipped (the `Combo: ` prefix already conveys multi-upstream). |
|
||||
| `providerTag` | `boolean` | `true` | Prepend a short upstream-provider label to the enriched display name with `" - "` separator, so `cc/claude-opus-4-7 → Claude - Claude Opus 4.7` differs visibly from `kr/claude-opus-4-7 → Kiro - Claude Opus 4.7` in the OC TUI model picker. Label resolution: use `/api/pricing/models[<alias>].name` verbatim when ≤8 chars (e.g. `Claude`, `Kiro`, `Codex`, `Qwen`), otherwise fall back to `UPPER(alias)` (e.g. `GitHub Models` → `GHM`, `Gemini` → `GEMINI`). Idempotent. Combos intentionally skipped (the `Combo: ` prefix already conveys multi-upstream). |
|
||||
| `usableOnly` | `boolean` | `false` | Read `/api/providers` and filter the catalog to providers that have at least one connection with `isActive: true` AND `testStatus: 'active'`. Subtract-filter semantics: providers unknown to BOTH the pricing-models catalog AND the connection table pass through (so synthetic prefixes like `agentrouter/*` survive). On fetch failure the filter is disabled for the refresh — never hides the whole catalog. |
|
||||
| `diskCache` | `boolean` | `true` | Persist the last successful `/v1/models` + `/api/combos` + enrichment + connections + compression snapshot to `${OPENCODE_DATA_DIR ?? ~/.local/share/opencode}/plugins/omniroute-<providerId>.json`. On a subsequent cold start where `/v1/models` throws (network down / IP whitelist drop / 5xx) the static block hydrates from the snapshot so OC's model picker survives offline. Soft-fail on read/write — never blocks publishing. |
|
||||
| `geminiSanitization` | `boolean` | `true` | Strip `$schema`/`$ref`/`additionalProperties` from tool params when the model id matches `gemini` |
|
||||
|
||||
@@ -1225,7 +1225,7 @@ export interface OmniRouteEnrichmentEntry {
|
||||
cacheWrite?: number;
|
||||
};
|
||||
/**
|
||||
* Provider alias prefix seen in `/v1/models` ids (e.g. `cc`, `gemini-cli`).
|
||||
* Provider alias prefix seen in `/v1/models` ids (e.g. `cc`, `gemini`).
|
||||
* Populated by `defaultOmniRouteEnrichmentFetcher` from
|
||||
* `/api/pricing/models` keys. Drives the `usableOnly` alias↔canonical
|
||||
* resolution.
|
||||
@@ -1233,7 +1233,7 @@ export interface OmniRouteEnrichmentEntry {
|
||||
providerAlias?: string;
|
||||
/**
|
||||
* Canonical provider id used by `/api/providers` connections (e.g.
|
||||
* `claude`, `gemini-cli`, `kiro`). Populated from the per-provider
|
||||
* `claude`, `gemini`, `kiro`). Populated from the per-provider
|
||||
* `entry.id` field inside `/api/pricing/models`.
|
||||
*/
|
||||
providerCanonical?: string;
|
||||
@@ -2046,7 +2046,7 @@ export function formatCompressionPipeline(pipeline: OmniRouteCompressionStep[]):
|
||||
export interface OmniRouteProviderConnection {
|
||||
/** Connection UUID. */
|
||||
id: string;
|
||||
/** Canonical provider id, e.g. `claude`, `gemini-cli`, `kiro`. Matches `entry.id` in `/api/pricing/models`. */
|
||||
/** Canonical provider id, e.g. `claude`, `gemini`, `kiro`. Matches `entry.id` in `/api/pricing/models`. */
|
||||
provider: string;
|
||||
/** Connection auth flavor, e.g. `apikey`, `oauth`, `cookie`. */
|
||||
authType?: string;
|
||||
@@ -2125,7 +2125,7 @@ export const defaultOmniRouteProvidersFetcher: OmniRouteProvidersFetcher = async
|
||||
* walk only the namespaced keys to derive the alias↔canonical mapping).
|
||||
*
|
||||
* Returns:
|
||||
* - `aliases`: set of alias prefixes safe to keep (e.g. `cc`, `gemini-cli`).
|
||||
* - `aliases`: set of alias prefixes safe to keep (e.g. `cc`, `gemini`).
|
||||
* - `canonicals`: set of canonical provider ids (e.g. `claude`, `kiro`).
|
||||
*
|
||||
* Callers should treat membership in EITHER set as "usable" — raw model
|
||||
@@ -2174,7 +2174,7 @@ export function usableProviderAliasSet(
|
||||
}
|
||||
// Always include every usable canonical as an alias too — handles the
|
||||
// common case where `/v1/models` ids use the canonical id directly
|
||||
// (e.g. `gemini-cli/gemini-1.5-pro`).
|
||||
// (e.g. `gemini/gemini-1.5-pro`).
|
||||
for (const canonical of usableCanonicals) aliases.add(canonical);
|
||||
return { aliases, canonicals: usableCanonicals, knownAliases };
|
||||
}
|
||||
@@ -3174,7 +3174,6 @@ export function sanitizeGeminiToolSchemas(payload: unknown): unknown {
|
||||
* `gemini-2.5-flash`, etc.)
|
||||
* - `models/gemini-…` (Google Generative AI canonical id form)
|
||||
* - `google-vertex/gemini-…` (OpenCode + AI-SDK Vertex routing prefix)
|
||||
* - `gemini-cli/…` (real OmniRoute alias surfaced on b35 prod `/v1/models`)
|
||||
*
|
||||
* Liberal by design: a false positive (cleaning a payload that didn't
|
||||
* need cleaning) costs only a structuredClone + one walk; a false negative
|
||||
|
||||
@@ -1311,9 +1311,9 @@ test("config: providerTag (default-on) prepends '<provider> - ' to enriched raw-
|
||||
"gemini-3-flash",
|
||||
{
|
||||
name: "Gemini 3 Flash",
|
||||
providerAlias: "gemini-cli",
|
||||
providerCanonical: "gemini-cli",
|
||||
providerDisplayName: "Gemini-cli",
|
||||
providerAlias: "gemini",
|
||||
providerCanonical: "gemini",
|
||||
providerDisplayName: "Gemini",
|
||||
},
|
||||
],
|
||||
])
|
||||
@@ -1335,10 +1335,7 @@ test("config: providerTag (default-on) prepends '<provider> - ' to enriched raw-
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"].name,
|
||||
"Claude - Claude Sonnet 4.6"
|
||||
);
|
||||
assert.equal(
|
||||
entry.models["opencode-omniroute/gemini-3-flash"].name,
|
||||
"Gemini-cli - Gemini 3 Flash"
|
||||
);
|
||||
assert.equal(entry.models["opencode-omniroute/gemini-3-flash"].name, "Gemini - Gemini 3 Flash");
|
||||
// Combos stay untouched — `Combo: ` prefix already conveys multi-upstream.
|
||||
assert.equal(entry.models["opencode-omniroute/claude-tier"].name, "Claude Tier");
|
||||
});
|
||||
|
||||
@@ -212,8 +212,8 @@ test("shouldSanitizeForGemini: google-vertex/gemini-1.5-flash → true", () => {
|
||||
assert.equal(shouldSanitizeForGemini({ model: "google-vertex/gemini-1.5-flash" }), true);
|
||||
});
|
||||
|
||||
test("shouldSanitizeForGemini: gemini-cli/gemini-2.5-pro → true (real OmniRoute alias)", () => {
|
||||
assert.equal(shouldSanitizeForGemini({ model: "gemini-cli/gemini-2.5-pro" }), true);
|
||||
test("shouldSanitizeForGemini: gemini/gemini-2.5-pro → true", () => {
|
||||
assert.equal(shouldSanitizeForGemini({ model: "gemini/gemini-2.5-pro" }), true);
|
||||
});
|
||||
|
||||
test("shouldSanitizeForGemini: claude-sonnet-4 → false", () => {
|
||||
|
||||
68
AGENTS.md
68
AGENTS.md
@@ -3,14 +3,14 @@
|
||||
## Project
|
||||
|
||||
Unified AI proxy/router — route any LLM through one endpoint. Multi-provider support
|
||||
with **231 provider entries** (OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Fireworks,
|
||||
with **237 provider entries** (OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Fireworks,
|
||||
Cohere, NVIDIA, Cerebras, Pollinations, Puter, Cloudflare AI, HuggingFace, DeepInfra,
|
||||
SambaNova, Meta Llama API, Moonshot AI, AI21 Labs, Databricks, Snowflake, and many more)
|
||||
with **MCP Server** (87 tools), **A2A v0.3 Protocol**, and **Electron desktop app**.
|
||||
with **MCP Server** (94 tools), **A2A v0.3 Protocol**, and **Electron desktop app**.
|
||||
|
||||
> **Live counts (v3.8.31)**: providers 231 · MCP tools 87 · MCP scopes 30 · A2A skills 6 ·
|
||||
> open-sse services 115 · routing strategies 15 · auto-combo scoring factors 9 ·
|
||||
> DB modules 83 · DB migrations 97 · base tables 17 · search providers 11 ·
|
||||
> **Live counts (v3.8.40)**: providers 237 · MCP tools 94 · MCP scopes 30 · A2A skills 6 ·
|
||||
> open-sse services 298 · routing strategies 17 · auto-combo scoring factors 12 ·
|
||||
> DB modules 94 · DB migrations 106 · base tables 17 · search providers 11 ·
|
||||
> i18n locales 42. **Refresh with `npm run check:docs-all`.**
|
||||
|
||||
## Doc Accuracy Discipline (read before writing any doc)
|
||||
@@ -267,7 +267,7 @@ Zod schemas, and unit tests aligned when editing.
|
||||
|
||||
### Provider Categories
|
||||
|
||||
- **Free** (4): Qoder AI, Qwen Code, Gemini CLI (deprecated), Kiro AI
|
||||
- **Free** (3): Qoder AI, Qwen Code, Kiro AI
|
||||
- **OAuth** (14): Claude Code, Antigravity, Codex, GitHub Copilot, Cursor, Kimi Coding, Kilo Code, Cline, Qwen (⚠️ free tier discontinued 2026-04-15), Kiro, Qoder, Gemini, Windsurf (v3.8), GitLab Duo (v3.8)
|
||||
- **API Key** (120+): OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Perplexity,
|
||||
Together, Fireworks, Cerebras, Cohere, NVIDIA, Nebius, SiliconFlow, Hyperbolic,
|
||||
@@ -291,7 +291,7 @@ Providers are registered in `src/shared/constants/providers.ts` with Zod validat
|
||||
### Executors (`open-sse/executors/`)
|
||||
|
||||
Provider-specific request executors: `base.ts`, `default.ts`, `cursor.ts`, `codex.ts`,
|
||||
`antigravity.ts`, `github.ts`, `gemini-cli.ts`, `kiro.ts`, `qoder.ts`, `vertex.ts`,
|
||||
`antigravity.ts`, `github.ts`, `kiro.ts`, `qoder.ts`, `vertex.ts`,
|
||||
`cloudflare-ai.ts`, `opencode.ts`, `pollinations.ts`, `puter.ts`.
|
||||
|
||||
#### Executor Internals
|
||||
@@ -391,7 +391,7 @@ Policy engine modules: `policyEngine.ts`, `comboResolver.ts`, `costRules.ts`,
|
||||
|
||||
### MCP Server (`open-sse/mcp-server/`)
|
||||
|
||||
**87 tools** total (`TOTAL_MCP_TOOL_COUNT`, `open-sse/mcp-server/server.ts`): a 33-entry base registry (`MCP_TOOLS` in `schemas/tools.ts`, bundling the core / cache / compression / 1proxy / advanced tools) **plus** standalone module sets — memory (3), skill (4), agentSkill (3), gamification (8), plugin (8), notion (6), obsidian (22). 3 transports (stdio / SSE / Streamable HTTP). Scoped auth (30 scopes — see `OMNIROUTE_MCP_SCOPES`), Zod schemas. See [`docs/frameworks/MCP-SERVER.md`](docs/frameworks/MCP-SERVER.md).
|
||||
**94 tools** total (`TOTAL_MCP_TOOL_COUNT`, `open-sse/mcp-server/server.ts`): a 34-entry base registry (`MCP_TOOLS` in `schemas/tools.ts`, bundling the core / cache / compression / 1proxy / advanced tools) **plus** standalone module sets — memory (3), skill (4), agentSkill (3), pool (6), gamification (8), plugin (8), notion (6), obsidian (22). 3 transports (stdio / SSE / Streamable HTTP). Scoped auth (30 scopes — see `OMNIROUTE_MCP_SCOPES`), Zod schemas. See [`docs/frameworks/MCP-SERVER.md`](docs/frameworks/MCP-SERVER.md).
|
||||
|
||||
**Core tools** (20): get_health, list_combos, get_combo_metrics, switch_combo, check_quota,
|
||||
route_request, cost_report, list_models_catalog, web_search, simulate_route, set_budget_guard,
|
||||
@@ -534,33 +534,33 @@ Cloudflare Quick/Named, ngrok, Tailscale Funnel. See [`docs/ops/TUNNELS_GUIDE.md
|
||||
|
||||
For any non-trivial change, read the matching deep-dive first:
|
||||
|
||||
| Area | Doc |
|
||||
| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Repo navigation | [`docs/architecture/REPOSITORY_MAP.md`](docs/architecture/REPOSITORY_MAP.md) |
|
||||
| Architecture | [`docs/architecture/ARCHITECTURE.md`](docs/architecture/ARCHITECTURE.md) |
|
||||
| Engineering reference | [`docs/architecture/CODEBASE_DOCUMENTATION.md`](docs/architecture/CODEBASE_DOCUMENTATION.md) |
|
||||
| Auto-Combo (12-factor, 15 strategies) | [`docs/routing/AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md) |
|
||||
| Resilience (3 layers) | [`docs/architecture/RESILIENCE_GUIDE.md`](docs/architecture/RESILIENCE_GUIDE.md) |
|
||||
| Skills | [`docs/frameworks/SKILLS.md`](docs/frameworks/SKILLS.md) |
|
||||
| Memory | [`docs/frameworks/MEMORY.md`](docs/frameworks/MEMORY.md) |
|
||||
| Cloud agents | [`docs/frameworks/CLOUD_AGENT.md`](docs/frameworks/CLOUD_AGENT.md) |
|
||||
| Guardrails | [`docs/security/GUARDRAILS.md`](docs/security/GUARDRAILS.md) |
|
||||
| Evals | [`docs/frameworks/EVALS.md`](docs/frameworks/EVALS.md) |
|
||||
| Compliance | [`docs/security/COMPLIANCE.md`](docs/security/COMPLIANCE.md) |
|
||||
| Webhooks | [`docs/frameworks/WEBHOOKS.md`](docs/frameworks/WEBHOOKS.md) |
|
||||
| Authz | [`docs/architecture/AUTHZ_GUIDE.md`](docs/architecture/AUTHZ_GUIDE.md) |
|
||||
| Stealth | [`docs/security/STEALTH_GUIDE.md`](docs/security/STEALTH_GUIDE.md) |
|
||||
| Reasoning replay | [`docs/routing/REASONING_REPLAY.md`](docs/routing/REASONING_REPLAY.md) |
|
||||
| Agent protocols (A2A / ACP / Cloud) | [`docs/frameworks/AGENT_PROTOCOLS_GUIDE.md`](docs/frameworks/AGENT_PROTOCOLS_GUIDE.md) |
|
||||
| MCP server | [`docs/frameworks/MCP-SERVER.md`](docs/frameworks/MCP-SERVER.md) |
|
||||
| A2A server | [`docs/frameworks/A2A-SERVER.md`](docs/frameworks/A2A-SERVER.md) |
|
||||
| Area | Doc |
|
||||
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
|
||||
| Repo navigation | [`docs/architecture/REPOSITORY_MAP.md`](docs/architecture/REPOSITORY_MAP.md) |
|
||||
| Architecture | [`docs/architecture/ARCHITECTURE.md`](docs/architecture/ARCHITECTURE.md) |
|
||||
| Engineering reference | [`docs/architecture/CODEBASE_DOCUMENTATION.md`](docs/architecture/CODEBASE_DOCUMENTATION.md) |
|
||||
| Auto-Combo (12-factor, 17 strategies) | [`docs/routing/AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md) |
|
||||
| Resilience (3 layers) | [`docs/architecture/RESILIENCE_GUIDE.md`](docs/architecture/RESILIENCE_GUIDE.md) |
|
||||
| Skills | [`docs/frameworks/SKILLS.md`](docs/frameworks/SKILLS.md) |
|
||||
| Memory | [`docs/frameworks/MEMORY.md`](docs/frameworks/MEMORY.md) |
|
||||
| Cloud agents | [`docs/frameworks/CLOUD_AGENT.md`](docs/frameworks/CLOUD_AGENT.md) |
|
||||
| Guardrails | [`docs/security/GUARDRAILS.md`](docs/security/GUARDRAILS.md) |
|
||||
| Evals | [`docs/frameworks/EVALS.md`](docs/frameworks/EVALS.md) |
|
||||
| Compliance | [`docs/security/COMPLIANCE.md`](docs/security/COMPLIANCE.md) |
|
||||
| Webhooks | [`docs/frameworks/WEBHOOKS.md`](docs/frameworks/WEBHOOKS.md) |
|
||||
| Authz | [`docs/architecture/AUTHZ_GUIDE.md`](docs/architecture/AUTHZ_GUIDE.md) |
|
||||
| Stealth | [`docs/security/STEALTH_GUIDE.md`](docs/security/STEALTH_GUIDE.md) |
|
||||
| Reasoning replay | [`docs/routing/REASONING_REPLAY.md`](docs/routing/REASONING_REPLAY.md) |
|
||||
| Agent protocols (A2A / ACP / Cloud) | [`docs/frameworks/AGENT_PROTOCOLS_GUIDE.md`](docs/frameworks/AGENT_PROTOCOLS_GUIDE.md) |
|
||||
| MCP server | [`docs/frameworks/MCP-SERVER.md`](docs/frameworks/MCP-SERVER.md) |
|
||||
| A2A server | [`docs/frameworks/A2A-SERVER.md`](docs/frameworks/A2A-SERVER.md) |
|
||||
| API reference | [`docs/reference/API_REFERENCE.md`](docs/reference/API_REFERENCE.md) + [`docs/openapi.yaml`](docs/openapi.yaml) |
|
||||
| Provider catalog (auto-generated) | [`docs/reference/PROVIDER_REFERENCE.md`](docs/reference/PROVIDER_REFERENCE.md) |
|
||||
| Tunnels | [`docs/ops/TUNNELS_GUIDE.md`](docs/ops/TUNNELS_GUIDE.md) |
|
||||
| Electron desktop | [`docs/guides/ELECTRON_GUIDE.md`](docs/guides/ELECTRON_GUIDE.md) |
|
||||
| Release flow | [`docs/ops/RELEASE_CHECKLIST.md`](docs/ops/RELEASE_CHECKLIST.md) |
|
||||
| Quality gates (35 gates, allowlist policy) | [`docs/architecture/QUALITY_GATES.md`](docs/architecture/QUALITY_GATES.md) |
|
||||
| Cluster opt-in profiles (memory, bifrost) | [`docs/architecture/cluster-decisions.md`](docs/architecture/cluster-decisions.md) |
|
||||
| Provider catalog (auto-generated) | [`docs/reference/PROVIDER_REFERENCE.md`](docs/reference/PROVIDER_REFERENCE.md) |
|
||||
| Tunnels | [`docs/ops/TUNNELS_GUIDE.md`](docs/ops/TUNNELS_GUIDE.md) |
|
||||
| Electron desktop | [`docs/guides/ELECTRON_GUIDE.md`](docs/guides/ELECTRON_GUIDE.md) |
|
||||
| Release flow | [`docs/ops/RELEASE_CHECKLIST.md`](docs/ops/RELEASE_CHECKLIST.md) |
|
||||
| Quality gates (35 gates, allowlist policy) | [`docs/architecture/QUALITY_GATES.md`](docs/architecture/QUALITY_GATES.md) |
|
||||
| Cluster opt-in profiles (memory, bifrost) | [`docs/architecture/cluster-decisions.md`](docs/architecture/cluster-decisions.md) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
77
CHANGELOG.md
77
CHANGELOG.md
@@ -4,6 +4,81 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.40] — TBD
|
||||
|
||||
_In development — bullets added per PR; finalized at release._
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(compression): relevance extractive engine** — a new opt-in compression engine that scores each sentence by term-overlap (Jaccard) with the user's last query minus a length/boilerplate penalty, greedily keeps the most relevant within a budget, and reconstructs the original order. Pure-string, deterministic, ReDoS-safe (char-code tokenization, no `RegExp` over user input), fail-open, default off. Ideal for trimming long pasted RAG context / tool output to what's relevant. Sentences carrying real signal (digits/URLs/errors/code/paths) are never dropped; `overlapThreshold`/`budgetPercent`/`boilerplateWeight` are configurable. Tier-2 item of the compression feature-extraction roadmap (#7). ([#5289](https://github.com/diegosouzapw/OmniRoute/pull/5289))
|
||||
- **feat(compression): hard-budget mode — compress to ≤ N tokens** — a deterministic post-pass (`targetTokens` / `targetRatio`, default unset → no-op) that trims a body to a token budget. It ranks sentences/lines by average `scoreToken` ascending and drops the lowest-saliency ones until the body fits (measured by the exact cl100k `countTextTokens`), preserving original order. Lines carrying real signal (digits, URLs, `Error:`-family, code fences, stack `at`-frames, multi-segment paths, `key=value`) are never dropped; the budget is distributed proportionally across messages so the total stays ≤ target; an unreachable target (all-preserved) surfaces a `validationWarnings` note instead of failing silently. Does NOT touch the `estimateCompressionTokens` budget-gate estimator. Tier-3 item of the compression feature-extraction roadmap (#17). ([#5288](https://github.com/diegosouzapw/OmniRoute/pull/5288), follow-up [#5291](https://github.com/diegosouzapw/OmniRoute/pull/5291))
|
||||
- **feat(compression): result memoization for deterministic engines (opt-in)** — caches `(input, config) → result` for provably pure, stateless modes (`lite`/`standard`/`rtk` and stacked pipelines of `{lite,caveman,rtk}`) to skip recompute on the hot path. Opt-in via `memoizeCompressionResults` (default off → zero behavior change). Conservative opt-in whitelist (stateful `ccr`/`session-dedup` — which write the cross-request CCR store — and model-backed `ultra`/`aggressive`/`llmlingua` are never cached), principal-scoped (skipped without a principal, so no cross-principal body leak), and clone-on-store + clone-on-read. Tier-3 item of the compression feature-extraction roadmap (#21). ([#5286](https://github.com/diegosouzapw/OmniRoute/pull/5286))
|
||||
- **feat(compression): inline transparency annotation** — surfaces `tokens=847→312; rules: filler×8, dedup×2` derived from existing compression stats. The `X-OmniRoute-Compression` response header is extended **append-only** (the `mode; source=X` prefix stays byte-identical, so existing header parsers don't break) and the compression studio cockpit shows a matching badge. Zero new computation — it aggregates the `rulesApplied`/`techniquesUsed` already on the stats. Tier-3 item of the compression feature-extraction roadmap (#18). ([#5284](https://github.com/diegosouzapw/OmniRoute/pull/5284))
|
||||
- **feat(compression): saliency heatmap in the compression studio** — the preview studio can now color each token by saliency: `ultra` per-token `scoreToken` (0–1, green→red gradient) or universal kept/removed from the existing diff. A dry-run visualization behind a toggle (no cost on a normal preview; backward-compatible when off). Completes the visualization half of roadmap item #13 (the A/B comparison shipped in [#5080](https://github.com/diegosouzapw/OmniRoute/pull/5080)). ([#5285](https://github.com/diegosouzapw/OmniRoute/pull/5285))
|
||||
- **feat(compression): composite-command splitter for RTK detection** — `cd /x && git status` now detects as `git-status` (previously the whole string was treated as one command and matched no filter). A quote-aware top-level tokenizer splits on `&&`/`||`/`;` (never inside quotes or `$(…)`/backtick subshells) and feeds the **last** segment to RTK command detection, so every RTK filter/renderer fires on commands wrapped in `cd … &&`/`||`/`;` chains. O(n), no RegExp over the command (ReDoS-safe). Tier-3 item of the compression feature-extraction roadmap (#16). ([#5283](https://github.com/diegosouzapw/OmniRoute/pull/5283))
|
||||
- **feat(mcp): `omniroute_tool_search` tool + one-line TS signatures** — new MCP tool that does lexical keyword search over every MCP tool's name/description and returns the top matches as compact one-line TypeScript signatures (~half the JSON-schema token cost), so agents discover tools on demand instead of carrying all ~88 schemas every turn. Search is ReDoS-safe (substring scoring, never `new RegExp` on the query) and deterministic; `tools/list` stays complete (no hidden tools). Adds the `read:tools` scope. Tier-1 item of the compression feature-extraction roadmap. ([#5269](https://github.com/diegosouzapw/OmniRoute/pull/5269))
|
||||
- **feat(compression): RTK semantic command-output renderers (opt-in)** — adds a second, opt-in compaction layer to the RTK engine that rewrites structured command output into a far more compact semantic form: `git diff` → file headers + `@@` hunks + changed lines only; an all-green `pytest`/`jest`/`vitest`/`eslint` run → its one-line summary; `terraform`/`tofu plan` → `Plan: +N ~M -K` plus the resource list; `kubectl`/`aws` JSON arrays → a minimal table. Each renderer is conservative (no-op when the shape doesn't match) and the integration is fail-open; the test-green renderer never collapses output that carries any failure signal. Gated by `RtkConfig.enableRenderers` (default off → zero behavioral change). Eighth item of the compression feature-extraction roadmap. ([#5268](https://github.com/diegosouzapw/OmniRoute/pull/5268))
|
||||
- **feat(compression): QuantumLock cache-prefix stabilization (opt-in, default off)** — recovers upstream prompt-cache hits that a volatile fragment in the system prompt would otherwise bust. When a caller injects a session UUID, unix timestamp, request-id, JWT, API-key shape, or long hex digest into the `role:system` message every turn, the longest common prefix across turns ends at that changing byte → the whole system prompt after it is re-billed and re-processed each turn. QuantumLock replaces each non-semantic volatile fragment with a **positional, value-independent** placeholder `⟦Q{i}⟧` and appends the real values in a delimited `⟦QUANTUMLOCK⟧` tail. The rewrite is **sent to the model** (lossless — not restored), so the system-prompt body becomes **byte-identical across turns** and the provider caches the long stable prefix while only the small tail differs. Opt-in, default off, applied only for caching providers (`isCachingProvider && config.quantumLock.enabled`); bounded ReDoS-safe patterns; idempotent; **no date/time patterns** (semantically meaningful — explicit non-goal). Studio gets a toggle + a "🔒 N volatile fragment(s) stabilized" dry-run badge. Seventh item of the compression feature-extraction roadmap (bench: [#5080](https://github.com/diegosouzapw/OmniRoute/pull/5080), gate: [#5127](https://github.com/diegosouzapw/OmniRoute/pull/5127), fuzzy: [#5143](https://github.com/diegosouzapw/OmniRoute/pull/5143), ionizer: [#5148](https://github.com/diegosouzapw/OmniRoute/pull/5148), TOON: [#5163](https://github.com/diegosouzapw/OmniRoute/pull/5163), CCR ranged: [#5187](https://github.com/diegosouzapw/OmniRoute/pull/5187), risk-gate: [#5243](https://github.com/diegosouzapw/OmniRoute/pull/5243)). ([#5260](https://github.com/diegosouzapw/OmniRoute/pull/5260))
|
||||
- **kilocode:** anonymous (no-auth) access to Kilo Code's free models, mirroring the `opencode`/`mimocode` pattern. With no Kilo account connected, requests now fall back to the gateway's anonymous tier (`Authorization: Bearer anonymous` on `api.kilo.ai/api/openrouter`) so the free models work without signup; a connected OAuth account is still used unchanged for the paid tier ([#5259](https://github.com/diegosouzapw/OmniRoute/pull/5259), #4019 — thanks @Theadd for the reference implementation)
|
||||
- **feat(logging): call-log correlation ID (end-to-end)** — every request now gets a unique correlation id, returned in the `X-Correlation-Id` response header, persisted in `call_logs` (migration **109**), filterable via `/api/usage/call-logs`, and surfaced in the dashboard request logger (per-chunk stream timestamps + active-requests-first sort). This is the safe, cohesive core subset of the larger #5275 — landed on its own so the low-risk value isn't blocked by the parts of that PR still under review. ([#5279](https://github.com/diegosouzapw/OmniRoute/pull/5279) — thanks @hartmark)
|
||||
- **feat(providers): Microsoft 365 Copilot individual provider** — adds the `copilot-m365-web` provider (the 237th), wiring the M365 BizChat framing/connection helpers into a selectable web-session provider backed by `m365.cloud.microsoft/chat` for individual Microsoft 365 plans. Builds on the M365 pure-framing groundwork from #4696. Regression guard: `tests/unit/copilot-m365-web-executor.test.ts`. ([#5302](https://github.com/diegosouzapw/OmniRoute/pull/5302) — thanks @skyzea1)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **ci(docker):** re-point the Docker Hub / GHCR `:latest` (and `:latest-web`) tags to the just-published release. On a `release: released` event the freshly-created git tag is often not yet visible to `git fetch --tags` when `docker-publish` runs, so the `:latest`-promotion gate built its candidate set purely from `git tag -l` and resolved the highest semver to the **previous** version — leaving `latest` one release behind (3.8.39 published, `latest` still 3.8.38). The decision now lives in `scripts/ci/should-promote-latest.sh`, which folds the current `VERSION` into the candidate set before picking the highest stable semver, making promotion independent of tag-sync timing (a patch published after a higher minor still won't grab `latest`). Regression guard: `tests/unit/build/should-promote-latest-5301.test.ts` ([#5301](https://github.com/diegosouzapw/OmniRoute/issues/5301))
|
||||
- **command-code:** treat a non-positive `max_tokens`/`max_completion_tokens` (e.g. Zoo Code's `-1` "let the server choose") as "no limit" — omit the field instead of forcing it to `1`. `clampMaxTokens` previously did `Math.max(1, …)`, so a client `-1` was sent upstream as `max_tokens: 1`, truncating the response to a single token (the observed `completion_tokens: 1`, `content: null`, `reasoning_content: "The"` with `finish_reason: stop`). Now any value `≤ 0` is dropped so Command Code applies the model's native default; positive values are still floored and clamped to the 200k ceiling. Regression guard: `tests/unit/command-code-maxtokens-negative-5166.test.ts` ([#5166](https://github.com/diegosouzapw/OmniRoute/issues/5166) — thanks @Stazyu)
|
||||
- **fix(auth): compare-and-swap guard on the OAuth refresh persist** — under multi-agent load, the per-connection refresh mutex makes `[network refresh + DB write]` atomic for **one** connection, but it does not protect against a **third** writer (a sibling request, a concurrent HealthCheck, or a replica) landing a fresher `refresh_token` rotation on the same `connection_id` between the staleness read and the persist. Overwriting that fresher row reverts the sibling's rotation; the next caller then loads the now-consumed token, Auth0/Anthropic flag it as `refresh_token_reused`, and the whole token family gets revoked (the 1352× claude/`aa5dd5cf` invalidation storm). `getAccessToken` now re-reads the row's current `refresh_token` immediately before persisting (inside the mutex) and **skips the write** when it has rotated past the token the caller presented — the caller still receives the freshly-issued access token, only the DB overwrite is skipped. Opt-in via `runWithCasGuard` (no active guard ⇒ byte-identical behavior); skip/persist counters exposed via `getCasGuardStats()`. Regression guard: `tests/unit/token-refresh-cas-guard-4038.test.ts`. ([#4038](https://github.com/diegosouzapw/OmniRoute/issues/4038) — thanks @KooshaPari for the root-cause diagnosis)
|
||||
- **mcp:** break the `schemas/tools.ts ↔ schemas/toolSearch.ts` import cycle introduced when the `tool_search` defs (#5269) were extracted into their own module — `toolSearch.ts` imported `McpToolDefinition` from `tools.ts` while `tools.ts` imported `toolSearchTool` from `toolSearch.ts`, failing `check:cycles` on `release/v3.8.40`. The shared `AuditLevel` + `McpToolDefinition` types now live in a leaf `schemas/toolDefinition.ts` that both import; `tools.ts` re-exports them for backward compatibility.
|
||||
- **compression (analytics):** record attempted-but-no-op compression runs so Stacked is no longer invisible when it saves nothing. Previously a `compression_analytics` row was written only on a net-positive saving, so a Stacked (RTK→Caveman) pipeline that ran on already-compact context produced no row — indistinguishable from "never dispatched" (`byMode.stacked.count` stayed flat while Ultra climbed). Such runs are now recorded with `skip_reason` and surfaced as a per-mode `skipped` count plus `totalSkipped`/`bySkipReason` in the analytics summary and the Mode Breakdown; the existing net-saving totals/averages are unchanged (skip rows are excluded from them) (#4268 — thanks @abdulkadirozyurt, @androw)
|
||||
- **cli (tray):** fix `omniroute server --tray` showing no tray on macOS/Linux with no error printed. The wired Unix tray path loaded `systray2` through an inline loader that called `require("module")` inside an ESM `.mjs` file (`"type":"module"`) → `ReferenceError: require is not defined`, silently swallowed (regressed in v3.8.34); even if it had loaded, `systray2` isn't in `node_modules` (it's lazily installed into `~/.omniroute/runtime`). The loader now delegates to the runtime loader, the icon path (`icon.png`) is corrected, `isTemplateIcon` is `false` (the full-color icon rendered as a white square under macOS template mode), and tray start failures are surfaced to stderr instead of being swallowed (#4605 — thanks @ProgMEM-CC)
|
||||
- **agent-bridge (antigravity):** unwrap the cloudcode-pa `.request` envelope when converting Antigravity IDE requests. The real IDE sends `cloudcode-pa.googleapis.com/v1internal:generateContent` with the Gemini request nested under `.request` (`{ project, model, request: { contents, systemInstruction, generationConfig } }`), but the bridge read those fields at the top level — yielding an empty conversation, so prompts hung mid-execution. The legacy `/v1beta/models/<model>:generateContent` top-level shape still works (#4294 — thanks @shabeer)
|
||||
- **dashboard:** add a GitHub releases fallback to the "Update Available" lookup. After the v3.8.28 fix added an npm-registry HTTP fallback, the banner could still stay hidden on networks that reach GitHub (where the news feed already loads) but not `registry.npmjs.org`. `resolveLatestVersion()` now tries npm CLI → npm registry → GitHub releases (`/repos/diegosouzapw/OmniRoute/releases/latest`) before giving up, and logs a warning only when all three fail (#4100)
|
||||
- **command-code:** omit `max_tokens` when the client omits it so the upstream applies the model's native default, fixing `400 "expected <=200000"` on `/alpha/generate` for high-cap models; an explicit oversized client value is clamped to the 200k endpoint ceiling (#5221 — thanks @adivekar-utexas)
|
||||
- **combo:** wire session stickiness into the round-robin dispatch path. Multi-turn conversations from clients that send no session id (Codex CLI, Claude Code, most OpenAI-compatible tools) were rotated to a different connection on every turn by round-robin combos, busting the upstream prompt-cache → cold high-reasoning starts, intermittent `504`s and throughput collapse under concurrency. The weighted/priority paths already honored per-conversation stickiness; the round-robin handler returned before reaching it. Round-robin now starts the rotation at the conversation's sticky connection (failover to the other targets is preserved), and different conversations still spread across connections — only intra-conversation rotation is removed ([#5248](https://github.com/diegosouzapw/OmniRoute/pull/5248), #3825 — thanks @bypanghu, @jpsn123, @xz-dev)
|
||||
- **kiro:** replace the synthesized trailing `"Continue"` turn with a neutral filler (`"..."`) — when an OpenAI→Kiro request ends on an assistant/tool turn, the translator synthesizes the protocol-required trailing user turn, and the literal word `"Continue"` could be read by Kiro/CodeWhisperer as a real user instruction and trigger unintended agent action. A trailing tool-result turn is still promoted as-is (it already collapses to a real user turn); only the assistant-text-ending case is affected. Regression guards: `tests/unit/kiro-continue-filler-5231.test.ts`. ([#5231](https://github.com/diegosouzapw/OmniRoute/issues/5231))
|
||||
- **combo:** advance to the next combo target on a `400 "requested model is not supported"` instead of hard-failing. The 400 guard in the priority strategy treated `MODEL_CAPACITY` as a block-fallback reason, so a combo that hit a provider lacking a specific model returned a hard `400` even when other targets (different providers) supported it. Such 400s now fall through to the next target. ([#5249](https://github.com/diegosouzapw/OmniRoute/pull/5249) — thanks @Chewji9875)
|
||||
- **dashboard:** disabled no-auth providers no longer vanish from the All Providers page. Disabling a no-auth provider (the "No authentication required" toggle, which adds it to `blockedProviders`) silently removed its card because the page _dropped_ blocked no-auth entries from its render list — the only way back was buried under Settings → Security → Blocked Providers. The page now **partitions** no-auth entries: visible providers render as before, blocked ones appear in a "Disabled" sub-group with an **Enable** button that un-blocks them in place. Aggregates, counts and `/v1/models` still consume the visible-only list (blocked providers stay out of routing). Regression guard: `tests/unit/noauth-blocked-partition-5183.test.ts`. ([#5183](https://github.com/diegosouzapw/OmniRoute/issues/5183), follow-up from [#5166](https://github.com/diegosouzapw/OmniRoute/issues/5166) — thanks @WslzGmzs)
|
||||
- **dashboard:** add a parent `/dashboard/context` page so RSC prefetches of the compression-context hub no longer 404. The route only had sub-routes (`settings`, `combos`, `ultra`, …) and no parent page, so the App Router returned 404 for the bare segment. The parent now redirects to its canonical sub-route (`/dashboard/context/settings`), honoring a legacy `?tab=` query for deep links. Regression guard: `tests/unit/dashboard/context-parent-redirect-5298.test.ts` ([#5298](https://github.com/diegosouzapw/OmniRoute/issues/5298) — thanks @KooshaPari)
|
||||
- **i18n:** add the missing `sidebar.gamificationGroup` message across all 42 locales — the Gamification sidebar group referenced a `titleKey` that existed in no locale, logging `MISSING_MESSAGE: sidebar.gamificationGroup (en)` at runtime (the group still rendered via its `titleFallback`). The key is now present everywhere so the warning is gone and locale coverage is unaffected ([#5298](https://github.com/diegosouzapw/OmniRoute/issues/5298) — thanks @KooshaPari)
|
||||
- **api(stream):** `/v1/chat/completions` no longer returns SSE for a non-stream OpenAI-compatible request when `stream` is omitted and the client sends `Accept: application/json, text/event-stream` — the Vercel AI SDK / OpenAI SDK non-stream signature (`doGenerate()`/`generateText()`), which then failed with `Invalid JSON response` (`Unexpected token 'd', "data: {"id"...`). The route-level Accept override (#302) and `resolveStreamFlag` now treat an Accept header that explicitly lists `application/json` as a JSON opt-in even when it also lists `text/event-stream`; only a pure `Accept: text/event-stream` (no `application/json`) still opts an omitted-`stream` request into SSE, and an explicit body `stream` value always wins. The shared decision now lives in `acceptHeaderForcesStream`. Regression guard: `tests/unit/sse-nonstream-accept-5305.test.ts`. ([#5305](https://github.com/diegosouzapw/OmniRoute/issues/5305) — thanks @md-riaz)
|
||||
- **providers:** drop the retired GPT‑5.2 / GPT‑4.5 models from the direct **ChatGPT‑web** and **Codex** surfaces (OpenAI removed them there), so OmniRoute stops advertising/routing models that no longer exist. Scoped on purpose to those two providers — third‑party proxies that still expose the ids are untouched. ([#5280](https://github.com/diegosouzapw/OmniRoute/pull/5280) — thanks @backryun)
|
||||
- **codex:** drop the deprecated `local_shell` hosted tool type before forwarding to OpenAI's Responses API, resolving the omni-combo `400 "The local_shell tool is no longer supported."` spike. Inbound Responses `local_shell` is still accepted and mapped to a caller-side Chat `shell` function for compatibility. ([#5250](https://github.com/diegosouzapw/OmniRoute/pull/5250), [#5256](https://github.com/diegosouzapw/OmniRoute/pull/5256) — thanks @KooshaPari)
|
||||
- **antigravity:** retry excluded accounts via the fallback LRU. The combo same-model retry loop accumulates excluded Antigravity connection ids after account-level failures, but auth selection only treated a single `excludeConnectionId` as a fallback scenario — once exclusions accumulated through `excludedConnectionIds`, selection could fall back to normal sticky/priority behavior instead of LRU-selecting the next eligible account for the same model/family. Any non-empty accumulated exclude set is now treated as fallback mode. Builds on the family-scoped lockout work in #5180 (v3.8.39). ([#5222](https://github.com/diegosouzapw/OmniRoute/pull/5222) — thanks @Ardem2025)
|
||||
- **grok-cli:** strip unsupported sampling params (`presencePenalty`, `frequencyPenalty`, `logprobs`, `topLogprobs`) before sending to the Grok Build API, fixing `400 'Model does not support parameter presencePenalty'` when clients (MiMoCode, Cursor, etc.) send OpenAI-style params. ([#5273](https://github.com/diegosouzapw/OmniRoute/pull/5273) — thanks @fulorgnas)
|
||||
- **grok-cli:** accept the full `~/.grok/auth.json` object in the dashboard import-token endpoint. The `oauthImportTokenSchema` only accepted a bare string token while the UI sends the whole auth.json object → `400 Bad Request`; the schema now accepts the object and stores the original under `providerSpecificData.rawAuthJson` for diagnostics and token refresh. ([#5258](https://github.com/diegosouzapw/OmniRoute/pull/5258) — thanks @fulorgnas)
|
||||
- **qoder:** coalesce concurrent PAT→job-token exchanges per PAT so high-concurrency / multi-agent bursts no longer stampede `openapi.qoder.sh/api/v1/jobToken/exchange` before the first exchange populates the completed-token cache; the shared exchange is also decoupled from any single caller's `AbortSignal` so one aborted waiter can't cancel it for the others. ([#5254](https://github.com/diegosouzapw/OmniRoute/pull/5254), [#5265](https://github.com/diegosouzapw/OmniRoute/pull/5265) — thanks @KooshaPari)
|
||||
- **proxy:** scope the fallback reachability cache by normalized target **URL** instead of by hostname, so a failed probe for one endpoint on a shared API host no longer suppresses a later probe for a different endpoint on that same host for the full TTL (host fallback is preserved for malformed URLs). ([#5261](https://github.com/diegosouzapw/OmniRoute/pull/5261) — thanks @KooshaPari)
|
||||
- **proxy:** cache failed fast-fail health probes with a short negative TTL instead of the full positive health TTL, so a single transient timeout/load blip no longer marks a working residential SOCKS5 proxy unreachable for the whole window (#5109 regression coverage added). ([#5255](https://github.com/diegosouzapw/OmniRoute/pull/5255) — thanks @KooshaPari)
|
||||
- **mcp:** forward HTTP auth to internal tool fetches so MCP tools that call back into the local API surface carry the caller's authorization. ([#5218](https://github.com/diegosouzapw/OmniRoute/pull/5218) — thanks @KooshaPari)
|
||||
- **logging:** preserve the **outbound provider request** headers in the detailed call-log Provider Request payload (previously the upstream **response** headers were shown there). chatCore now keeps executor-returned request headers when wrapping streaming and non-streaming responses; response headers stay scoped to the `Response`. ([#5257](https://github.com/diegosouzapw/OmniRoute/pull/5257) — thanks @rdself)
|
||||
- **sse:** scope textual `<think>`/`<thinking>` tag extraction so generic OpenAI-compatible paths don't rewrite prompt-format content into `reasoning_content`; an explicit opt-in keeps tag-native families (DeepSeek-R1, QwQ) working while Antigravity/Agy stay excluded by provider/model prefix. ([#5224](https://github.com/diegosouzapw/OmniRoute/pull/5224) — thanks @rdself)
|
||||
- **mcp:** break the `schemas/tools.ts ↔ schemas/toolSearch.ts` import cycle introduced when the `tool_search` defs (#5269) were extracted into their own module — `toolSearch.ts` imported `McpToolDefinition` from `tools.ts` while `tools.ts` imported `toolSearchTool` from `toolSearch.ts`, failing `check:cycles` on `release/v3.8.40`. The shared `AuditLevel` + `McpToolDefinition` types now live in a leaf `schemas/toolDefinition.ts` that both import; `tools.ts` re-exports them for backward compatibility. ([#5282](https://github.com/diegosouzapw/OmniRoute/pull/5282))
|
||||
- **compression (analytics):** record attempted-but-no-op compression runs so Stacked is no longer invisible when it saves nothing. A `compression_analytics` row was previously written only on a net-positive saving, so a Stacked pipeline that ran on already-compact context produced no row — indistinguishable from "never dispatched". Such runs are now recorded with `skip_reason` and surfaced as a per-mode `skipped` count plus `totalSkipped`/`bySkipReason`; net-saving totals/averages are unchanged (skip rows excluded). ([#5277](https://github.com/diegosouzapw/OmniRoute/pull/5277), #4268 — thanks @abdulkadirozyurt, @androw)
|
||||
- **cli (tray):** fix `omniroute server --tray` showing no tray on macOS/Linux with no error printed. The Unix tray path loaded `systray2` through an inline loader that called `require("module")` inside an ESM `.mjs` file → `ReferenceError: require is not defined`, silently swallowed (regressed in v3.8.34); even if loaded, `systray2` is lazily installed into `~/.omniroute/runtime`, not `node_modules`. The loader now delegates to the runtime loader, the icon path is corrected, `isTemplateIcon` is `false` (the full-color icon rendered as a white square under macOS template mode), and tray start failures surface to stderr. ([#5276](https://github.com/diegosouzapw/OmniRoute/pull/5276), #4605 — thanks @ProgMEM-CC)
|
||||
- **agent-bridge (antigravity):** unwrap the cloudcode-pa `.request` envelope when converting Antigravity IDE requests. The real IDE sends `cloudcode-pa.googleapis.com/v1internal:generateContent` with the Gemini request nested under `.request`, but the bridge read those fields at the top level — yielding an empty conversation, so prompts hung mid-execution. The legacy `/v1beta/models/<model>:generateContent` top-level shape still works. ([#5267](https://github.com/diegosouzapw/OmniRoute/pull/5267), #4294 — thanks @shabeer)
|
||||
- **dashboard:** add a GitHub releases fallback to the "Update Available" lookup. After the v3.8.28 npm-registry fallback, the banner could still stay hidden on networks that reach GitHub but not `registry.npmjs.org`. `resolveLatestVersion()` now tries npm CLI → npm registry → GitHub releases before giving up. ([#5266](https://github.com/diegosouzapw/OmniRoute/pull/5266), #4100)
|
||||
- **command-code:** omit `max_tokens` when the client omits it so the upstream applies the model's native default, fixing `400 "expected <=200000"` on `/alpha/generate` for high-cap models; an explicit oversized client value is clamped to the 200k endpoint ceiling. ([#5221](https://github.com/diegosouzapw/OmniRoute/pull/5221) — thanks @adivekar-utexas)
|
||||
|
||||
### 🔒 Security
|
||||
|
||||
- **authz:** require auth for the `/v1beta/*` Gemini-compatible client API. `next.config.mjs` rewrote `/v1beta/:path*` → `/api/v1beta/:path*`, but `src/proxy.ts` didn't match `/v1beta` before the rewrite and `classifyRoute()` didn't classify `/api/v1beta/*` as client API — so unauthenticated `/v1beta/models/...:generateContent` traffic could reach the model-serving route without the central client-API auth policy. Both alias and rewritten forms are now classified `CLIENT_API`, enforcing Bearer auth when `REQUIRE_API_KEY` is enabled. ([#5274](https://github.com/diegosouzapw/OmniRoute/pull/5274) — thanks @rdself)
|
||||
- **sentinel:** security hardening pass across request handling. ([#5241](https://github.com/diegosouzapw/OmniRoute/pull/5241) — thanks @iamedwardngo)
|
||||
- **providers:** refresh impersonation User-Agents + TLS fingerprint profiles to current real-client versions; several had drifted or were inconsistent across files, a bot-detection/blocking risk. ([#5237](https://github.com/diegosouzapw/OmniRoute/pull/5237) — thanks @backryun)
|
||||
- **authz (public origin):** centralize browser-mutation origin validation into `src/server/origin/publicOrigin.ts` and wire it through the authz pipeline, replacing the per-route same-origin-only check that `403`'d dashboard mutations when served behind a reverse proxy on a different public origin. The module resolves the allowed public origin from configured base-URL env vars or trusted forwarded headers (only when `OMNIROUTE_TRUST_PROXY` is set **and** the peer is loopback/LAN via peer-stamp), validates `Sec-Fetch-Site` metadata, and sanitizes `Host`/`Forwarded` inputs (rejects control chars, userinfo, path/query in Host). Regression guards: `tests/unit/authz/public-origin.test.ts` + `tests/unit/authz/pipeline.test.ts`. ([#5278](https://github.com/diegosouzapw/OmniRoute/pull/5278) — thanks @Thinkscape / @abodera)
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **docs:** reorganize `docs/`, run an accuracy audit, and drop Node 20 from the supported matrix (rebased onto the current release tip). ([#5262](https://github.com/diegosouzapw/OmniRoute/pull/5262))
|
||||
- **providers:** remove the discontinued Gemini CLI channel — Google shut it down on 2026-06-18; the supported migration path is Antigravity. ([#5246](https://github.com/diegosouzapw/OmniRoute/pull/5246) — thanks @rdself)
|
||||
- **dashboard:** add more provider icons from Lobehub. ([#5220](https://github.com/diegosouzapw/OmniRoute/pull/5220) — thanks @backryun)
|
||||
- **deps:** resolve `npm install` warnings, fix a runtime `Cannot find module` crash on `omniroute serve` (the runtime-env module was missing from the npm `files` allow-list), and clear the 4 moderate `npm audit` advisories. ([#5252](https://github.com/diegosouzapw/OmniRoute/pull/5252) — thanks @yunaamelia), ([#5230](https://github.com/diegosouzapw/OmniRoute/pull/5230), #5227)
|
||||
- **docker:** harden the base image against container-scan CVEs and skip comment lines in the #4076 builder heap-ordering check. ([#5229](https://github.com/diegosouzapw/OmniRoute/pull/5229), [#5233](https://github.com/diegosouzapw/OmniRoute/pull/5233))
|
||||
- **ci:** Trivy advisory scan now ignores unfixed CVEs to cut Security-tab noise. ([#5235](https://github.com/diegosouzapw/OmniRoute/pull/5235))
|
||||
- **test(combo):** reconcile the #4279 stop-guard test with the #5249 advance policy. ([#5300](https://github.com/diegosouzapw/OmniRoute/pull/5300))
|
||||
- **test(targets):** add 14 unit tests for the shared `combo/targetExhaustion.ts` handler (provider-exhausted / connection-error / transient rate-limited classification), registered in the vitest discovery list. ([#5296](https://github.com/diegosouzapw/OmniRoute/pull/5296) — thanks @KooshaPari)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.39] — 2026-06-28
|
||||
|
||||
### ✨ New Features
|
||||
@@ -15,6 +90,7 @@
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(cli): global npm install no longer fails with "Cannot find module scripts/build/runtime-env.mjs"** — the v3.8.39 heap auto-calibration fix made `bin/cli/commands/serve.mjs` import `scripts/build/runtime-env.mjs`, but that file was never added to the `files` whitelist in `package.json`, so the published npm tarball shipped the importing CLI without the imported module — breaking **every** `npm install -g omniroute` at startup (regression, all platforms). The file is now whitelisted (`npm pack` confirms it ships) and added to the pack-artifact policy allow/required lists, and a new regression test scans all `bin/**` entrypoints for runtime imports resolving under `scripts/` and asserts each is covered by the package `files` whitelist, so any future unpackaged CLI import fails CI instead of users' installs. ([#5227](https://github.com/diegosouzapw/OmniRoute/issues/5227) — thanks @PriyomSaha, @m-Yaghoubi, @jonlwheat2-gif)
|
||||
- **fix(oauth): Antigravity refresh no longer nulls the stored refresh_token on an empty upstream response** — Google's OAuth token endpoint uses non-rotating refresh tokens: a refresh response normally OMITS `refresh_token` and occasionally returns it as an empty string. The Antigravity executor's `refreshCredentials` used `typeof tokens.refresh_token === "string" ? tokens.refresh_token : credentials.refreshToken`, and because `typeof "" === "string"` is true, an empty-string response overwrote the good token with `""` — nulling it on first refresh. The check now treats a non-string **or empty** value as absent and preserves the stored token, matching the canonical `refreshGoogleToken` (`tokens.refresh_token || refreshToken`) semantics. ([#3850](https://github.com/diegosouzapw/OmniRoute/issues/3850) — thanks @3xa228148)
|
||||
- **fix(api): LAN/Tailscale dashboard access — `ws:` CSP scheme, GET-exempt version route, surface combo field errors** — three failures when opening the dashboard from a non-loopback host: (1) CSP `connect-src` allowed the `ws:` scheme only for loopback origins, blocking the dashboard's `ws://<lan-host>:*` Live WebSocket from LAN/Tailscale clients; the bare `ws:` scheme is now permitted (symmetric with the bare `wss:` already allowed), kept declarative in `next.config.mjs` with no global middleware (the project has none by design); (2) `GET /api/system/version` was blocked by `LOCAL_ONLY_API_PREFIXES` for all methods despite only `POST` spawning child processes (git/npm/pm2) — a new `LOCAL_ONLY_API_GET_EXEMPTIONS` set exempts safe read methods for this path while keeping `POST`/`PUT`/`PATCH`/`DELETE` strictly loopback-only; (3) `COMBO_002` validation errors only surfaced the generic message — `firstField`/`firstMessage` are now extracted from the first Zod issue and included in the response body. ([#5083](https://github.com/diegosouzapw/OmniRoute/issues/5083) — thanks @KooshaPari for the diagnosis and original PR #5084)
|
||||
- **fix(sse): defer `</think>` close so it never leaks before `tool_calls` in Claude→OpenAI streaming** — when a Claude thinking block was followed by a tool_use block, the translator unconditionally emitted a `content: "</think>"` chunk at `content_block_stop`, injecting a spurious assistant text chunk immediately before the `tool_calls` delta and corrupting OpenAI-compatible clients (e.g. Kimi Coding). The close marker is now deferred: it is flushed at the first `text_delta` that follows the thinking block (preserving the #4633 / decolua/9router#454 behavior for Claude Code / Cursor) or at stream finish when no tool_calls were collected. Tool-use streams never get a `text_delta` after the thinking block, so `</think>` is never emitted into content before `tool_calls`. ([#5123](https://github.com/diegosouzapw/OmniRoute/issues/5123))
|
||||
@@ -45,6 +121,7 @@
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **test(docker): de-brittle the #4076 builder-stage heap-ordering test (false-failing on a comment)** — `dockerfile-build-heap-4076.test.ts` located the `npm run build` step via a raw `findIndex(/npm run build\b/)`, which matched a **comment line** in the builder stage (`# … npm run build fails with "Module not…"`, added with the v3.8.40 workspace-deps Docker fix) sitting before the `NODE_OPTIONS` heap line — making the test report the heap ceiling as set _after_ the build and fail, even though the real `RUN … npm run build` correctly follows the `ENV NODE_OPTIONS` line. Instruction matching now skips Dockerfile comment lines (`# …`), so the ordering guard checks real `ENV`/`RUN` instructions only. The Dockerfile itself was already correct; this was a base-red blocking every PR into `release/v3.8.40`. ([#4076](https://github.com/diegosouzapw/OmniRoute/issues/4076))
|
||||
- **test(combo): deterministic context-relay universal-handoff coverage** — covers the universal (provider-agnostic) session-handoff path in `context-relay` (`combo.ts:2099–2139`), which previously had only a definition-order assertion and a `TODO(phase-2)`. The test drives the real pipeline via session seams (`x-session-id` → `relayOptions.sessionId` → `maybeGenerateUniversalHandoff`) without live infrastructure. ([#5168](https://github.com/diegosouzapw/OmniRoute/pull/5168))
|
||||
- **test(combo): end-to-end quota-share DRR routing-decision coverage (matrix parity)** — adds the missing E2E test for the `quota-share` strategy, driving the real `handleChat` → chatCore → `selectQuotaShareTarget` → executor pipeline via in-process seams and asserting which connection is dispatched. The DRR selector already had 29 unit tests; this closes the E2E gap and brings quota-share to parity with the 17-strategy public matrix. ([#5179](https://github.com/diegosouzapw/OmniRoute/pull/5179))
|
||||
- **test(combo): deterministic context-relay codex quota-handoff coverage (closes last gap)** — covers the codex-specific handoff block of `context-relay` (`combo.ts:2143–2183`), which #5168 left documented-but-untested because it requires a `codex` connection. All seams (`fetchCodexQuota`, handoff generation, session relay) are mocked deterministically without live infra. ([#5195](https://github.com/diegosouzapw/OmniRoute/pull/5195))
|
||||
|
||||
109
CLAUDE.md
109
CLAUDE.md
@@ -35,22 +35,22 @@ For full test matrix, see `CONTRIBUTING.md` → "Running Tests". For deep archit
|
||||
|
||||
## Project at a Glance
|
||||
|
||||
**OmniRoute** — unified AI proxy/router. One endpoint, 231 LLM providers, auto-fallback.
|
||||
**OmniRoute** — unified AI proxy/router. One endpoint, 237 LLM providers, auto-fallback.
|
||||
|
||||
| Layer | Location | Purpose |
|
||||
| ------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| API Routes | `src/app/api/v1/` | Next.js App Router — entry points |
|
||||
| Handlers | `open-sse/handlers/` | Request processing (chat, embeddings, etc) |
|
||||
| Executors | `open-sse/executors/` | Provider-specific HTTP dispatch |
|
||||
| Translators | `open-sse/translator/` | Format conversion (OpenAI↔Claude↔Gemini) |
|
||||
| Transformer | `open-sse/transformer/` | Responses API ↔ Chat Completions |
|
||||
| Services | `open-sse/services/` | Combo routing, rate limits, caching, etc |
|
||||
| Database | `src/lib/db/` | SQLite domain modules (83 files, 97 migrations) |
|
||||
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
|
||||
| MCP Server | `open-sse/mcp-server/` | 87 tools (33 base + memory/skill/notion/obsidian/gamification/plugin modules), 3 transports (stdio / SSE / Streamable HTTP), 30 scopes |
|
||||
| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
|
||||
| Skills | `src/lib/skills/` | Extensible skill framework |
|
||||
| Memory | `src/lib/memory/` | Persistent conversational memory |
|
||||
| Layer | Location | Purpose |
|
||||
| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| API Routes | `src/app/api/v1/` | Next.js App Router — entry points |
|
||||
| Handlers | `open-sse/handlers/` | Request processing (chat, embeddings, etc) |
|
||||
| Executors | `open-sse/executors/` | Provider-specific HTTP dispatch |
|
||||
| Translators | `open-sse/translator/` | Format conversion (OpenAI↔Claude↔Gemini) |
|
||||
| Transformer | `open-sse/transformer/` | Responses API ↔ Chat Completions |
|
||||
| Services | `open-sse/services/` | Combo routing, rate limits, caching, etc |
|
||||
| Database | `src/lib/db/` | SQLite domain modules (94 files, 106 migrations) |
|
||||
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
|
||||
| MCP Server | `open-sse/mcp-server/` | 94 tools (34 base + memory/skill/agentSkill/pool/notion/obsidian/gamification/plugin modules), 3 transports (stdio / SSE / Streamable HTTP), 30 scopes |
|
||||
| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
|
||||
| Skills | `src/lib/skills/` | Extensible skill framework |
|
||||
| Memory | `src/lib/memory/` | Persistent conversational memory |
|
||||
|
||||
Monorepo: `src/` (Next.js 16 app), `open-sse/` (streaming engine workspace), `electron/` (desktop app), `tests/`, `bin/` (CLI entry point).
|
||||
|
||||
@@ -72,7 +72,7 @@ Client → /v1/chat/completions (Next.js route)
|
||||
|
||||
API routes follow a consistent pattern: `Route → CORS preflight → Zod body validation → Optional auth (extractApiKey/isValidApiKey) → API key policy enforcement → Handler delegation (open-sse)`. No global Next.js middleware — interception is route-specific.
|
||||
|
||||
**Combo routing** (`open-sse/services/combo.ts`): 17 strategies (priority, weighted, fill-first, round-robin, P2C, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion). Each target calls `handleSingleModel()` which wraps `handleChatCore()` with per-target error handling and circuit breaker checks. The `fusion` strategy is the exception: it fans out to a panel of models in parallel, then a judge model synthesizes one final answer (`open-sse/services/fusion.ts`). See `docs/routing/AUTO-COMBO.md` for the 9-factor Auto-Combo scoring + the full strategy table and `docs/architecture/RESILIENCE_GUIDE.md` for the 3 resilience layers.
|
||||
**Combo routing** (`open-sse/services/combo.ts`): 17 strategies (priority, weighted, fill-first, round-robin, P2C, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion). Each target calls `handleSingleModel()` which wraps `handleChatCore()` with per-target error handling and circuit breaker checks. The `fusion` strategy is the exception: it fans out to a panel of models in parallel, then a judge model synthesizes one final answer (`open-sse/services/fusion.ts`). See `docs/routing/AUTO-COMBO.md` for the 12-factor Auto-Combo scoring + the full strategy table and `docs/architecture/RESILIENCE_GUIDE.md` for the 3 resilience layers.
|
||||
|
||||
---
|
||||
|
||||
@@ -327,33 +327,33 @@ connection continue serving other models.
|
||||
|
||||
For any non-trivial change, read the matching deep-dive first:
|
||||
|
||||
| Area | Doc |
|
||||
| --------------------------------------------- | ----------------------------------------------------------------- |
|
||||
| Repo navigation | `docs/architecture/REPOSITORY_MAP.md` |
|
||||
| Architecture | `docs/architecture/ARCHITECTURE.md` |
|
||||
| Engineering reference | `docs/architecture/CODEBASE_DOCUMENTATION.md` |
|
||||
| Auto-Combo (9-factor scoring, 17 strategies) | `docs/routing/AUTO-COMBO.md` |
|
||||
| Resilience (3 mechanisms) | `docs/architecture/RESILIENCE_GUIDE.md` |
|
||||
| Reasoning replay | `docs/routing/REASONING_REPLAY.md` |
|
||||
| Skills framework | `docs/frameworks/SKILLS.md` |
|
||||
| Memory system (FTS5 + Qdrant) | `docs/frameworks/MEMORY.md` |
|
||||
| Cloud agents | `docs/frameworks/CLOUD_AGENT.md` |
|
||||
| Guardrails (PII / injection / vision) | `docs/security/GUARDRAILS.md` |
|
||||
| Public upstream credentials (Gemini/etc.) | `docs/security/PUBLIC_CREDS.md` |
|
||||
| Error message sanitization | `docs/security/ERROR_SANITIZATION.md` |
|
||||
| Evals | `docs/frameworks/EVALS.md` |
|
||||
| Compliance / audit | `docs/security/COMPLIANCE.md` |
|
||||
| Webhooks | `docs/frameworks/WEBHOOKS.md` |
|
||||
| Authorization pipeline | `docs/architecture/AUTHZ_GUIDE.md` |
|
||||
| Stealth (TLS / fingerprint) | `docs/security/STEALTH_GUIDE.md` |
|
||||
| Agent protocols (A2A / ACP / Cloud) | `docs/frameworks/AGENT_PROTOCOLS_GUIDE.md` |
|
||||
| MCP server | `docs/frameworks/MCP-SERVER.md` |
|
||||
| A2A server | `docs/frameworks/A2A-SERVER.md` |
|
||||
| Area | Doc |
|
||||
| --------------------------------------------- | ------------------------------------------------------- |
|
||||
| Repo navigation | `docs/architecture/REPOSITORY_MAP.md` |
|
||||
| Architecture | `docs/architecture/ARCHITECTURE.md` |
|
||||
| Engineering reference | `docs/architecture/CODEBASE_DOCUMENTATION.md` |
|
||||
| Auto-Combo (12-factor scoring, 17 strategies) | `docs/routing/AUTO-COMBO.md` |
|
||||
| Resilience (3 mechanisms) | `docs/architecture/RESILIENCE_GUIDE.md` |
|
||||
| Reasoning replay | `docs/routing/REASONING_REPLAY.md` |
|
||||
| Skills framework | `docs/frameworks/SKILLS.md` |
|
||||
| Memory system (FTS5 + Qdrant) | `docs/frameworks/MEMORY.md` |
|
||||
| Cloud agents | `docs/frameworks/CLOUD_AGENT.md` |
|
||||
| Guardrails (PII / injection / vision) | `docs/security/GUARDRAILS.md` |
|
||||
| Public upstream credentials (Gemini/etc.) | `docs/security/PUBLIC_CREDS.md` |
|
||||
| Error message sanitization | `docs/security/ERROR_SANITIZATION.md` |
|
||||
| Evals | `docs/frameworks/EVALS.md` |
|
||||
| Compliance / audit | `docs/security/COMPLIANCE.md` |
|
||||
| Webhooks | `docs/frameworks/WEBHOOKS.md` |
|
||||
| Authorization pipeline | `docs/architecture/AUTHZ_GUIDE.md` |
|
||||
| Stealth (TLS / fingerprint) | `docs/security/STEALTH_GUIDE.md` |
|
||||
| Agent protocols (A2A / ACP / Cloud) | `docs/frameworks/AGENT_PROTOCOLS_GUIDE.md` |
|
||||
| MCP server | `docs/frameworks/MCP-SERVER.md` |
|
||||
| A2A server | `docs/frameworks/A2A-SERVER.md` |
|
||||
| API reference + OpenAPI | `docs/reference/API_REFERENCE.md` + `docs/openapi.yaml` |
|
||||
| Provider catalog (auto-generated) | `docs/reference/PROVIDER_REFERENCE.md` |
|
||||
| Release flow | `docs/ops/RELEASE_CHECKLIST.md` |
|
||||
| Embedded services | `docs/frameworks/EMBEDDED-SERVICES.md` |
|
||||
| Quality gates (~48 scripts, allowlist policy) | `docs/architecture/QUALITY_GATES.md` |
|
||||
| Provider catalog (auto-generated) | `docs/reference/PROVIDER_REFERENCE.md` |
|
||||
| Release flow | `docs/ops/RELEASE_CHECKLIST.md` |
|
||||
| Embedded services | `docs/frameworks/EMBEDDED-SERVICES.md` |
|
||||
| Quality gates (~48 scripts, allowlist policy) | `docs/architecture/QUALITY_GATES.md` |
|
||||
|
||||
---
|
||||
|
||||
@@ -388,6 +388,31 @@ Why this matters: fixing bug A while opening bug B is worse than not fixing at a
|
||||
|
||||
---
|
||||
|
||||
## Planning & Research Artifacts (superpowers, deep-research)
|
||||
|
||||
`_tasks/` is a **separate, isolated git repository** that is gitignored by the main
|
||||
repo (`.gitignore` → `_tasks/`). It is the canonical home for working artifacts —
|
||||
plans, specs/designs, research, hand-offs — so they stay **versioned in their own
|
||||
repo** instead of polluting the main OmniRoute tree.
|
||||
|
||||
**Hard rule — never write superpowers / planning / research output under `docs/` or
|
||||
the repo root.** The superpowers skills ship with defaults that point at `docs/…`
|
||||
(`writing-plans` → `docs/superpowers/plans/`, `brainstorming` → `docs/superpowers/specs/`).
|
||||
Those defaults are **overridden here**. Whenever you invoke superpowers (or any
|
||||
plan/spec/research generator) in this project, save to `_tasks/` instead, using the
|
||||
same filename convention:
|
||||
|
||||
| Artifact (skill) | Default (do NOT use) | Save here instead |
|
||||
| ---------------------------------- | ------------------------- | ------------------------------------------------------------- |
|
||||
| Plans (`writing-plans`) | `docs/superpowers/plans/` | `_tasks/superpowers/plans/YYYY-MM-DD-<feature>.md` |
|
||||
| Specs / design (`brainstorming`) | `docs/superpowers/specs/` | `_tasks/superpowers/specs/YYYY-MM-DD-<topic>-design.md` |
|
||||
| Research (`deep-research`, ad-hoc) | `docs/research/` | `_tasks/research/…` |
|
||||
| Hand-offs (`/handoff`) | — | `_tasks/hands-off/<YYYY-MM-DD>_<branch>_v<versão>_sess-<id>/` |
|
||||
|
||||
When a superpowers skill announces a path like "saved to `docs/superpowers/plans/…`",
|
||||
rewrite it to the `_tasks/…` equivalent before writing. Commit those artifacts inside
|
||||
the `_tasks/` repo (`git -C _tasks …`), never in the main repo.
|
||||
|
||||
## Git Workflow
|
||||
|
||||
```bash
|
||||
|
||||
@@ -160,7 +160,7 @@ npm run test:protocols:e2e
|
||||
# Ecosystem compatibility tests
|
||||
npm run test:ecosystem
|
||||
|
||||
# Coverage gate: 75% statements/lines/functions, 70% branches
|
||||
# Coverage gate: 60% statements/lines/functions/branches
|
||||
npm run test:coverage
|
||||
npm run coverage:report
|
||||
|
||||
@@ -184,7 +184,7 @@ npm run test:combo:live:vps:failover # adds a real cross-provider failover s
|
||||
Coverage notes:
|
||||
|
||||
- `npm run test:coverage` measures source coverage for the main unit test suite, excludes `tests/**`, and includes `open-sse/**`
|
||||
- Pull requests must keep the coverage gate at **75%+** statements/lines/functions and **70%+** branches
|
||||
- Pull requests must keep the coverage gate at **60%+** statements/lines/functions/branches
|
||||
- If a PR changes production code in `src/`, `open-sse/`, `electron/`, or `bin/`, it must add or update automated tests in the same PR
|
||||
- `npm run coverage:report` prints the detailed file-by-file report from the latest coverage run
|
||||
- `npm run test:coverage:legacy` preserves the older metric for historical comparison
|
||||
@@ -196,7 +196,7 @@ Before opening or merging a PR:
|
||||
|
||||
- Run `npm run test:unit`
|
||||
- Run `npm run test:coverage`
|
||||
- Ensure the coverage gate stays at **75%+** statements/lines/functions, **70%+** branches
|
||||
- Ensure the coverage gate stays at **60%+** statements/lines/functions/branches
|
||||
- Include the changed or added test files in the PR description when production code changed
|
||||
- Check the SonarQube result on the PR when the project secrets are configured in CI
|
||||
|
||||
|
||||
@@ -71,8 +71,8 @@ ENV OMNIROUTE_USE_TURBOPACK=0
|
||||
# Raise the V8 heap ceiling for the build. The webpack production optimization
|
||||
# pass (forced above since Turbopack panics) needs more than V8's default ceiling
|
||||
# (~2 GB) for a codebase this size; a memory-constrained Docker build otherwise
|
||||
# dies with "FATAL ERROR: ... JavaScript heap out of memory" at `[builder] npm run
|
||||
# build` (#4076). NODE_OPTIONS propagates to the spawned `next build` child
|
||||
# dies with "FATAL ERROR: ... JavaScript heap out of memory" during the builder
|
||||
# stage (#4076). NODE_OPTIONS propagates to the spawned `next build` child
|
||||
# (build-next-isolated.mjs → resolveNextBuildEnv spreads process.env). Build-only;
|
||||
# the runtime heap is set separately on the runner stage (OMNIROUTE_MEMORY_MB).
|
||||
# Override for hosts with more/less RAM: `--build-arg OMNIROUTE_BUILD_MEMORY_MB=6144`.
|
||||
|
||||
36
README.md
36
README.md
@@ -6,7 +6,7 @@
|
||||
|
||||
# 🚀 OmniRoute — The Free AI Gateway
|
||||
|
||||
### Never stop coding. Connect every AI tool to **231 providers** — **50+ free** — through one endpoint.
|
||||
### Never stop coding. Connect every AI tool to **237 providers** — **50+ free** — through one endpoint.
|
||||
|
||||
**Plug Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini. Auto-fallback.**
|
||||
<br/>
|
||||
@@ -138,11 +138,11 @@
|
||||
|
||||
</div>
|
||||
|
||||
> One endpoint. **231 providers.** Never stop building — and let OmniRoute pick the cheapest one that works.
|
||||
> One endpoint. **237 providers.** Never stop building — and let OmniRoute pick the cheapest one that works.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="33%" valign="top"><b>🚫 Never hit limits</b><br/><sub>Auto-fallback across 231 providers in milliseconds. Quota out? Next provider takes over — zero downtime.</sub></td>
|
||||
<td width="33%" valign="top"><b>🚫 Never hit limits</b><br/><sub>Auto-fallback across 237 providers in milliseconds. Quota out? Next provider takes over — zero downtime.</sub></td>
|
||||
<td width="33%" valign="top"><b>💸 Save up to 95% tokens</b><br/><sub>RTK + Caveman stacked compression cuts 15–95% of eligible tokens (~89% avg on tool-heavy sessions).</sub></td>
|
||||
<td width="33%" valign="top"><b>🆓 $0 to start</b><br/><sub>50+ providers with a free tier, 11 free <i>forever</i> (Kiro, Qoder, Pollinations, LongCat…). No card needed.</sub></td>
|
||||
</tr>
|
||||
@@ -293,7 +293,7 @@ Result: 4 layers of fallback = zero downtime
|
||||
> Recent highlights from **v3.8.20 → v3.8.39**. Full history in [`CHANGELOG.md`](CHANGELOG.md).
|
||||
|
||||
- **⚖️ Quota-Share routing** — a dedicated combo strategy that spreads load across accounts by _available quota_: Deficit-Round-Robin scheduling, per-connection `max_concurrent` with cooldown-wait queueing, multi-window usage buckets (5h / 7d / per-model), per-(key,model) caps, session stickiness for prompt-cache integrity, and proactive saturation from upstream token-usage headers. → [Resilience Guide](docs/architecture/RESILIENCE_GUIDE.md)
|
||||
- **🤖 One-command CLI/agent setup** — a dedicated `setup-*` command configures each coding tool to route through OmniRoute (Claude Code, Codex, Cline, Continue, Cursor, Roo Code, Kilo Code, Crush, Goose, Qwen Code, Aider, OpenCode, Gemini CLI); `omniroute launch` / `omniroute launch-codex` are zero-config launchers. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
|
||||
- **🤖 One-command CLI/agent setup** — a dedicated `setup-*` command configures each coding tool to route through OmniRoute (Claude Code, Codex, Cline, Continue, Cursor, Roo Code, Kilo Code, Crush, Goose, Qwen Code, Aider, OpenCode); `omniroute launch` / `omniroute launch-codex` are zero-config launchers. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
|
||||
- **🛰️ Remote mode** — drive a remote OmniRoute from any machine with scoped access tokens (`omniroute connect` / `omniroute contexts` / `omniroute tokens`), plus an `omniroute login antigravity` helper that runs Google "native/desktop" OAuth on your own machine and pastes a credential blob into a remote/VPS install (where the loopback redirect is unreachable). → [Remote Mode](docs/guides/REMOTE-MODE.md)
|
||||
- **🧭 Smarter auto-routing** — OpenRouter-style `auto/<category>:<tier>` combos (e.g. `auto/coding:fast`, `auto/reasoning:pro`), a **Fusion** strategy (fan out to a panel of models in parallel, then synthesize via a judge), **task-aware routing** (best-fit connection per task type), per-request `X-Route-Model` override, live Arena-ELO + models.dev model intelligence, per-step account allowlists, provider-wildcard combo steps, nested combo-ref execution, sticky weighted selection, and `web_search`-aware routing. → [Auto-Combo](docs/routing/AUTO-COMBO.md)
|
||||
- **🗜️ Pluggable compression** — an async pipeline of **9 composable engines** with Compression Studios, an LLMLingua-2 ONNX engine and a heuristic/SLM two-tier **Ultra**, RTK, delegated Anthropic Context Editing, **Output Styles** (output-axis steering: terse-prose / less-code / terse-CJK), an **adaptive context-budget dial** (escalate only as far as needed to fit the context window), per-request `x-omniroute-compression` control, an opt-in offline eval harness, one-click **Headroom** proxy lifecycle management from the dashboard (Docker sidecar supported), a synthetic **compression playground** (Play lanes + A/B Compare with USD-capped fidelity verdicts), an opt-in **per-step fidelity gate** that rejects a lossy engine before it degrades the prompt, a **best-of-N candidate encoder** (GCF vs TOON — keep whichever is shorter, with an A/B bytes/token table in the studio), **CCR ranged/grep/stats retrieval** (pull an exact byte/line slice or summary of a stored block instead of re-expanding it), and a unified panel with named profiles + an active-profile selector. → [Compression](docs/compression/COMPRESSION_ENGINES.md)
|
||||
@@ -301,7 +301,7 @@ Result: 4 layers of fallback = zero downtime
|
||||
- **💸 Cost telemetry everywhere** — `X-OmniRoute-*` cost/usage headers on every endpoint (including media), a non-token cost engine, a cache-HIT `X-OmniRoute-Cost-Saved` header, and per-key USD spend quotas. → [API Reference](docs/reference/API_REFERENCE.md)
|
||||
- **🧠 Memory you control** — opt-in int8 vector quantization (Qdrant + sqlite-vec), memory off by default, and a per-request `x-omniroute-no-memory` header. → [Memory](docs/frameworks/MEMORY.md)
|
||||
- **🛡️ Security** — a prompt-injection guard across every LLM route (backed by a red-team suite), plus a free DuckDuckGo last-resort web search. → [Guardrails](docs/security/GUARDRAILS.md)
|
||||
- **🤝 More providers & agents** — Cursor Cloud Agent (a 4th cloud agent), CodeBuddy CN (`copilot.tencent.com`), a Google Flow video-generation provider, new gateways **DGrid** and **Pioneer AI** (Fastino Labs), inbound **xAI Grok** translators plus **Grok Build (xAI)** with an OAuth import-token flow, GPT-4 / GPT-4o-mini on the GitHub Copilot provider, multi-model **Factory Droid**, **ZenMux Free** (session-cookie free tier), **Alibaba DashScope** text-to-video (`wan2.7-t2v`), a refreshed 231-provider catalog (OrcaRouter, Wafer AI, OpenAdapter, dit.ai, TokenRouter, …), Vertex AI media generation (speech / transcription / music / video), and one-click account import from CLIProxyAPI (`~/.cli-proxy-api/`). → [Providers](docs/reference/PROVIDER_REFERENCE.md)
|
||||
- **🤝 More providers & agents** — Cursor Cloud Agent (a 4th cloud agent), CodeBuddy CN (`copilot.tencent.com`), a Google Flow video-generation provider, new gateways **DGrid** and **Pioneer AI** (Fastino Labs), inbound **xAI Grok** translators plus **Grok Build (xAI)** with an OAuth import-token flow, GPT-4 / GPT-4o-mini on the GitHub Copilot provider, multi-model **Factory Droid**, **ZenMux Free** (session-cookie free tier), **Alibaba DashScope** text-to-video (`wan2.7-t2v`), a refreshed 237-provider catalog (OrcaRouter, Wafer AI, OpenAdapter, dit.ai, TokenRouter, …), Vertex AI media generation (speech / transcription / music / video), and one-click account import from CLIProxyAPI (`~/.cli-proxy-api/`). → [Providers](docs/reference/PROVIDER_REFERENCE.md)
|
||||
- **⚡ Local performance & infra** — a one-click local Redis launcher (`omniroute redis up`, plus a dashboard Redis panel), one-click **Cloudflare Workers** and **Deno Deploy** relay deployers wired into the proxy pool, and an optional Bifrost Go sidecar that offloads the hottest relay path (`BIFROST_BASE_URL`, with automatic fallback to the TypeScript path on timeout). → [Environment](docs/reference/ENVIRONMENT.md)
|
||||
|
||||
<br/>
|
||||
@@ -317,7 +317,6 @@ Result: 4 layers of fallback = zero downtime
|
||||
<tr>
|
||||
<td align="center" width="120"><a href="https://github.com/anthropics/claude-code"><img src="./public/providers/claude.svg" width="52" alt="Claude Code"/><br/><b>Claude Code</b></a></td>
|
||||
<td align="center" width="120"><a href="https://github.com/openai/codex"><img src="./public/providers/codex.svg" width="52" alt="Codex CLI"/><br/><b>Codex CLI</b></a></td>
|
||||
<td align="center" width="120"><a href="https://github.com/google-gemini/gemini-cli"><img src="./public/providers/gemini-cli.svg" width="52" alt="Gemini CLI"/><br/><b>Gemini CLI</b></a></td>
|
||||
<td align="center" width="120"><img src="./public/providers/cursor.png" width="52" alt="Cursor"/><br/><b>Cursor</b></td>
|
||||
<td align="center" width="120"><img src="./public/providers/copilot.png" width="52" alt="Copilot"/><br/><b>Copilot</b></td>
|
||||
<td align="center" width="120"><img src="./public/providers/continue.png" width="52" alt="Continue"/><br/><b>Continue</b></td>
|
||||
@@ -349,7 +348,7 @@ Result: 4 layers of fallback = zero downtime
|
||||
|
||||
</div>
|
||||
|
||||
> The most complete catalog of any open-source router: **231 providers**, **50+ with a free tier**, **11 free forever**.
|
||||
> The most complete catalog of any open-source router: **237 providers**, **50+ with a free tier**, **11 free forever**.
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -364,7 +363,6 @@ Result: 4 layers of fallback = zero downtime
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="150"><img src="https://img.shields.io/badge/Cloudflare_AI-F38020?style=flat-square&logo=cloudflare&logoColor=white" alt="Cloudflare AI"/><br/><sub>50+ models<br/>10K neurons/day</sub></td>
|
||||
<td align="center" width="150"><img src="https://img.shields.io/badge/Gemini_CLI-8E75B2?style=flat-square&logo=googlegemini&logoColor=white" alt="Gemini CLI"/><br/><sub>gemini-3-flash<br/>180K/mo free</sub></td>
|
||||
<td align="center" width="150"><img src="https://img.shields.io/badge/NVIDIA_NIM-76B900?style=flat-square&logo=nvidia&logoColor=white" alt="NVIDIA NIM"/><br/><sub>129 models<br/>~40 RPM free</sub></td>
|
||||
<td align="center" width="150"><img src="https://img.shields.io/badge/Cerebras-F15A29?style=flat-square" alt="Cerebras"/><br/><sub>Qwen3 235B<br/>1M tokens/day</sub></td>
|
||||
</tr>
|
||||
@@ -383,16 +381,16 @@ Result: 4 layers of fallback = zero downtime
|
||||
|
||||
> Same app, your machine, your rules. From a global npm install to **your phone** via Termux.
|
||||
|
||||
| Platform | Install | Highlights |
|
||||
| ------------------------- | -------------------------------------------- | --------------------------------------------------------- |
|
||||
| 📦 **npm (global)** | `npm install -g omniroute` | One command, any OS |
|
||||
| 🐳 **Docker** | `docker run … diegosouzapw/omniroute` | Multi-arch **AMD64 + ARM64** |
|
||||
| 🖥️ **Desktop (Electron)** | `npm run electron:build` | Native window + system tray — **Windows / macOS / Linux** |
|
||||
| 💪 **ARM** | native `arm64` | Raspberry Pi, ARM servers, Apple Silicon |
|
||||
| 📱 **Android (Termux)** | `pkg install nodejs-lts && npx -y omniroute` | Runs **on your phone**, 24/7, no root |
|
||||
| 📲 **PWA** | "Add to Home Screen" | Fullscreen, offline, installable from browser |
|
||||
| 🧩 **OpenCode plugin** | `@omniroute/opencode-provider` | Native OpenCode integration |
|
||||
| 🛠️ **From source** | `npm install && npm run dev` | Hack on it, contribute |
|
||||
| Platform | Install | Highlights |
|
||||
| ------------------------- | ---------------------------------------- | --------------------------------------------------------- |
|
||||
| 📦 **npm (global)** | `npm install -g omniroute` | One command, any OS |
|
||||
| 🐳 **Docker** | `docker run … diegosouzapw/omniroute` | Multi-arch **AMD64 + ARM64** |
|
||||
| 🖥️ **Desktop (Electron)** | `npm run electron:build` | Native window + system tray — **Windows / macOS / Linux** |
|
||||
| 💪 **ARM** | native `arm64` | Raspberry Pi, ARM servers, Apple Silicon |
|
||||
| 📱 **Android (Termux)** | `pkg install nodejs && npx -y omniroute` | Runs **on your phone**, 24/7, no root |
|
||||
| 📲 **PWA** | "Add to Home Screen" | Fullscreen, offline, installable from browser |
|
||||
| 🧩 **OpenCode plugin** | `@omniroute/opencode-provider` | Native OpenCode integration |
|
||||
| 🛠️ **From source** | `npm install && npm run dev` | Hack on it, contribute |
|
||||
|
||||
<sub>📖 [Docker Guide](docs/guides/DOCKER_GUIDE.md) · [Desktop](electron/README.md) · [Termux](docs/guides/TERMUX_GUIDE.md) · [PWA](docs/guides/PWA_GUIDE.md) · [OpenCode](docs/frameworks/OPENCODE.md)</sub>
|
||||
|
||||
@@ -806,7 +804,7 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
|
||||
**Will I be charged by OmniRoute?** No — it's free, open-source software on your machine. You only pay paid providers directly. OmniRoute has no billing system.
|
||||
**Are FREE providers really unlimited?** Mostly — Qoder, Pollinations, LongCat, and Cloudflare are free with no per-account credit cap. Kiro is free too but capped at ~50 credits/month per account. Stack multiple free providers in a combo and auto-fallback keeps you serving for $0.
|
||||
**Will compression hurt quality?** No — it only compresses the **input**; code, URLs, JSON are always protected.
|
||||
**Does it work where AI is blocked?** Yes — 3-level proxy + 1proxy marketplace reach all 231 providers.
|
||||
**Does it work where AI is blocked?** Yes — 3-level proxy + 1proxy marketplace reach all 237 providers.
|
||||
|
||||
📖 [User Guide](docs/guides/USER_GUIDE.md) · [API Reference](docs/reference/API_REFERENCE.md) · [Environment Config](docs/reference/ENVIRONMENT.md)
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# bin/_ops-common.sh — shared helpers for the OmniRoute ops runbook scripts.
|
||||
#
|
||||
# Sourced (not executed) by rollback.sh / snapshot-data.sh / restore-data.sh /
|
||||
# restore-policies.sh / cold-start-bench.sh. The runbook context lives in
|
||||
# docs/INCIDENT_RESPONSE.md and docs/PERF_BUDGETS.md.
|
||||
# restore-policies.sh / cold-start-bench.sh — the self-hoster incident-recovery
|
||||
# and cold-start ops tooling. Each script documents its own contract via --help.
|
||||
#
|
||||
# Path resolution mirrors the app (src/lib/db/core.ts): the SQLite store is
|
||||
# $DATA_DIR/storage.sqlite and managed backups go to $DATA_DIR/db_backups
|
||||
|
||||
@@ -71,7 +71,6 @@ import { registerSetupCrush } from "./setup-crush.mjs";
|
||||
import { registerSetupGoose } from "./setup-goose.mjs";
|
||||
import { registerSetupQwen } from "./setup-qwen.mjs";
|
||||
import { registerSetupAider } from "./setup-aider.mjs";
|
||||
import { registerSetupGemini } from "./setup-gemini.mjs";
|
||||
import { registerConnect } from "./connect.mjs";
|
||||
import { registerContexts } from "./contexts.mjs";
|
||||
import { registerTokens } from "./tokens.mjs";
|
||||
@@ -154,7 +153,6 @@ export function registerCommands(program) {
|
||||
registerSetupGoose(program);
|
||||
registerSetupQwen(program);
|
||||
registerSetupAider(program);
|
||||
registerSetupGemini(program);
|
||||
registerConnect(program);
|
||||
registerContexts(program);
|
||||
registerTokens(program);
|
||||
|
||||
@@ -313,7 +313,7 @@ async function maybeStartTray(port, apiPort, supervisor) {
|
||||
if (!isTraySupported()) return;
|
||||
const { default: open } = await import("open").catch(() => ({ default: null }));
|
||||
const dashboardUrl = `http://localhost:${port}`;
|
||||
const tray = initTray({
|
||||
const tray = await initTray({
|
||||
port,
|
||||
onQuit: () => {
|
||||
killTrayIfActive();
|
||||
@@ -329,8 +329,12 @@ async function maybeStartTray(port, apiPort, supervisor) {
|
||||
const { killTray } = await import("../tray/index.mjs");
|
||||
_killTray = killTray;
|
||||
}
|
||||
} catch {
|
||||
// tray is optional — do not fail the server
|
||||
} catch (err) {
|
||||
// tray is optional — do not fail the server, but surface why it failed so
|
||||
// "--tray shows nothing" is diagnosable instead of silent (#4605).
|
||||
process.stderr.write(
|
||||
`[omniroute][tray] failed to start: ${err?.message ?? String(err)}\n`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
/**
|
||||
* omniroute setup-gemini — point the Gemini CLI at OmniRoute's Gemini endpoint.
|
||||
*
|
||||
* The Gemini CLI is NOT OpenAI-compatible — it speaks the native Gemini API.
|
||||
* OmniRoute exposes a Gemini-native surface at /v1beta (e.g.
|
||||
* /v1beta/models/<model>:generateContent), so the CLI can target it via the
|
||||
* @google/genai SDK env `GOOGLE_GEMINI_BASE_URL` (ROOT — the SDK appends /v1beta)
|
||||
* + `GEMINI_API_KEY`. There is no settings.json key for the base URL, so this is
|
||||
* primarily an env recipe; we optionally write ~/.gemini/settings.json `model`.
|
||||
*
|
||||
* ⚠ Known Gemini CLI caveat: it may ignore GOOGLE_GEMINI_BASE_URL if a cached
|
||||
* Google login exists — run `gemini` logged-out / API-key-only for it to take.
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import os from "node:os";
|
||||
import { printHeading, printInfo, printSuccess, printError, createPrompt } from "../io.mjs";
|
||||
import { resolveActiveContext } from "../contexts.mjs";
|
||||
|
||||
function stripToRoot(url) {
|
||||
const s = String(url || "").replace(/\/+$/, "");
|
||||
return s.endsWith("/v1beta") ? s.slice(0, -7) : s.endsWith("/v1") ? s.slice(0, -3) : s;
|
||||
}
|
||||
|
||||
/** Resolve GOOGLE_GEMINI_BASE_URL (ROOT — SDK appends /v1beta) + apiKey. */
|
||||
export function resolveGeminiTarget(opts = {}) {
|
||||
let root;
|
||||
if (opts.remote) root = stripToRoot(opts.remote);
|
||||
else {
|
||||
try {
|
||||
root = stripToRoot(resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT)?.baseUrl);
|
||||
} catch {
|
||||
/* none */
|
||||
}
|
||||
if (!root) root = `http://localhost:${Number(opts.port ?? process.env.PORT ?? 20128) || 20128}`;
|
||||
}
|
||||
let apiKey = opts.apiKey ?? opts["api-key"];
|
||||
if (!apiKey) {
|
||||
try {
|
||||
const c = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT);
|
||||
apiKey = c?.accessToken || c?.apiKey;
|
||||
} catch {
|
||||
/* none */
|
||||
}
|
||||
}
|
||||
if (!apiKey) apiKey = process.env.OMNIROUTE_API_KEY || "";
|
||||
return { baseUrl: root, apiKey };
|
||||
}
|
||||
|
||||
/** The guaranteed env recipe (pure → testable). */
|
||||
export function buildGeminiRecipe({ baseUrl, model }) {
|
||||
return [
|
||||
`export GOOGLE_GEMINI_BASE_URL=${baseUrl}`,
|
||||
"export GEMINI_API_KEY=$OMNIROUTE_API_KEY",
|
||||
`export GEMINI_MODEL=${model}`,
|
||||
`gemini -p "reply OK" # or: gemini (interactive)`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
/** Merge the model into ~/.gemini/settings.json (base URL is env-only). */
|
||||
export function buildGeminiSettings(existing, { model }) {
|
||||
const s = existing && typeof existing === "object" ? { ...existing } : {};
|
||||
if (model) s.model = model;
|
||||
return s;
|
||||
}
|
||||
|
||||
function readJson(path) {
|
||||
try {
|
||||
if (existsSync(path)) return JSON.parse(readFileSync(path, "utf8"));
|
||||
} catch {
|
||||
/* corrupt/missing */
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
async function fetchGeminiModelIds(baseUrl, apiKey) {
|
||||
try {
|
||||
const res = await fetch(`${baseUrl}/v1beta/models`, {
|
||||
headers: { "x-goog-api-key": apiKey || "" },
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
if (!res.ok) return [];
|
||||
const body = await res.json();
|
||||
return (body.models || []).map((m) => String(m.name || "").replace(/^models\//, "")).filter(Boolean);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function runSetupGeminiCommand(opts = {}) {
|
||||
const { baseUrl, apiKey } = resolveGeminiTarget(opts);
|
||||
const dryRun = Boolean(opts.dryRun ?? opts["dry-run"]);
|
||||
const configPath = opts.configPath ?? opts["config-path"] ?? join(os.homedir(), ".gemini", "settings.json");
|
||||
|
||||
printHeading("OmniRoute → Gemini CLI (native Gemini /v1beta endpoint)");
|
||||
printInfo(`GOOGLE_GEMINI_BASE_URL: ${baseUrl} (root — SDK appends /v1beta)`);
|
||||
|
||||
let model = opts.model;
|
||||
if (!model) {
|
||||
const ids = await fetchGeminiModelIds(baseUrl, apiKey);
|
||||
if (ids.length && !opts.yes) {
|
||||
printInfo(`Examples: ${ids.slice(0, 20).join(", ")}${ids.length > 20 ? " …" : ""}`);
|
||||
const prompt = createPrompt();
|
||||
try {
|
||||
model = await prompt.ask("Model id for Gemini CLI");
|
||||
} finally {
|
||||
prompt.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!model) {
|
||||
printError("A model is required. Pass --model <id>.");
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
console.log(`\n── [dry-run] ${configPath} ── { "model": "${model}" }`);
|
||||
} else {
|
||||
const merged = buildGeminiSettings(readJson(configPath), { model });
|
||||
mkdirSync(join(configPath, ".."), { recursive: true });
|
||||
writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
||||
printSuccess(`Wrote ${configPath} (model)`);
|
||||
}
|
||||
|
||||
printInfo("\nThe base URL is env-only for Gemini CLI — export these:");
|
||||
console.log(buildGeminiRecipe({ baseUrl, model }));
|
||||
printInfo("\n⚠ If Gemini CLI ignores the base URL, you have a cached Google login —");
|
||||
printInfo(" run logged-out (API-key only) so GOOGLE_GEMINI_BASE_URL takes effect.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function registerSetupGemini(program) {
|
||||
program
|
||||
.command("setup-gemini")
|
||||
.description("Point the Gemini CLI at OmniRoute's native Gemini /v1beta endpoint (env recipe + settings model)")
|
||||
.option("--port <port>", "Local OmniRoute port (ignored when --remote is set)", "20128")
|
||||
.option("--remote <url>", "Remote OmniRoute URL, e.g. http://192.168.0.15:20128")
|
||||
.option("--api-key <key>", "OmniRoute API key (defaults to OMNIROUTE_API_KEY env var)")
|
||||
.option("--model <id>", "Model id for Gemini CLI (required unless picked interactively)")
|
||||
.option("--config-path <path>", "settings.json path (default: ~/.gemini/settings.json)")
|
||||
.option("--yes", "Non-interactive: do not prompt (requires --model)")
|
||||
.option("--dry-run", "Print what would be written without touching the filesystem")
|
||||
.action(async (opts) => {
|
||||
const code = await runSetupGeminiCommand(opts);
|
||||
if (code !== 0) process.exit(code);
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* omniroute setup-qwen — configure Qwen Code (QwenLM/qwen-code) for OmniRoute.
|
||||
*
|
||||
* Qwen Code is a terminal AI agent (gemini-cli fork) with a file-based config at
|
||||
* Qwen Code is a terminal AI agent with a file-based config at
|
||||
* ~/.qwen/settings.json. For a custom OpenAI-compatible endpoint it uses a
|
||||
* `modelProviders` entry with authType "openai", baseUrl WITH /v1, and an
|
||||
* `envKey` naming the env var holding the key (secret stays in the env, never the
|
||||
@@ -47,7 +47,9 @@ export function resolveQwenTarget(opts = {}) {
|
||||
/** Merge the OmniRoute modelProvider into Qwen's settings.json (preserve rest). */
|
||||
export function buildQwenSettings(existing, { baseUrl, model }) {
|
||||
const s = existing && typeof existing === "object" ? { ...existing } : {};
|
||||
const providers = Array.isArray(s.modelProviders) ? s.modelProviders.filter((p) => p?.id !== "omniroute") : [];
|
||||
const providers = Array.isArray(s.modelProviders)
|
||||
? s.modelProviders.filter((p) => p?.id !== "omniroute")
|
||||
: [];
|
||||
providers.push({
|
||||
id: "omniroute",
|
||||
name: "OmniRoute",
|
||||
@@ -82,7 +84,7 @@ async function fetchModelIds(baseUrl, apiKey) {
|
||||
});
|
||||
if (!res.ok) return [];
|
||||
const body = await res.json();
|
||||
const list = Array.isArray(body) ? body : body.data ?? body.models ?? [];
|
||||
const list = Array.isArray(body) ? body : (body.data ?? body.models ?? []);
|
||||
return list.map((m) => (typeof m === "string" ? m : m?.id)).filter(Boolean);
|
||||
} catch {
|
||||
return [];
|
||||
@@ -92,7 +94,8 @@ async function fetchModelIds(baseUrl, apiKey) {
|
||||
export async function runSetupQwenCommand(opts = {}) {
|
||||
const { baseUrl, apiKey } = resolveQwenTarget(opts);
|
||||
const dryRun = Boolean(opts.dryRun ?? opts["dry-run"]);
|
||||
const configPath = opts.configPath ?? opts["config-path"] ?? join(os.homedir(), ".qwen", "settings.json");
|
||||
const configPath =
|
||||
opts.configPath ?? opts["config-path"] ?? join(os.homedir(), ".qwen", "settings.json");
|
||||
|
||||
printHeading("OmniRoute → Qwen Code (openai-compatible)");
|
||||
printInfo(`baseUrl: ${baseUrl}`);
|
||||
@@ -126,7 +129,9 @@ export async function runSetupQwenCommand(opts = {}) {
|
||||
writeFileSync(configPath, out, "utf8");
|
||||
printSuccess(`Wrote ${configPath}`);
|
||||
}
|
||||
printInfo("\nProvide the key (settings reference OMNIROUTE_API_KEY): export OMNIROUTE_API_KEY=...");
|
||||
printInfo(
|
||||
"\nProvide the key (settings reference OMNIROUTE_API_KEY): export OMNIROUTE_API_KEY=..."
|
||||
);
|
||||
printInfo('Then run: qwen (or headless: qwen -p "reply OK")');
|
||||
return 0;
|
||||
}
|
||||
@@ -134,7 +139,9 @@ export async function runSetupQwenCommand(opts = {}) {
|
||||
export function registerSetupQwen(program) {
|
||||
program
|
||||
.command("setup-qwen")
|
||||
.description("Configure Qwen Code for OmniRoute: write ~/.qwen/settings.json (openai modelProvider)")
|
||||
.description(
|
||||
"Configure Qwen Code for OmniRoute: write ~/.qwen/settings.json (openai modelProvider)"
|
||||
)
|
||||
.option("--port <port>", "Local OmniRoute port (ignored when --remote is set)", "20128")
|
||||
.option("--remote <url>", "Remote OmniRoute URL, e.g. http://192.168.0.15:20128")
|
||||
.option("--api-key <key>", "OmniRoute API key (defaults to OMNIROUTE_API_KEY env var)")
|
||||
|
||||
@@ -5,10 +5,12 @@ let active = null;
|
||||
|
||||
export { isTraySupported };
|
||||
|
||||
export function initTray({ port, onQuit, onOpenDashboard, onShowLogs }) {
|
||||
export async function initTray({ port, onQuit, onOpenDashboard, onShowLogs }) {
|
||||
if (!isTraySupported()) return null;
|
||||
const ctx = { port, onQuit, onOpenDashboard, onShowLogs };
|
||||
active = process.platform === "win32" ? initWinTray(ctx) : initSystrayUnix(ctx);
|
||||
// initSystrayUnix is async: it lazily installs/loads systray2 from the runtime
|
||||
// dir (trayRuntime.ts) rather than from node_modules. (#4605)
|
||||
active = process.platform === "win32" ? initWinTray(ctx) : await initSystrayUnix(ctx);
|
||||
return active;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,30 +14,31 @@ export function isTraySupported() {
|
||||
return true;
|
||||
}
|
||||
|
||||
function loadSystray2() {
|
||||
const candidates = [
|
||||
() => {
|
||||
const { createRequire } = require("module");
|
||||
const req = createRequire(import.meta.url);
|
||||
return req("systray2").default;
|
||||
},
|
||||
];
|
||||
for (const attempt of candidates) {
|
||||
try {
|
||||
return attempt();
|
||||
} catch {}
|
||||
}
|
||||
return null;
|
||||
// systray2 is NOT a static dependency — it is lazily installed into
|
||||
// ~/.omniroute/runtime by trayRuntime.ts (loadSystray). The previous inline
|
||||
// loader called `require("module")`, which throws `ReferenceError: require is
|
||||
// not defined` in this ESM file (package "type":"module"); the throw was
|
||||
// silently swallowed, so the tray never appeared on macOS/Linux with no error
|
||||
// printed (#4605, regressed in v3.8.34). Delegate to the runtime loader, which
|
||||
// resolves systray2 from the runtime dir and surfaces install/import failures.
|
||||
async function loadSystray2() {
|
||||
const { loadSystray } = await import("../runtime/trayRuntime.ts");
|
||||
return loadSystray();
|
||||
}
|
||||
|
||||
function getIconBase64() {
|
||||
const iconPath = join(__dirname, "icons", "icon.png");
|
||||
// Icon ships at bin/cli/tray/icon.png — the previous "icons/icon.png" path
|
||||
// never existed, so the tray was created with an empty icon (#4605).
|
||||
const iconPath = join(__dirname, "icon.png");
|
||||
if (existsSync(iconPath)) return readFileSync(iconPath).toString("base64");
|
||||
return "";
|
||||
}
|
||||
|
||||
export function initSystrayUnix({ port, onQuit, onOpenDashboard, onShowLogs }) {
|
||||
const SysTray = loadSystray2();
|
||||
export async function initSystrayUnix(
|
||||
{ port, onQuit, onOpenDashboard, onShowLogs },
|
||||
loadCtor = loadSystray2
|
||||
) {
|
||||
const SysTray = await loadCtor();
|
||||
if (!SysTray) return null;
|
||||
|
||||
const autostartEnabled = isAutostartEnabled();
|
||||
@@ -57,7 +58,10 @@ export function initSystrayUnix({ port, onQuit, onOpenDashboard, onShowLogs }) {
|
||||
tray = new SysTray({
|
||||
menu: {
|
||||
icon: getIconBase64(),
|
||||
isTemplateIcon: process.platform === "darwin",
|
||||
// isTemplateIcon must be false: icon.png is a full-color RGBA logo, and
|
||||
// macOS template mode uses only the alpha channel → a solid white square
|
||||
// (the icon looked "missing" even when the tray loaded). (PR #1080)
|
||||
isTemplateIcon: false,
|
||||
title: "",
|
||||
tooltip: `OmniRoute — port ${port}`,
|
||||
items,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# bin/cold-start-bench.sh — measure OmniRoute cold-start against the budgets in
|
||||
# docs/PERF_BUDGETS.md §5 (container start → HTTP listening ≤ 800 ms; first warm
|
||||
# bin/cold-start-bench.sh — measure OmniRoute cold-start against the target
|
||||
# budgets (container start → HTTP listening ≤ 800 ms; first warm
|
||||
# TTFB ≤ 200 ms). Boots the server on a throwaway port, times until
|
||||
# /api/health/ping answers 200, measures a warm request, and reports PASS/FAIL.
|
||||
set -euo pipefail
|
||||
@@ -13,7 +13,7 @@ Usage: bin/cold-start-bench.sh [--port <n>] [--start-cmd "<cmd>"] [--url <base>]
|
||||
[--listen-budget-ms <n>] [--ttfb-budget-ms <n>] [-h|--help]
|
||||
|
||||
Boots OmniRoute, times cold-start to the first /api/health/ping 200, measures
|
||||
warm TTFB, and compares against the PERF_BUDGETS.md §5 budgets
|
||||
warm TTFB, and compares against the cold-start budgets
|
||||
(listen ≤ 800 ms, TTFB ≤ 200 ms). Exits non-zero if a budget is exceeded.
|
||||
|
||||
--url benches an already-running server instead of booting one (skips the boot
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
export const SECURE_NODE_LINES = Object.freeze([
|
||||
Object.freeze({ major: 20, minor: 20, patch: 2 }),
|
||||
Object.freeze({ major: 22, minor: 22, patch: 2 }),
|
||||
Object.freeze({ major: 24, minor: 0, patch: 0 }),
|
||||
Object.freeze({ major: 25, minor: 0, patch: 0 }),
|
||||
@@ -9,9 +8,9 @@ export const SECURE_NODE_LINES = Object.freeze([
|
||||
]);
|
||||
|
||||
export const RECOMMENDED_NODE_VERSION = "24.14.1";
|
||||
export const SUPPORTED_NODE_RANGE = ">=20.20.2 <21 || >=22.22.2 <23 || >=24.0.0 <27";
|
||||
export const SUPPORTED_NODE_RANGE = ">=22.22.2 <23 || >=24.0.0 <27";
|
||||
export const SUPPORTED_NODE_DISPLAY =
|
||||
"Node.js 20.20.2+ (20.x LTS), 22.22.2+ (22.x LTS), 24.0.0+ (24.x LTS), 25.0.0+ (25.x), or 26.0.0+ (26.x)";
|
||||
"Node.js 22.22.2+ (22.x LTS), 24.0.0+ (24.x LTS), 25.0.0+ (25.x), or 26.0.0+ (26.x)";
|
||||
|
||||
function formatVersion(version) {
|
||||
return `${version.major}.${version.minor}.${version.patch}`;
|
||||
@@ -78,7 +77,7 @@ export function getNodeRuntimeWarning(version = process.versions.node) {
|
||||
}
|
||||
|
||||
if (support.reason === "unreleased-major") {
|
||||
return `Node.js ${support.nodeVersion} is outside the supported LTS lines. OmniRoute currently supports Node.js 20.x, 22.x, 24.x, 25.x, and 26.x.`;
|
||||
return `Node.js ${support.nodeVersion} is outside the supported LTS lines. OmniRoute currently supports Node.js 22.x, 24.x, 25.x, and 26.x.`;
|
||||
}
|
||||
|
||||
return `Node.js ${support.nodeVersion} is outside OmniRoute's approved secure runtime policy.`;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# bin/restore-data.sh — restore the OmniRoute SQLite data volume from a snapshot
|
||||
# created by bin/snapshot-data.sh. Used by the data-layer incident runbook
|
||||
# (docs/INCIDENT_RESPONSE.md §4.4) after stopping writers.
|
||||
# created by bin/snapshot-data.sh. Used by the data-layer incident-recovery
|
||||
# flow after stopping writers.
|
||||
#
|
||||
# Safety: takes a pre-restore snapshot of the CURRENT data, refuses to run
|
||||
# unattended without --yes, and verifies the snapshot before overwriting.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# bin/restore-policies.sh — restore ONLY the API-key policy tables from a
|
||||
# snapshot, leaving request/audit/runtime state intact. Used by the auth-layer
|
||||
# incident runbook (docs/INCIDENT_RESPONSE.md §4.3) when policies_active is empty
|
||||
# incident-recovery flow when policies_active is empty
|
||||
# but the rest of the database is healthy (so a full restore-data is overkill).
|
||||
#
|
||||
# "Policy" tables = api_key* definition tables (the key + its limits/allowed
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# bin/rollback.sh — roll OmniRoute back to a previous release to mitigate a bad
|
||||
# deploy. Used by the incident runbook (docs/INCIDENT_RESPONSE.md §3 / §4).
|
||||
# deploy. Part of the deploy-rollback incident-recovery flow.
|
||||
#
|
||||
# Methods (auto-detected; override with --method):
|
||||
# • npm — `npm install -g omniroute@<version>` and, if PM2 manages it,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# bin/snapshot-data.sh — consistent point-in-time snapshot of the OmniRoute data
|
||||
# volume (the SQLite store under $DATA_DIR). Used by the data-layer incident
|
||||
# runbook (docs/INCIDENT_RESPONSE.md §4.4) before any restore.
|
||||
# volume (the SQLite store under $DATA_DIR). Used by the data-layer
|
||||
# incident-recovery flow before any restore.
|
||||
#
|
||||
# Output: a directory $DB_BACKUPS_DIR/snapshot_<UTC>[_<label>] holding a
|
||||
# `VACUUM INTO` copy of storage.sqlite (consistent even with WAL writers active)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"_comment": "Catraca de complexidade (check-complexity.mjs, ESLint core rules complexity>=15 e max-lines-per-function>80 sobre src+open-sse+electron+bin via eslint.complexity.config.mjs). Conta total de violacoes; so pode cair. --update ratcheta.",
|
||||
"count": 1980,
|
||||
"count": 1981,
|
||||
"_rebaseline_2026_06_28_v3840_5237_reconcile": "1980->1981 (+1). Inherited release/v3.8.40 drift surfaced while merging PR #5237 (impersonation-UA refresh) — the +1 is present on the pristine release tip (d8a392a47) WITHOUT #5237's changes, so it is #5222 (antigravity fallback-LRU retry) / #5221 (command-code) growth that merged via --admin without ratcheting complexity (the PR->release fast-gates do not run check:complexity). #5237 itself is complexity-net-zero: its only edits are a single UA constant, a regenerated golden snapshot, and baseline JSONs. Structural reduction tracked in #3501.",
|
||||
"_rebaseline_2026_06_27_v3838_release": "1978->1980 (+2). v3.8.38 cycle-close drift surfaced by the release-green pre-flight (check:complexity does NOT run on PR->release fast-gates). +2 from late-cycle feature/fix merges (compression fidelity-gate steps #5143, SSE hardening). Release-finalize working tree touches ONLY CHANGELOG.md + i18n mirrors + the 2 baseline JSONs — 0 production-code change. Structural reduction tracked in #3501.",
|
||||
"_rebaseline_2026_06_26_v3838_ownerprs_batch": "1972->1978 (+6). Drift do lote de merges de PRs do dono + contribuidores em release/v3.8.38 (sessao /review-prs): #4845 (antigravity convertGeminiToOpenAI), #5105 (executor zenmux-free), #5020 (executor grok-cli), #4940 (usage dedupe guard), #5093 (resilience: quota cutoff/gemini mime/model-lockout cooldown), #5015 (quota hydration + auto-combo scoping). Cada um e crescimento de feature/fix legitimo recem-TDD'd, nao regressao; o gate check:complexity NAO roda no fast-path PR->release, entao o ramo acumula sem rebaselinar (mesma familia dos rebaselines anteriores). #5121 cookie-dedup foi mantido complexity-NEUTRO via extracao do helper findExistingCookieConnection. Reducao estrutural fica como debt (#3501).",
|
||||
"_rebaseline_2026_06_26_v3838_release_fast_gate": "1963->1972 (+9). Reconciles inherited release/v3.8.38 drift surfaced by PR #5124. Local origin/release/v3.8.38 measured 1971 and this PR also measured 1971 after refactoring the new JSON-body SSE sniffing path, while the GitHub Ubuntu fast gate measured 1972; use the CI-observed value so the release branch gate is deterministic. The streaming fix is complexity-net-zero relative to the local release base. The release fast-path does not consistently ratchet complexity between release-cycle merges; keep structural reductions as separate debt.",
|
||||
|
||||
@@ -24,10 +24,8 @@
|
||||
"@testing-library/jest-dom",
|
||||
"@testing-library/react",
|
||||
"@toon-format/toon",
|
||||
"@types/bcryptjs",
|
||||
"@types/better-sqlite3",
|
||||
"@types/bun",
|
||||
"@types/keytar",
|
||||
"@types/mdx",
|
||||
"@types/node",
|
||||
"@types/react",
|
||||
@@ -46,6 +44,7 @@
|
||||
"concurrently",
|
||||
"cross-env",
|
||||
"csv-stringify",
|
||||
"dompurify",
|
||||
"dpdm",
|
||||
"electron",
|
||||
"electron-builder",
|
||||
|
||||
@@ -135,10 +135,11 @@
|
||||
"open-sse/executors/deepseek-web.ts": 1148,
|
||||
"_rebaseline_2026_06_22_4644_deepseek_web_tools": "PR #4644 (BugsBag/robust deepseek-web tool-call parsing): open-sse/executors/deepseek-web.ts 1117->1125 (+8). The new agentic tool-call path emits surrounding text + reasoning before tool_calls and swaps to the dedicated deepseekWebTools.ts parser; the +8 lines are cohesive wiring at the existing transformSSE chokepoint (the parser itself lives in the new deepseekWebTools.ts file, already under cap). The PR's own fast-gate (PR->release) does not run check:file-size, so this surfaced only at release reconcile. Covered by tests/unit/deepseek-web-tools-variants.test.ts + deepseek-web-tools-execute.test.ts.",
|
||||
"_rebaseline_2026_06_23_4712_deepseek_web_tool_results": "PR for #4712 (deepseek-web drops role:tool): open-sse/executors/deepseek-web.ts 1125->1148 (+23). messagesToPrompt() now folds role:\"tool\" results into the single-prompt transcript (recovering the tool name from the preceding assistant tool_calls by tool_call_id) instead of silently dropping them; the lines are cohesive wiring inside the existing function. Covered by tests/unit/deepseek-web-tool-result-prompt-4712.test.ts.",
|
||||
"_rebaseline_2026_06_28_5237_impersonation_ua_refresh": "PR #5237 (refresh impersonation UAs): grok-web.ts 1871->1873 (+2), muse-spark-web.ts 1284->1302 (+18), perplexity-web.ts 1013->1032 (+19). Net semantic change in each file is a single User-Agent constant (Chrome 147->149 for grok/muse; perplexity kept at Firefox 148 to stay matched with the firefox_148 TLS profile — the contributor's 152 bump was reverted to avoid a UA-vs-JA3 mismatch, #2459). The growth is Prettier reflow that lint-staged unavoidably applies to these grandfathered long-line files the moment they are touched; not extractable. src/sse/services/auth.ts 2336->2401 in the same reconcile is #5222's antigravity-LRU-retry growth that merged via --admin without a baseline bump.",
|
||||
"open-sse/executors/duckduckgo-web.ts": 925,
|
||||
"open-sse/executors/grok-web.ts": 1871,
|
||||
"open-sse/executors/muse-spark-web.ts": 1284,
|
||||
"open-sse/executors/perplexity-web.ts": 1013,
|
||||
"open-sse/executors/grok-web.ts": 1873,
|
||||
"open-sse/executors/muse-spark-web.ts": 1302,
|
||||
"open-sse/executors/perplexity-web.ts": 1032,
|
||||
"open-sse/handlers/audioSpeech.ts": 1061,
|
||||
"open-sse/handlers/chatCore.ts": 5125,
|
||||
"open-sse/handlers/imageGeneration.ts": 3777,
|
||||
@@ -160,9 +161,12 @@
|
||||
"_rebaseline_2026_06_24_task_aware_routing": "Task-aware routing strategy (port PR #2045, OmniRoute #4945): combo.ts 3190->3225 (+35) = one new `else if (strategy === \"task-aware\")` dispatch branch delegating 100% to selectTaskAwareTarget + its imports/log lines. All scoring/classification logic lives OUT of the god-file in the new leaf open-sse/services/taskAwareRouting.ts (553 LOC <cap). Only the dispatch wiring is irreducible at the existing combo strategy chokepoint (mirrors quota-share/headroom/reset-aware branches). ZERO existing strategy cases modified. Covered by tests/unit/combo-task-aware.test.ts (35 tests). Structural shrink of combo.ts tracked in #3501.",
|
||||
"open-sse/services/combo.ts": 3368,
|
||||
"_rebaseline_2026_06_26_fidelity_gate_extraction": "Milestone-B fidelity-gate wiring residual: bodyToText+gateAdvance extracted to fidelityGateStep.ts (889->854, -35), but the StackOptions.fidelityGate field, the `const fidelityGate` reads at the two stacked-loop dispatch chokepoints, and the import of FidelityGateConfig are irreducible wiring that cannot leave strategySelector without an architectural refactor of the pre-existing stacked pipeline. Net: 889->854 (+6 vs the pre-Milestone-B frozen 848). Covered by tests/unit/compression/*.test.ts (940 pass).",
|
||||
"open-sse/services/compression/strategySelector.ts": 854,
|
||||
"_rebaseline_2026_06_28_5243_risk_gate_prepass": "PR #5243 (compression risk-gate pre-pass) own growth: open-sse/services/compression/strategySelector.ts 854->899 (+45). The three exported entry points (applyCompression/applyStackedCompression/applyStackedCompressionAsync) become thin wrappers over pure-extracted private bodies (runCompression/runStackedCompression/runStackedCompressionAsync) so the risk-gate mask->run->restore wrapper sits strictly OUTSIDE the per-step loop — a single universal integration point. The wrapper logic itself (resolveRiskGate/withRiskGate) lives in the new riskGate/strategyWrap.ts (<cap); the residual growth is the duplicated thin-wrapper signatures + the extracted bodies' dispatch boundary, guarded by a byte-identical parity test (riskGateIntegration). Default off (DEFAULT_COMPRESSION_CONFIG unchanged). Not extractable without hiding the dispatch boundary, mirroring prior compression rebaselines. Structural shrink tracked in #3501.",
|
||||
"_rebaseline_2026_06_29_5286_memoization": "PR #5286 own growth: strategySelector.ts 899->960 (+61 = the opt-in result-memoization branches in applyCompression/applyCompressionAsync — principal+determinism gate, makeMemoKey lookup/store with model+supportsVision folded into the key, recompute-with-memo-off). Default off (memoizeCompressionResults), so zero behavior change. The memo helpers live in the leaf resultMemo.ts (<cap); the chokepoint wiring here is not extractable. Structural shrink of this hot-path file tracked in #3501.",
|
||||
"open-sse/services/compression/strategySelector.ts": 997,
|
||||
"open-sse/services/rateLimitManager.ts": 1035,
|
||||
"open-sse/services/tokenRefresh.ts": 2103,
|
||||
"_rebaseline_2026_06_29_4038_cas_guard": "PR (#4038) own growth: tokenRefresh.ts 2103->2181 (+78 = the compare-and-swap guard on the refresh persist — runWithCasGuard/getActiveCasGuard AsyncLocalStorage pair mirroring runWithOnPersist, casGuardShouldSkipPersist that rereads the row right before persisting and skips the write when a concurrent writer already rotated the refresh_token past the one presented, plus getCasGuardStats counters). Fixes the sibling-rotation-revert → token-family-revocation storm. Gated behind an active guard (opt-in; no guard => byte-identical). Wiring lives at the two persist chokepoints inside getAccessToken; the comparison reuses wasRefreshTokenRotated from refreshSerializer. Not extractable without splitting the refresh hot path.",
|
||||
"open-sse/services/tokenRefresh.ts": 2181,
|
||||
"open-sse/services/usage.ts": 3454,
|
||||
"open-sse/translator/request/openai-to-gemini.ts": 906,
|
||||
"open-sse/translator/request/openai-to-kiro.ts": 842,
|
||||
@@ -225,9 +229,9 @@
|
||||
"src/lib/providers/validation.ts": 4523,
|
||||
"src/lib/resilience/settings.ts": 841,
|
||||
"src/lib/tailscaleTunnel.ts": 1202,
|
||||
"src/lib/usage/callLogs.ts": 975,
|
||||
"src/lib/usage/callLogs.ts": 985,
|
||||
"src/lib/usage/providerLimits.ts": 955,
|
||||
"src/lib/usage/usageHistory.ts": 983,
|
||||
"src/lib/usage/usageHistory.ts": 988,
|
||||
"_rebaseline_2026_06_27_5193_5203_antigravity_oauthmodal": "Antigravity remote-login own growth: OAuthModal.tsx 960->969 (gate units). #5193 (+~4: remote paste instruction shown for all remote incl. Google + its rationale comment) and #5203 (+~5: handleManualSubmit credential-blob branch + button guard; submit logic extracted to oauthBlobSubmit.ts to minimize). Frozen set to the SUM so either merge order passes. Cohesive at the existing manual-submit chokepoint.",
|
||||
"src/shared/components/OAuthModal.tsx": 969,
|
||||
"src/shared/components/RequestLoggerV2.tsx": 1316,
|
||||
@@ -238,8 +242,10 @@
|
||||
"src/shared/constants/sidebarVisibility.ts": 1198,
|
||||
"src/shared/services/cliRuntime.ts": 1090,
|
||||
"src/shared/validation/schemas.ts": 2523,
|
||||
"src/sse/handlers/chat.ts": 1575,
|
||||
"src/sse/services/auth.ts": 2336,
|
||||
"_rebaseline_2026_06_28_5275_correlation_id_extract": "Extraction of the safe CorrelationId subset of #5275 (hartmark) — request correlation id stored in call_logs (migration 109) and returned via the X-Correlation-Id response header, WITHOUT the combo/resilience or build/lazy-loading changes (those stay in #5275). Own growth: callLogs.ts 975->985 (correlation_id column on CallLogSummaryRow + read/map), usageHistory.ts 983->988 (correlationId metadata normalize), chat.ts 1575->1632 (withCorrelationId response wiring + combo-failure log carrying correlationId), chatHelpers.ts new 811 (withCorrelationId helper + reqId threading; was 791<cap pre-feature). Cohesive request/logging chokepoint wiring; structural shrink of chat.ts tracked in #3501.",
|
||||
"src/sse/handlers/chat.ts": 1632,
|
||||
"src/sse/handlers/chatHelpers.ts": 811,
|
||||
"src/sse/services/auth.ts": 2401,
|
||||
"open-sse/executors/default.ts": 876,
|
||||
"open-sse/translator/request/openai-responses.ts": 902,
|
||||
"open-sse/executors/kiro.ts": 944,
|
||||
@@ -264,7 +270,7 @@
|
||||
"tests/unit/db-settings-crud.test.ts": 941,
|
||||
"tests/unit/deepseek-web.test.ts": 1081,
|
||||
"tests/unit/executor-antigravity.test.ts": 942,
|
||||
"tests/unit/executor-codex.test.ts": 1340,
|
||||
"tests/unit/executor-codex.test.ts": 1347,
|
||||
"tests/unit/executor-default-base.test.ts": 1477,
|
||||
"tests/unit/grok-web.test.ts": 2437,
|
||||
"tests/unit/image-generation-handler.test.ts": 2019,
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
"_comment": "Catraca de qualidade. 'down' = nao pode aumentar; 'up' = nao pode cair. Atualize via 'npm run quality:ratchet -- --update' (somente quando melhora). Cada valor e um numero REAL medido, nunca um chute. Cobertura entra na Fase 4 a partir de um run de cobertura mergeada no CI.",
|
||||
"metrics": {
|
||||
"eslintWarnings": {
|
||||
"value": 4090,
|
||||
"value": 4103,
|
||||
"direction": "down",
|
||||
"_rebaseline_2026_06_29_v3840_release": "4090->4103 (+13). v3.8.40 cycle drift surfaced by the release-green pre-flight + the release PR Quality Ratchet (the ratchet does NOT run on PR->release fast-gates, so warnings accrued unmeasured across this cycle's ~57 commits — compression roadmap relevance/hard-budget/memoization/transparency/saliency/splitter/tool_search/RTK/QuantumLock #5289/#5288/#5286/#5284/#5285/#5283/#5269/#5268/#5260, ~20 SSE/translator/combo fixes #5248/#5250/#5254/#5261/#5255/#5273/#5258, M365 Copilot provider #5302, public-origin centralization #5278). Trust-but-verify: measured 4103 locally via `npm run quality:collect` on the release tip INCLUDING my reconciliation commits (CHANGELOG + main merge + the 2 regression test fixes 165c823f5) — the test fixes add 0 `any`/warnings (health-autopilot added a NextRequest import + asserts; chat-pipeline changed one Accept string + a comment), so all +13 is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle.",
|
||||
"_rebaseline_2026_06_28_v3839_release": "4002->4090 (+88). v3.8.39 cycle drift surfaced by the release-green pre-flight (the Quality Ratchet does NOT run on PR->release fast-gates, so warnings accrued unmeasured across this cycle's 40 commits — antigravity remote-login + quota-family #5203/#5180/#5193, compression CCR-retrieve + TOON encoder #5187/#5163, ~20 SSE/translator/responses fixes #5156/#5154/#5197/#5204/#5158/#5123/#5166, proxy/health hardening #5202/#5208/#5209/#5201 from @KooshaPari, combo quota-share/context-relay E2E tests #5179/#5168/#5195). Trust-but-verify: this release-finalize working tree touches ONLY CHANGELOG.md, docs/i18n/*/CHANGELOG.md mirrors, README.md and these baselines — 0 production-code change, so all +88 is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle.",
|
||||
"_rebaseline_2026_06_27_v3838_release": "3987->4002 (+15). v3.8.38 cycle drift surfaced by the release-green pre-flight (the Quality Ratchet does NOT run on PR->release fast-gates, so warnings accrued unmeasured across this cycle's ~78 commits — provider adds Factory/Grok-Build/ZenMux-Free/Alibaba-video, ~30 SSE/translator/diagnostics fixes, compression fidelity-gate + playground #5080/#5143, Fusion editor #5074, salvage batches #5138/#5141). Trust-but-verify: this release-finalize working tree touches ONLY CHANGELOG.md, docs/i18n/*/CHANGELOG.md mirrors, README.md and these baselines — 0 production-code change, so all +15 is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle.",
|
||||
"_rebaseline_2026_06_25_v3836_release": "v3.8.36 cycle drift surfaced by the post-merge fix PR #5029 (the Quality Ratchet was SKIPPED on the release PR #4854 itself, and does NOT run on the PR→release fast-gates, so warnings accrued unmeasured across this cycle's 137 commits — Quota-Share Fase 2/3 features, god-file decomposition #3501/#4811-#4956, 14 external contributor PRs). 3912→3970 (+58), the exact value measured by the CI Quality Ratchet on #5029. Trust-but-verify: this fix PR touches ONLY scripts/build/pack-artifact-policy.ts (a string-literal allowlist array, scripts/ is eslint-light) and tests/integration/resilience-http-e2e.test.ts (2 string keys, no `any`) — 0 new warnings, so all +58 is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Same precedent as _rebaseline_2026_06_23_v3835_release. Tighten via --require-tighten next cycle.",
|
||||
|
||||
@@ -1,389 +0,0 @@
|
||||
# OmniRoute Documentation Overhaul Plan
|
||||
|
||||
> Goal: Make all docs accessible to non-tech users while preserving technical depth for developers.
|
||||
> Strategy: Two-tier docs (User Guide + Technical Reference) with cross-links.
|
||||
|
||||
---
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **User Guide** (new files in `docs/guides/`)
|
||||
- Plain language, no jargon
|
||||
- "Why → What → How" structure
|
||||
- Tables for comparisons
|
||||
- Copy-paste examples
|
||||
- "What's next?" at the end
|
||||
|
||||
2. **Technical Reference** (existing files)
|
||||
- Keep as-is (already well-written)
|
||||
- Add "For Users" callout at top linking to user guide
|
||||
- Add "Prerequisites" section
|
||||
- Ensure consistent formatting
|
||||
|
||||
3. **Cross-Links**
|
||||
- User guides link to technical docs for "learn more"
|
||||
- Technical docs link to user guides for "quick start"
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: User-Facing Quick Start Docs (Highest Impact)
|
||||
|
||||
### 1.1 `docs/guides/QUICK-START.md` — NEW
|
||||
|
||||
**Replaces**: Condensed version of README.md
|
||||
**Content**:
|
||||
|
||||
- Install (npm, Docker, source)
|
||||
- Connect a free provider (3 steps)
|
||||
- Point your IDE to OmniRoute
|
||||
- Verify it works
|
||||
- "What's next?" → link to other guides
|
||||
|
||||
### 1.2 `docs/guides/AUTO-COMBO-GUIDE.md` — NEW
|
||||
|
||||
**Complements**: `docs/routing/AUTO-COMBO.md`
|
||||
**Content**:
|
||||
|
||||
- What auto-combo does (30-second version)
|
||||
- Which `auto` should I use? (table)
|
||||
- How it picks providers (simple version)
|
||||
- Common questions (FAQ)
|
||||
- "Learn more" → link to technical reference
|
||||
|
||||
### 1.3 `docs/guides/PROVIDERS-GUIDE.md` — NEW
|
||||
|
||||
**Complements**: `docs/reference/PROVIDER_REFERENCE.md`
|
||||
**Content**:
|
||||
|
||||
- What is a provider? (analogy)
|
||||
- How to connect a provider (step-by-step)
|
||||
- Free vs paid providers (table)
|
||||
- OAuth vs API key (which do I need?)
|
||||
- Troubleshooting connection issues
|
||||
- "Learn more" → link to provider reference
|
||||
|
||||
### 1.4 `docs/guides/FREE-TIERS-GUIDE.md` — NEW
|
||||
|
||||
**Complements**: `docs/reference/FREE_TIERS.md`
|
||||
**Content**:
|
||||
|
||||
- What are free tiers? (analogy)
|
||||
- Best free providers (table with quotas)
|
||||
- How to stack free tiers for unlimited usage
|
||||
- Common questions (FAQ)
|
||||
- "Learn more" → link to free tiers reference
|
||||
|
||||
### 1.5 `docs/guides/TROUBLESHOOTING.md` — REWRITE
|
||||
|
||||
**Current**: Technical, code-heavy
|
||||
**New**: "I see X → Do Y" format
|
||||
**Content**:
|
||||
|
||||
- Problem → Solution table
|
||||
- Common error messages (with screenshots)
|
||||
- "Still stuck?" → link to community
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Refine Existing User Docs
|
||||
|
||||
### 2.1 `docs/guides/USER_GUIDE.md` — UPDATE
|
||||
|
||||
**Changes**:
|
||||
|
||||
- Add "What you'll learn" section at top
|
||||
- Remove jargon, explain terms
|
||||
- Add step-by-step screenshots
|
||||
- Add "Common mistakes" section
|
||||
|
||||
### 2.2 `docs/guides/SETUP_GUIDE.md` — UPDATE
|
||||
|
||||
**Changes**:
|
||||
|
||||
- Add "Prerequisites" section
|
||||
- Simplify commands (one-liners)
|
||||
- Add "Verify it works" step
|
||||
- Add "Troubleshooting" section
|
||||
|
||||
### 2.3 `docs/guides/FEATURES.md` — UPDATE
|
||||
|
||||
**Changes**:
|
||||
|
||||
- Add "Why this matters" for each feature
|
||||
- Add use-case examples
|
||||
- Add "How to enable" for each feature
|
||||
- Add screenshots
|
||||
|
||||
### 2.4 `docs/guides/DOCKER_GUIDE.md` — UPDATE
|
||||
|
||||
**Changes**:
|
||||
|
||||
- Add "Docker for beginners" section
|
||||
- Simplify commands
|
||||
- Add "Common Docker issues" section
|
||||
- Add "What's next?" section
|
||||
|
||||
### 2.5 `docs/guides/ELECTRON_GUIDE.md` — UPDATE
|
||||
|
||||
**Changes**:
|
||||
|
||||
- Add "What is Electron?" explanation
|
||||
- Add screenshots
|
||||
- Add "Common issues" section
|
||||
|
||||
### 2.6 `docs/guides/TERMUX_GUIDE.md` — UPDATE
|
||||
|
||||
**Changes**:
|
||||
|
||||
- Add "What is Termux?" explanation
|
||||
- Add step-by-step with screenshots
|
||||
- Add "Common issues" section
|
||||
|
||||
### 2.7 `docs/guides/PWA_GUIDE.md` — UPDATE
|
||||
|
||||
**Changes**:
|
||||
|
||||
- Add "What is a PWA?" explanation
|
||||
- Add step-by-step with screenshots
|
||||
- Add "Common issues" section
|
||||
|
||||
### 2.8 `docs/guides/I18N.md` — UPDATE
|
||||
|
||||
**Changes**:
|
||||
|
||||
- Add "How to change language" (user perspective)
|
||||
- Add "How to contribute translations" (contributor perspective)
|
||||
|
||||
### 2.9 `docs/guides/KIRO_SETUP.md` — UPDATE
|
||||
|
||||
**Changes**:
|
||||
|
||||
- Add "What is Kiro?" explanation
|
||||
- Add step-by-step with screenshots
|
||||
- Add "Common issues" section
|
||||
|
||||
### 2.10 `docs/guides/UNINSTALL.md` — UPDATE
|
||||
|
||||
**Changes**:
|
||||
|
||||
- Add "Why uninstall?" section (common reasons)
|
||||
- Add "Before you uninstall" checklist
|
||||
- Simplify commands
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Technical Docs (Add Cross-Links, Keep As-Is)
|
||||
|
||||
### 3.1 `docs/architecture/` (6 files)
|
||||
|
||||
**Changes**: Add "For Users" callout at top → link to user guide
|
||||
|
||||
- ARCHITECTURE.md
|
||||
- AUTHZ_GUIDE.md
|
||||
- CODEBASE_DOCUMENTATION.md
|
||||
- MONITORING_SECTIONS.md
|
||||
- REPOSITORY_MAP.md
|
||||
- RESILIENCE_GUIDE.md
|
||||
|
||||
### 3.2 `docs/frameworks/` (16 files)
|
||||
|
||||
**Changes**: Add "For Users" callout at top → link to user guide
|
||||
|
||||
- A2A-SERVER.md
|
||||
- AGENT-SKILLS.md
|
||||
- AGENTBRIDGE.md
|
||||
- AGENT_PROTOCOLS_GUIDE.md
|
||||
- CLOUD_AGENT.md
|
||||
- EMBEDDED-SERVICES.md
|
||||
- EVALS.md
|
||||
- GAMIFICATION.md
|
||||
- MCP-SERVER.md
|
||||
- MEMORY.md
|
||||
- OPENCODE.md
|
||||
- PLAYGROUND_STUDIO.md
|
||||
- SEARCH_TOOLS_STUDIO.md
|
||||
- SKILLS.md
|
||||
- TRAFFIC_INSPECTOR.md
|
||||
- WEBHOOKS.md
|
||||
|
||||
### 3.3 `docs/security/` (9 files)
|
||||
|
||||
**Changes**: Add "For Users" callout at top → link to user guide
|
||||
|
||||
- CLI_TOKEN.md
|
||||
- CLI_TOKEN_AUTH.md
|
||||
- COMPLIANCE.md
|
||||
- ERROR_SANITIZATION.md
|
||||
- GUARDRAILS.md
|
||||
- PUBLIC_CREDS.md
|
||||
- ROUTE_GUARD_TIERS.md
|
||||
- SOCKET_DEV_FINDINGS.md
|
||||
- STEALTH_GUIDE.md
|
||||
|
||||
### 3.4 `docs/ops/` (8 files)
|
||||
|
||||
**Changes**: Add "For Users" callout at top → link to user guide
|
||||
|
||||
- COVERAGE_PLAN.md
|
||||
- E2E_DASHBOARD_SHAKEDOWN_v3.8.0.md
|
||||
- FLY_IO_DEPLOYMENT_GUIDE.md
|
||||
- PROXY_GUIDE.md
|
||||
- RELEASE_CHECKLIST.md
|
||||
- SQLITE_RUNTIME.md
|
||||
- TUNNELS_GUIDE.md
|
||||
- VM_DEPLOYMENT_GUIDE.md
|
||||
|
||||
### 3.5 `docs/compression/` (5 files)
|
||||
|
||||
**Changes**: Add "For Users" callout at top → link to user guide
|
||||
|
||||
- COMPRESSION_ENGINES.md
|
||||
- COMPRESSION_GUIDE.md
|
||||
- COMPRESSION_LANGUAGE_PACKS.md
|
||||
- COMPRESSION_RULES_FORMAT.md
|
||||
- RTK_COMPRESSION.md
|
||||
|
||||
### 3.6 `docs/routing/` (3 files)
|
||||
|
||||
**Changes**: Add "For Users" callout at top → link to user guide
|
||||
|
||||
- AUTO-COMBO.md (→ link to AUTO-COMBO-GUIDE.md)
|
||||
- QUOTA_SHARE.md
|
||||
- REASONING_REPLAY.md
|
||||
|
||||
### 3.7 `docs/reference/` (5 files)
|
||||
|
||||
**Changes**: Add "For Users" callout at top → link to user guide
|
||||
|
||||
- API_REFERENCE.md
|
||||
- CLI-TOOLS.md
|
||||
- ENVIRONMENT.md
|
||||
- FREE_TIERS.md (→ link to FREE-TIERS-GUIDE.md)
|
||||
- PROVIDER_REFERENCE.md (→ link to PROVIDERS-GUIDE.md)
|
||||
|
||||
### 3.8 Other Docs (keep as-is)
|
||||
|
||||
- `docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md` — Already user-friendly
|
||||
- `docs/marketing/TIERS.md` — Already user-friendly
|
||||
- `docs/diagrams/README.md` — Keep as-is
|
||||
- `docs/dev/plugins.md` — Developer-only
|
||||
- `docs/plugins/PLUGIN_SDK.md` — Developer-only
|
||||
- `docs/providers/ZED-DOCKER.md` — Provider-specific
|
||||
- `docs/AGENTROUTER.md` — Provider-specific
|
||||
- `docs/PROVIDERS.md` — Provider-specific
|
||||
- `docs/README.md` — Keep as-is
|
||||
- `docs/SUBMIT_PR.md` — Contributor-only
|
||||
- `docs/releases/v3.8.0.md` — Release notes
|
||||
- `docs/research/` — Internal research
|
||||
- `docs/specs/` — Internal specs
|
||||
- `docs/openspec/` — Internal specs
|
||||
- `docs/superpowers/` — Internal plans
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Update Main README.md
|
||||
|
||||
**Changes**:
|
||||
|
||||
- Add "Quick Start" section (3 steps)
|
||||
- Add "Which `auto` should I use?" table
|
||||
- Add "Free providers" table
|
||||
- Add "Common questions" section
|
||||
- Link to user guides
|
||||
|
||||
---
|
||||
|
||||
## Execution Order
|
||||
|
||||
### Week 1: Phase 1 (User-Facing Quick Start Docs)
|
||||
|
||||
1. `docs/guides/QUICK-START.md` — NEW
|
||||
2. `docs/guides/AUTO-COMBO-GUIDE.md` — NEW
|
||||
3. `docs/guides/PROVIDERS-GUIDE.md` — NEW
|
||||
4. `docs/guides/FREE-TIERS-GUIDE.md` — NEW
|
||||
5. `docs/guides/TROUBLESHOOTING.md` — REWRITE
|
||||
|
||||
### Week 2: Phase 2 (Refine Existing User Docs)
|
||||
|
||||
1. `docs/guides/USER_GUIDE.md` — UPDATE
|
||||
2. `docs/guides/SETUP_GUIDE.md` — UPDATE
|
||||
3. `docs/guides/FEATURES.md` — UPDATE
|
||||
4. `docs/guides/DOCKER_GUIDE.md` — UPDATE
|
||||
5. `docs/guides/ELECTRON_GUIDE.md` — UPDATE
|
||||
6. `docs/guides/TERMUX_GUIDE.md` — UPDATE
|
||||
7. `docs/guides/PWA_GUIDE.md` — UPDATE
|
||||
8. `docs/guides/I18N.md` — UPDATE
|
||||
9. `docs/guides/KIRO_SETUP.md` — UPDATE
|
||||
10. `docs/guides/UNINSTALL.md` — UPDATE
|
||||
|
||||
### Week 3: Phase 3 (Technical Docs Cross-Links)
|
||||
|
||||
1. `docs/architecture/` — Add cross-links
|
||||
2. `docs/frameworks/` — Add cross-links
|
||||
3. `docs/security/` — Add cross-links
|
||||
4. `docs/ops/` — Add cross-links
|
||||
5. `docs/compression/` — Add cross-links
|
||||
6. `docs/routing/` — Add cross-links
|
||||
7. `docs/reference/` — Add cross-links
|
||||
|
||||
### Week 4: Phase 4 (Update Main README.md)
|
||||
|
||||
1. Update README.md with user-friendly sections
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
- [ ] User can install OmniRoute in < 5 minutes (QUICK-START.md)
|
||||
- [ ] User can connect a provider in < 3 minutes (PROVIDERS-GUIDE.md)
|
||||
- [ ] User can use auto-combo in < 1 minute (AUTO-COMBO-GUIDE.md)
|
||||
- [ ] User can find free providers in < 2 minutes (FREE-TIERS-GUIDE.md)
|
||||
- [ ] User can troubleshoot common issues in < 5 minutes (TROUBLESHOOTING.md)
|
||||
- [ ] All technical docs link to user guides
|
||||
- [ ] All user guides link to technical docs
|
||||
|
||||
---
|
||||
|
||||
## File Naming Convention
|
||||
|
||||
- User guides: `docs/guides/[FEATURE]-GUIDE.md` (e.g., `AUTO-COMBO-GUIDE.md`)
|
||||
- Technical docs: Keep existing names (e.g., `docs/routing/AUTO-COMBO.md`)
|
||||
- Cross-links: "For Users" callout at top of technical docs
|
||||
|
||||
---
|
||||
|
||||
## Template for User Guides
|
||||
|
||||
```markdown
|
||||
# [Feature Name]: [One-Line Description]
|
||||
|
||||
## What It Does
|
||||
|
||||
[2-3 sentences explaining what the feature does in plain language]
|
||||
|
||||
## Quick Start
|
||||
|
||||
[Step-by-step instructions to get started]
|
||||
|
||||
## [Main Section]
|
||||
|
||||
[Detailed explanation with tables, examples, screenshots]
|
||||
|
||||
## Common Questions
|
||||
|
||||
[FAQ section with common questions and answers]
|
||||
|
||||
## What's Next?
|
||||
|
||||
[Links to related guides and technical docs]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template for Technical Doc Cross-Links
|
||||
|
||||
```markdown
|
||||
> **For Users**: Looking for a quick start? See the [User Guide](../guides/[FEATURE]-GUIDE.md).
|
||||
|
||||
> **Prerequisites**: [List prerequisites]
|
||||
```
|
||||
@@ -1,174 +0,0 @@
|
||||
# Incident Response Runbook — OmniRoute (2026-06-18)
|
||||
|
||||
**Status**: Authoritative. The 71-pillar audit (L61) references this doc
|
||||
for the `Obs > 2.00` gate.
|
||||
**Owner**: observability-circle (lead: security-circle lead).
|
||||
**SLOs**: see `docs/PERF_BUDGETS.md` § 1 (top-level SLOs) and
|
||||
`ops/slos.yaml` (machine-readable form, generated by the Bifrost team).
|
||||
**Disclosure policy**: see `SECURITY.md` (vulnerability disclosure only,
|
||||
separate flow).
|
||||
|
||||
This runbook is the operational playbook for **non-security** incidents:
|
||||
outages, latency regressions, error-budget burn, and provider-side
|
||||
failures. Vulnerability disclosure stays on `SECURITY.md`; do not route
|
||||
those through this runbook.
|
||||
|
||||
---
|
||||
|
||||
## 1. Severity ladder
|
||||
|
||||
| Sev | Definition | Examples | Page on | Resolve by |
|
||||
|---|---|---|---|---|
|
||||
| **SEV-1** | User-visible outage; > 50 % of requests failing or > 2x SLO breach for 5 min. | Cluster down; auth layer broken; 5xx flood. | On-call P0 (immediate) | 4 h |
|
||||
| **SEV-2** | Significant degradation; 1.5–2x SLO breach for 15 min, or single-tenant impact. | Single provider down; p95 > 1.5x budget; rate-limit runaway. | On-call P1 (15 min) | 24 h |
|
||||
| **SEV-3** | Latent bug or near-miss; no current user impact but error budget at risk. | Memory leak trending up; circuit breaker tripping on one provider. | Slack `#omniroute-ops` (next standup) | 7 d |
|
||||
| **SEV-4** | Cosmetic / informational. | Log line noise; non-binding UI glitch. | Next weekly review | Next refactor cycle |
|
||||
|
||||
**Burn-rate escalation** (per `docs/PERF_BUDGETS.md` § 1): 6x for 5 min
|
||||
is SEV-1; 2x for 1 h is SEV-2; sustained < 1x for 7 d demotes to SEV-3.
|
||||
|
||||
---
|
||||
|
||||
## 2. Detection sources
|
||||
|
||||
| Source | Signal | Routing |
|
||||
|---|---|---|
|
||||
| Prometheus (`/metrics`) | Counter deltas (5xx, latency) | Alertmanager → PagerDuty |
|
||||
| Grafana SLO dashboards | SLO burn-rate panels | Slack `#omniroute-ops` |
|
||||
| Uptime probe (`/api/health/ping`) | 3 consecutive failures from 3 regions | Alertmanager → PagerDuty |
|
||||
| Dependabot | New CVE in dependency | GitHub issue + Slack `#security` |
|
||||
| User report (support@) | Manual triage | Slack `#omniroute-triage` |
|
||||
| Error budget burn alert | `slo_burn_rate > threshold` | Alertmanager |
|
||||
|
||||
Prometheus and Alertmanager are configured in the deploy repo (see
|
||||
`docs/operations/DEPLOY.md` once published; currently inline in
|
||||
`docker-compose.prod.yml`).
|
||||
|
||||
---
|
||||
|
||||
## 3. First-15-minutes checklist
|
||||
|
||||
When paged, the on-call engineer runs this checklist verbatim. **Do
|
||||
not** skip steps; each is timed.
|
||||
|
||||
1. **0:00** — Acknowledge the page in PagerDuty. Stops the escalation
|
||||
timer and notifies the secondary.
|
||||
2. **0:02** — Open the [SLO dashboard][dash] and the [incident
|
||||
channel][chan] (`#inc-YYYY-MM-DD-slug`). Post a single-line ack
|
||||
with the alert name and the time.
|
||||
3. **0:05** — Classify severity per § 1. If SEV-1 or SEV-2, declare
|
||||
the incident in the channel and tag `@incident-commander`.
|
||||
4. **0:08** — Capture the alert payload, the most recent deploy SHA,
|
||||
and the top 5 slow / erroring endpoints. Post to the channel.
|
||||
5. **0:12** — Decide: **mitigate first, root-cause later**. Choose
|
||||
one of:
|
||||
- **Roll back** to the last green deploy (`bin/rollback.sh vX.Y.Z`).
|
||||
- **Failover** to the healthy replicas (Caddy LB removes the bad
|
||||
replica automatically; verify with `curl /api/health/ping`).
|
||||
- **Disable** a broken provider connection via `PUT /api/providers/{id}`
|
||||
with `{ "isActive": false }` (one-line toggle; safe by default).
|
||||
6. **0:15** — Post the chosen mitigation in the channel. If the page
|
||||
is still firing after 5 more minutes, escalate to the secondary.
|
||||
|
||||
[chan]: https://phenotype.slack.com/archives/incidents
|
||||
[dash]: https://grafana.phenotype.internal/d/omniroute-slos
|
||||
|
||||
---
|
||||
|
||||
## 4. Mitigation runbooks (per failure mode)
|
||||
|
||||
### 4.1 Provider outage (single provider down)
|
||||
|
||||
1. `PUT /api/providers/{id}` with `{ "isActive": false }` — toggles the
|
||||
connection off in the registry; all routes re-resolve on next request.
|
||||
2. Verify p95 returns to budget within 5 min.
|
||||
3. If all providers for a model are down, **disable the model** (see
|
||||
`src/lib/a2a/skills/providerDiscovery.ts` for the disable path).
|
||||
4. Update the [status page][status] with a banner if the outage
|
||||
exceeds 15 min.
|
||||
|
||||
[status]: https://status.phenotype.dev
|
||||
|
||||
### 4.2 Cluster-wide latency regression
|
||||
|
||||
1. Check the most recent deploy (`/api/system/version` returns the running version).
|
||||
2. If p95 doubled vs the 7-day baseline, **roll back** to the prior
|
||||
SHA via `bin/rollback.sh`.
|
||||
3. If the regression is provider-side, see § 4.1.
|
||||
|
||||
### 4.3 Auth layer broken (5xx on /v1/responses for all keys)
|
||||
|
||||
1. Check the authz-inventory endpoint:
|
||||
`curl https://api.omniroute.dev/api/settings/authz-inventory | jq`.
|
||||
2. If `policies_active` is empty, restore from the last good backup
|
||||
(`bin/restore-policies.sh <sha>`).
|
||||
3. Roll back if the cause is unclear.
|
||||
|
||||
### 4.4 Data-layer incident (sqlite corruption, audit log gap)
|
||||
|
||||
1. **Stop the cluster** (`docker compose -f docker-compose.prod.yml
|
||||
stop`) — preventing further writes is more important than uptime.
|
||||
2. Snapshot the data volume (`bin/snapshot-data.sh`).
|
||||
3. Open a SEV-1; this is data-loss territory. Page the data-team.
|
||||
4. Restore from the last verified backup (see `docs/BACKUP.md` once
|
||||
published; currently the runbook is `bin/restore-data.sh <sha>`).
|
||||
|
||||
### 4.5 Security incident (vulnerability disclosure)
|
||||
|
||||
**Stop.** This is the `SECURITY.md` path, not this runbook. Page the
|
||||
security on-call (`@security-team`); do not post details to
|
||||
`#omniroute-ops`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Communication
|
||||
|
||||
| Audience | Channel | Cadence | Owner |
|
||||
|---|---|---|---|
|
||||
| Engineering | `#inc-YYYY-MM-DD-slug` | Real-time | Incident commander |
|
||||
| Status page | `status.phenotype.dev` | Every 30 min during SEV-1/2 | On-call |
|
||||
| Customers (email) | `announce@phenotype.dev` | At SEV-1 start + resolution | Comms lead |
|
||||
| Upstream providers | Direct contact | At SEV-1 start | Vendor mgmt |
|
||||
| Postmortem | `docs/postmortem/YYYY-MM-DD-slug.md` | Within 5 business days | Incident commander |
|
||||
|
||||
Postmortem template is at `docs/postmortem/TEMPLATE.md` (forthcoming;
|
||||
see ADR-024 for the cadence and ADR-029 for the postmortem convention).
|
||||
|
||||
---
|
||||
|
||||
## 6. On-call rotation
|
||||
|
||||
| Role | Primary | Secondary | Rotation |
|
||||
|---|---|---|---|
|
||||
| Engineering on-call | security-circle lead | @open-sse | Weekly, Mon 09:00 PDT |
|
||||
| Security on-call | @security-team | — | Weekly |
|
||||
| Data on-call | @db-team | — | Weekly |
|
||||
| Comms lead | @comms | — | As needed |
|
||||
|
||||
**Handoff**: every Monday 09:00 PDT, the outgoing on-call posts a
|
||||
written handoff to the incoming in `#omnirouse-ops-handoff` covering:
|
||||
open SEV-3/4 items, scheduled maintenance windows, and any
|
||||
in-flight mitigations.
|
||||
|
||||
---
|
||||
|
||||
## 7. Postmortem expectations
|
||||
|
||||
- **Blameless**. People did the best they could with the information
|
||||
they had. Focus on systems, signals, and decision points.
|
||||
- **Within 5 business days** of resolution. File via
|
||||
`gh issue create --label postmortem --label SEV-1` (or `--label SEV-2`).
|
||||
- **Action items** must be assigned, dated, and tracked in
|
||||
`docs/TECH_DEBT.md` (P0 < 30 d, P1 < 90 d per that doc's SLA).
|
||||
- **Mandatory attendees**: incident commander, on-call, any engineer
|
||||
who touched the mitigation, and one person who was *not* involved
|
||||
(fresh-eyes review).
|
||||
|
||||
---
|
||||
|
||||
## 8. Review log
|
||||
|
||||
| Date | Reviewer | Change |
|
||||
|---|---|---|
|
||||
| 2026-06-18 | security-circle lead | Initial runbook; severity ladder + 15-min checklist + 4.1–4.5 mitigation runbooks. Closes 71-pillar audit L61 (1/3 → 2/3). |
|
||||
| 2026-07-18 (planned) | observability-circle | Wire on-call rotation into PagerDuty schedule; add the postmortem template. |
|
||||
@@ -1,222 +0,0 @@
|
||||
# Performance Budgets — OmniRoute (2026-06-18)
|
||||
|
||||
**Status**: Authoritative. SLO targets that the 71-pillar audit (L13)
|
||||
references for the `Perf > 2.00` gate.
|
||||
**Methodology**: per-endpoint p50/p95/p99 latency budgets, plus a
|
||||
top-level availability SLO. Budgets are derived from the 3-replica
|
||||
Caddy + Redis topology (commit `038439fa7`); adjust on infra change.
|
||||
**Enforcement**: `benches/perf-gate.k6.js` (k6 reference script) —
|
||||
**not** yet wired into CI; `bench/` already exists in the repo
|
||||
and the script can be added there in a follow-up PR.
|
||||
**Re-evaluation cadence**: quarterly, or on any major infra change.
|
||||
|
||||
---
|
||||
|
||||
## 1. Top-level SLOs
|
||||
|
||||
| SLO | Target | Window | Page on breach |
|
||||
|---|---|---|---|
|
||||
| **Availability** (2xx or 4xx for /v1/* and /api/settings/*) | 99.9% | rolling 30 days | on-call P2 |
|
||||
| **Error budget burn rate** (1xx normalized rate) | < 2x for 1h, < 6x for 5m | 1h / 5m windows | on-call P1 |
|
||||
| **Aggregate p95 latency** (all /v1/*) | ≤ 1.5 s | rolling 5 min | on-call P2 |
|
||||
| **Aggregate p99 latency** (all /v1/*) | ≤ 4.0 s | rolling 5 min | on-call P2 |
|
||||
|
||||
**Error budget**: 30-day window = 43.2 minutes of unavailability at
|
||||
99.9%. Burn rate > 2x is P2; > 6x is P1.
|
||||
|
||||
---
|
||||
|
||||
## 2. Per-endpoint latency budgets
|
||||
|
||||
All budgets measured **server-side** (Next.js Route Handler entry to
|
||||
response start, or last byte for streaming). Stream endpoints are
|
||||
measured to time-of-first-byte (TTFB) since the body is incremental.
|
||||
|
||||
### 2.1 Inference endpoints (the hot path)
|
||||
|
||||
| Endpoint | Method | p50 | p95 | p99 | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| `/v1/responses` (non-stream) | POST | 800 ms | 1.8 s | 3.5 s | Includes translator + provider roundtrip |
|
||||
| `/v1/responses` (stream) | POST (TTFB) | 350 ms | 900 ms | 1.8 s | TTFB only; total duration unbounded |
|
||||
| `/v1/relay/chat/completions` (non-stream) | POST | 1.0 s | 2.2 s | 4.0 s | Includes per-(token,IP) rate-limit check |
|
||||
| `/v1/relay/chat/completions` (stream) | POST (TTFB) | 400 ms | 1.0 s | 2.0 s | |
|
||||
| `/v1/embeddings` | POST | 300 ms | 700 ms | 1.4 s | Pure provider roundtrip; cheap |
|
||||
| `/v1/rerank` | POST | 600 ms | 1.4 s | 2.8 s | |
|
||||
| `/v1/moderations` | POST | 250 ms | 600 ms | 1.2 s | Lightweight classification |
|
||||
| `/v1/audio/speech` | POST | 1.2 s | 3.0 s | 6.0 s | Audio synthesis is slow; budget reflects that |
|
||||
| `/v1/audio/transcriptions` | POST | 2.0 s | 5.0 s | 10.0 s | STT is bounded by audio duration + model size |
|
||||
| `/v1/images/generations` | POST | 4.0 s | 8.0 s | 15.0 s | Image gen is async-bound by provider |
|
||||
| `/v1/videos/generations` | POST (TTFB) | 600 ms | 1.5 s | 3.0 s | Async; client polls `/v1/videos/{id}` |
|
||||
| `/v1/music/generations` | POST | 3.0 s | 6.0 s | 12.0 s | |
|
||||
|
||||
### 2.2 Files + batches
|
||||
|
||||
| Endpoint | Method | p50 | p95 | p99 | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| `/v1/files` (GET) | GET | 80 ms | 200 ms | 400 ms | Cached list |
|
||||
| `/v1/files` (POST upload) | POST | 500 ms | 1.2 s | 2.5 s | 25 MB cap; multipart parse |
|
||||
| `/v1/files/{id}` (GET) | GET | 60 ms | 150 ms | 300 ms | |
|
||||
| `/v1/files/{id}` (DELETE) | DELETE | 80 ms | 200 ms | 400 ms | |
|
||||
| `/v1/files/{id}/content` (download) | GET | 100 ms | 300 ms | 600 ms | + per-MB throughput |
|
||||
| `/v1/batches` (GET) | GET | 150 ms | 400 ms | 800 ms | |
|
||||
| `/v1/batches` (POST create) | POST | 200 ms | 500 ms | 1.0 s | Validates input file then enqueues |
|
||||
| `/v1/batches/{id}` (GET) | GET | 100 ms | 300 ms | 600 ms | |
|
||||
| `/v1/batches/{id}` (DELETE) | DELETE | 100 ms | 300 ms | 600 ms | |
|
||||
| `/v1/batches/delete-completed` (POST) | POST | 400 ms | 1.0 s | 2.0 s | Mass delete; n rows |
|
||||
|
||||
### 2.3 Agents
|
||||
|
||||
| Endpoint | Method | p50 | p95 | p99 | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| `/v1/agents/health` | GET | 1.5 s | 4.5 s | 5.0 s | 5s per-provider timeout cap; expect 3-provider total |
|
||||
| `/v1/agents/credentials` | GET | 100 ms | 250 ms | 500 ms | Metadata only; values never returned |
|
||||
| `/v1/agents/tasks` (GET list) | GET | 150 ms | 400 ms | 800 ms | |
|
||||
| `/v1/agents/tasks` (POST create) | POST | 250 ms | 600 ms | 1.2 s | Just enqueues; doesn't run agent |
|
||||
| `/v1/agents/tasks/{id}` (GET) | GET | 100 ms | 300 ms | 600 ms | |
|
||||
| `/v1/agents/tasks/{id}` (DELETE) | DELETE | 150 ms | 400 ms | 800 ms | |
|
||||
|
||||
### 2.4 Combos / me / providers
|
||||
|
||||
| Endpoint | Method | p50 | p95 | p99 |
|
||||
|---|---|---|---|---|
|
||||
| `/v1/combos` | GET | 80 ms | 200 ms | 400 ms |
|
||||
| `/v1/me/status` | GET | 60 ms | 150 ms | 300 ms |
|
||||
| `/v1/providers/{provider}/models` | GET | 100 ms | 250 ms | 500 ms |
|
||||
|
||||
### 2.5 Web / search
|
||||
|
||||
| Endpoint | Method | p50 | p95 | p99 | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| `/v1/web/fetch` | POST | 1.5 s | 4.0 s | 8.0 s | 10s timeout cap; recurse depth 3 |
|
||||
| `/v1/search` | POST | 800 ms | 2.0 s | 4.0 s | Provider search latency varies |
|
||||
|
||||
### 2.6 VSCode-CLI shim (token-scoped)
|
||||
|
||||
These are the legacy passthrough paths. Budgets are tighter because
|
||||
they're called frequently by the VSCode-CLI extension in tight loops.
|
||||
|
||||
| Endpoint | Method | p50 | p95 | p99 |
|
||||
|---|---|---|---|---|
|
||||
| `/v1/vscode/{token}/v1/chat/completions` | POST | 700 ms | 1.6 s | 3.0 s |
|
||||
| `/v1/vscode/{token}/v1/models` | GET | 60 ms | 150 ms | 300 ms |
|
||||
| `/v1/vscode/{token}/combos` | GET | 80 ms | 200 ms | 400 ms |
|
||||
| `/v1/vscode/{token}/chat/completions` (legacy) | POST | 700 ms | 1.6 s | 3.0 s |
|
||||
| `/v1/vscode/{token}/models` (legacy) | GET | 60 ms | 150 ms | 300 ms |
|
||||
| `/v1/vscode/{token}/responses` | POST | 800 ms | 1.8 s | 3.5 s |
|
||||
|
||||
### 2.7 Management / settings
|
||||
|
||||
Management endpoints are operator-only and not part of the hot path.
|
||||
Budgets are set conservatively; breaches don't page on-call but do
|
||||
flag in the weekly perf review.
|
||||
|
||||
| Endpoint group | p50 | p95 | p99 |
|
||||
|---|---|---|---|
|
||||
| `/api/settings/*` (GET) | 100 ms | 300 ms | 600 ms |
|
||||
| `/api/settings/*` (POST/PATCH/DELETE) | 200 ms | 500 ms | 1.0 s |
|
||||
| `/api/keys/*` (CRUD) | 150 ms | 400 ms | 800 ms |
|
||||
| `/api/quota/*` (CRUD) | 150 ms | 400 ms | 800 ms |
|
||||
| `/api/monitoring/health` (heavy) | 500 ms | 1.5 s | 3.0 s |
|
||||
|
||||
### 2.8 Public probes
|
||||
|
||||
| Endpoint | Method | p50 | p95 | p99 |
|
||||
|---|---|---|---|---|
|
||||
| `/api/health/ping` | GET | 5 ms | 20 ms | 50 ms |
|
||||
| `/api/system/version` | GET | 5 ms | 20 ms | 50 ms |
|
||||
| `/api/docs` | GET | 20 ms | 80 ms | 200 ms (HTML shell, no provider call) |
|
||||
|
||||
---
|
||||
|
||||
## 3. Throughput targets
|
||||
|
||||
| Tier | Per-replica RPS | Cluster RPS (3 replicas) | Notes |
|
||||
|---|---|---|---|
|
||||
| Inference (non-stream) | 50 RPS | 150 RPS | Bounded by provider quota + translator CPU |
|
||||
| Inference (stream) | 25 concurrent streams | 75 streams | Bounded by Node event-loop + memory |
|
||||
| Embeddings | 200 RPS | 600 RPS | Cheap |
|
||||
| Files (upload) | 10 RPS | 30 RPS | Multipart parse + DB write |
|
||||
| Files (download) | 100 RPS | 300 RPS | Static-content via Next.js |
|
||||
| Combos / me / providers | 500 RPS | 1,500 RPS | Cached |
|
||||
| WebSocket | 100 concurrent connections | 300 | Per-IP cap 5 |
|
||||
|
||||
**Cluster ceiling** (all endpoints combined, sustained): ~1,000 RPS
|
||||
before p95 latency begins to climb. Scale horizontally beyond that
|
||||
by adding replicas; the Caddy LB is stateless.
|
||||
|
||||
---
|
||||
|
||||
## 4. Resource budgets
|
||||
|
||||
| Resource | Per-replica cap | Notes |
|
||||
|---|---|---|
|
||||
| RSS memory | 1.5 GB | Spikes during audio/video gen; expect brief 2 GB |
|
||||
| Event-loop lag (p99) | 50 ms | Alert via `clinic doctor` regression |
|
||||
| Heap retained | 800 MB | Old-gen GC tuning in `node --max-old-space-size` |
|
||||
| File descriptors | 2,000 | `ulimit -n 4096` recommended at host |
|
||||
| DB connections (sql.js) | 1 per replica | sql.js is in-process; no pool needed |
|
||||
| Redis connections | 20 per replica | Pooled; idle reaped at 5 min |
|
||||
|
||||
---
|
||||
|
||||
## 5. Cold-start budget
|
||||
|
||||
Next.js App Router cold-start on a fresh container:
|
||||
|
||||
| Phase | Budget |
|
||||
|---|---|
|
||||
| Container start → HTTP listening | ≤ 800 ms |
|
||||
| First request TTFB (warm) | ≤ 200 ms |
|
||||
| Translator registry bootstrap | ≤ 500 ms (one-time, first /v1/responses) |
|
||||
|
||||
**Measurement script**: `bin/cold-start-bench.sh` (not yet committed;
|
||||
`bin/` is the canonical scripts dir).
|
||||
|
||||
---
|
||||
|
||||
## 6. Regression gate (k6 reference)
|
||||
|
||||
The `benches/perf-gate.k6.js` script asserts the SLOs above. Not yet
|
||||
wired into CI; the JSON output is the contract for the next infra PR.
|
||||
|
||||
```javascript
|
||||
// benches/perf-gate.k6.js — pseudo-code; not yet committed
|
||||
import http from 'k6/http';
|
||||
import { check, Trend } from 'k6';
|
||||
|
||||
const responsesTTFB = new Trend('v1_responses_ttfb', true);
|
||||
|
||||
export const options = {
|
||||
scenarios: {
|
||||
smoke: {
|
||||
executor: 'constant-vus',
|
||||
vus: 10,
|
||||
duration: '1m',
|
||||
},
|
||||
},
|
||||
thresholds: {
|
||||
'http_req_duration{endpoint:v1_responses}': ['p(95)<1800', 'p(99)<3500'],
|
||||
'http_req_failed': ['rate<0.01'],
|
||||
'v1_responses_ttfb': ['p(95)<900'],
|
||||
},
|
||||
};
|
||||
|
||||
export default function () {
|
||||
const res = http.post(`${__ENV.BASE_URL}/api/v1/responses`, JSON.stringify({
|
||||
model: 'gpt-4o-mini',
|
||||
input: 'ping',
|
||||
}), { headers: { 'Authorization': `Bearer ${__ENV.API_KEY}` }});
|
||||
check(res, { 'status is 200': (r) => r.status === 200 });
|
||||
responsesTTFB.add(res.timings.waiting);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Review log
|
||||
|
||||
| Date | Reviewer | Change |
|
||||
|---|---|---|
|
||||
| 2026-06-18 | security-circle lead | Initial per-endpoint budgets derived from 3-replica Caddy + Redis topology |
|
||||
| 2026-07-18 (planned) | observability-circle | Wire `benches/perf-gate.k6.js` into CI; gate on p95 + p99 breach |
|
||||
| 2026-09-18 (planned) | observability-circle | Quarterly review; adjust after real-traffic baseline data |
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "OmniRoute Documentation"
|
||||
version: 3.8.24
|
||||
lastUpdated: 2026-06-13
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# OmniRoute Documentation
|
||||
@@ -12,16 +12,6 @@ Navigable index of the OmniRoute documentation set. Topics are grouped by intent
|
||||
|
||||
---
|
||||
|
||||
## architecture/
|
||||
|
||||
How the system is put together — read these to understand the runtime, code layout, and resilience model.
|
||||
|
||||
- [ARCHITECTURE.md](architecture/ARCHITECTURE.md) — high-level system architecture (request pipeline, layers, modules).
|
||||
- [CODEBASE_DOCUMENTATION.md](architecture/CODEBASE_DOCUMENTATION.md) — engineering reference for the codebase.
|
||||
- [REPOSITORY_MAP.md](architecture/REPOSITORY_MAP.md) — directory-by-directory navigation guide.
|
||||
- [AUTHZ_GUIDE.md](architecture/AUTHZ_GUIDE.md) — authorization pipeline (route classifier + policy engine).
|
||||
- [RESILIENCE_GUIDE.md](architecture/RESILIENCE_GUIDE.md) — provider circuit breaker, connection cooldown, and model lockout.
|
||||
|
||||
## For Non-Tech Users
|
||||
|
||||
Simple guides for using OmniRoute — no technical background needed.
|
||||
@@ -38,26 +28,52 @@ Simple guides for using OmniRoute — no technical background needed.
|
||||
|
||||
- [SETUP_GUIDE.md](guides/SETUP_GUIDE.md) — first-time setup of OmniRoute.
|
||||
- [USER_GUIDE.md](guides/USER_GUIDE.md) — daily usage of the dashboard and API.
|
||||
- [FEATURES.md](guides/FEATURES.md) — dashboard feature gallery.
|
||||
- [TIERS.md](guides/TIERS.md) — OmniRoute tiers explained (user guide).
|
||||
- [USAGE_QUOTA_GUIDE.md](guides/USAGE_QUOTA_GUIDE.md) — usage, quota & spend tracking.
|
||||
- [COST_TRACKING.md](guides/COST_TRACKING.md) — cost and spend tracking.
|
||||
- [FREE_PROVIDER_RANKINGS.md](guides/FREE_PROVIDER_RANKINGS.md) — free provider rankings (Arena ELO).
|
||||
- [DOCKER_GUIDE.md](guides/DOCKER_GUIDE.md) — running OmniRoute under Docker.
|
||||
- [ELECTRON_GUIDE.md](guides/ELECTRON_GUIDE.md) — desktop (Electron) builds.
|
||||
- [TERMUX_GUIDE.md](guides/TERMUX_GUIDE.md) — running on Android via Termux.
|
||||
- [PWA_GUIDE.md](guides/PWA_GUIDE.md) — installing the dashboard as a PWA.
|
||||
- [UNINSTALL.md](guides/UNINSTALL.md) — clean removal steps.
|
||||
- [REMOTE-MODE.md](guides/REMOTE-MODE.md) — exposing OmniRoute remotely + scoped tokens.
|
||||
- [CLI-INTEGRATIONS.md](guides/CLI-INTEGRATIONS.md) — master table of `setup-*` CLI integrations.
|
||||
- [CLAUDE-CODE-CONFIGURATION.md](guides/CLAUDE-CODE-CONFIGURATION.md) — Claude Code CLI with OmniRoute.
|
||||
- [CODEX-CLI-CONFIGURATION.md](guides/CODEX-CLI-CONFIGURATION.md) — Codex CLI with OmniRoute.
|
||||
- [KIRO_SETUP.md](guides/KIRO_SETUP.md) — Kiro setup.
|
||||
- [I18N.md](guides/I18N.md) — translation and locale workflow.
|
||||
- [FEATURES.md](guides/FEATURES.md) — dashboard feature gallery.
|
||||
- [TROUBLESHOOTING.md](guides/TROUBLESHOOTING.md) — detailed troubleshooting reference.
|
||||
- [UNINSTALL.md](guides/UNINSTALL.md) — clean removal steps.
|
||||
|
||||
---
|
||||
|
||||
## For Tech Users
|
||||
|
||||
Technical documentation for developers and contributors.
|
||||
|
||||
## architecture/
|
||||
|
||||
How the system is put together — read these to understand the runtime, code layout, and resilience model.
|
||||
|
||||
- [ARCHITECTURE.md](architecture/ARCHITECTURE.md) — high-level system architecture (request pipeline, layers, modules).
|
||||
- [CODEBASE_DOCUMENTATION.md](architecture/CODEBASE_DOCUMENTATION.md) — engineering reference for the codebase.
|
||||
- [REPOSITORY_MAP.md](architecture/REPOSITORY_MAP.md) — directory-by-directory navigation guide.
|
||||
- [AUTHZ_GUIDE.md](architecture/AUTHZ_GUIDE.md) — authorization pipeline (route classifier + policy engine).
|
||||
- [RESILIENCE_GUIDE.md](architecture/RESILIENCE_GUIDE.md) — provider circuit breaker, connection cooldown, and model lockout.
|
||||
- [QUALITY_GATES.md](architecture/QUALITY_GATES.md) — quality-gate scripts and CI jobs inventory.
|
||||
- [MONITORING_SECTIONS.md](architecture/MONITORING_SECTIONS.md) — monitoring/costs dashboard navigation.
|
||||
- [cluster-decisions.md](architecture/cluster-decisions.md) — optional sidecar/cluster profile decisions.
|
||||
|
||||
## reference/
|
||||
|
||||
Lookup material — API surface, environment variables, CLI flags, provider catalog.
|
||||
|
||||
- [API_REFERENCE.md](reference/API_REFERENCE.md) — REST API endpoints and shapes.
|
||||
- [PROVIDER_REFERENCE.md](reference/PROVIDER_REFERENCE.md) — auto-generated provider catalog.
|
||||
- [openapi.yaml](openapi.yaml) — OpenAPI 3.1 spec for the public API.
|
||||
- [PROVIDER_REFERENCE.md](reference/PROVIDER_REFERENCE.md) — auto-generated provider catalog (do not edit by hand).
|
||||
- [openapi.yaml](openapi.yaml) — OpenAPI spec for the public API.
|
||||
- [ENVIRONMENT.md](reference/ENVIRONMENT.md) — environment variables reference.
|
||||
- [FEATURE_FLAGS.md](reference/FEATURE_FLAGS.md) — feature flags and their defaults.
|
||||
- [CLI-TOOLS.md](reference/CLI-TOOLS.md) — bundled CLI commands.
|
||||
- [FREE_TIERS.md](reference/FREE_TIERS.md) — free-tier LLM provider directory.
|
||||
|
||||
@@ -67,19 +83,35 @@ Pluggable subsystems exposed to clients, agents, and operators.
|
||||
|
||||
- [MCP-SERVER.md](frameworks/MCP-SERVER.md) — Model Context Protocol server.
|
||||
- [A2A-SERVER.md](frameworks/A2A-SERVER.md) — Agent-to-Agent (A2A) JSON-RPC server.
|
||||
- [ACP.md](frameworks/ACP.md) — Agent Client Protocol.
|
||||
- [AGENT_PROTOCOLS_GUIDE.md](frameworks/AGENT_PROTOCOLS_GUIDE.md) — A2A / ACP / Cloud agent overview.
|
||||
- [AGENTBRIDGE.md](frameworks/AGENTBRIDGE.md) — IDE agent bridge.
|
||||
- [AGENT-SKILLS.md](frameworks/AGENT-SKILLS.md) — agent skills catalog.
|
||||
- [CLOUD_AGENT.md](frameworks/CLOUD_AGENT.md) — cloud agent runtime and providers.
|
||||
- [SKILLS.md](frameworks/SKILLS.md) — Skills framework (sandboxed extension).
|
||||
- [MEMORY.md](frameworks/MEMORY.md) — persistent memory (FTS5 + Qdrant).
|
||||
- [WEBHOOKS.md](frameworks/WEBHOOKS.md) — webhook events and dispatch.
|
||||
- [EVALS.md](frameworks/EVALS.md) — eval suites.
|
||||
- [GAMIFICATION.md](frameworks/GAMIFICATION.md) — gamification & leaderboard system.
|
||||
- [EMBEDDED-SERVICES.md](frameworks/EMBEDDED-SERVICES.md) — embedded sidecar services (9Router, CLIProxyAPI).
|
||||
- [NOTION_CONTEXT.md](frameworks/NOTION_CONTEXT.md) — Notion context source.
|
||||
- [OBSIDIAN_CONTEXT.md](frameworks/OBSIDIAN_CONTEXT.md) — Obsidian context source.
|
||||
- [OPENCODE.md](frameworks/OPENCODE.md) — OpenCode integration.
|
||||
- [OPEN_SSE_ARCHITECTURE.md](frameworks/OPEN_SSE_ARCHITECTURE.md) — open-sse streaming engine internals.
|
||||
- [PLAYGROUND_STUDIO.md](frameworks/PLAYGROUND_STUDIO.md) — Playground Studio UI.
|
||||
- [SEARCH_TOOLS_STUDIO.md](frameworks/SEARCH_TOOLS_STUDIO.md) — Search Tools Studio UI.
|
||||
- [TRAFFIC_INSPECTOR.md](frameworks/TRAFFIC_INSPECTOR.md) — traffic inspector (MITM).
|
||||
- [PLUGINS.md](frameworks/PLUGINS.md) — CLI plugin system overview.
|
||||
- [PLUGIN_SDK.md](frameworks/PLUGIN_SDK.md) — plugin SDK reference.
|
||||
- [PLUGIN_MARKETPLACE.md](frameworks/PLUGIN_MARKETPLACE.md) — plugin marketplace.
|
||||
|
||||
## routing/
|
||||
|
||||
Combo routing, scoring, and replay.
|
||||
|
||||
- [AUTO-COMBO.md](routing/AUTO-COMBO.md) — Auto-Combo (9-factor scoring, 17 strategies).
|
||||
- [REASONING_REPLAY.md](routing/REASONING_REPLAY.md) — reasoning replay flow.
|
||||
- [AUTO-COMBO.md](routing/AUTO-COMBO.md) — Auto-Combo (multi-factor scoring, 17 strategies).
|
||||
- [QUOTA_SHARE.md](routing/QUOTA_SHARE.md) — quota sharing engine.
|
||||
- [REASONING_REPLAY.md](routing/REASONING_REPLAY.md) — reasoning replay cache.
|
||||
|
||||
## security/
|
||||
|
||||
@@ -90,6 +122,12 @@ Guardrails, compliance, stealth, and the mandatory patterns for handling public
|
||||
- [STEALTH_GUIDE.md](security/STEALTH_GUIDE.md) — TLS / fingerprint stealth.
|
||||
- [PUBLIC_CREDS.md](security/PUBLIC_CREDS.md) — **mandatory** pattern for embedding public upstream OAuth client_id/secret + Firebase Web keys without tripping secret scanners.
|
||||
- [ERROR_SANITIZATION.md](security/ERROR_SANITIZATION.md) — **mandatory** pattern for routing every error response through `sanitizeErrorMessage` to prevent stack-trace exposure.
|
||||
- [ROUTE_GUARD_TIERS.md](security/ROUTE_GUARD_TIERS.md) — route-guard classification tiers.
|
||||
- [CLI_TOKEN.md](security/CLI_TOKEN.md) — CLI machine-ID token (HMAC + legacy SHA-256) auth.
|
||||
- [EGRESS_POLICY.md](security/EGRESS_POLICY.md) — egress IP family (IPv4/IPv6) policy.
|
||||
- [MITM-TPROXY-DECRYPT.md](security/MITM-TPROXY-DECRYPT.md) — transparent MITM decrypt.
|
||||
- [SUPPLY_CHAIN.md](security/SUPPLY_CHAIN.md) — supply-chain gates (SLSA, SBOM, Trivy, osv-scanner, Scorecard).
|
||||
- [SOCKET_DEV_FINDINGS.md](security/SOCKET_DEV_FINDINGS.md) — supply-chain finding attestations.
|
||||
|
||||
## compression/
|
||||
|
||||
@@ -100,13 +138,33 @@ Prompt compression engines, rules, and language packs.
|
||||
- [COMPRESSION_RULES_FORMAT.md](compression/COMPRESSION_RULES_FORMAT.md) — rule file format.
|
||||
- [COMPRESSION_LANGUAGE_PACKS.md](compression/COMPRESSION_LANGUAGE_PACKS.md) — language packs.
|
||||
- [RTK_COMPRESSION.md](compression/RTK_COMPRESSION.md) — RTK engine deep dive.
|
||||
- [CONTEXT_EDITING.md](compression/CONTEXT_EDITING.md) — delegated context editing (Anthropic).
|
||||
- [EXTENDING_COMPRESSION.md](compression/EXTENDING_COMPRESSION.md) — adding a custom compression engine.
|
||||
|
||||
## providers/
|
||||
|
||||
Provider-specific integration guides.
|
||||
|
||||
- [CLAUDE_WEB.md](providers/CLAUDE_WEB.md) — Claude Web (cookie-auth) provider.
|
||||
- [AGENTROUTER.md](providers/AGENTROUTER.md) — AgentRouter setup.
|
||||
- [ZED-DOCKER.md](providers/ZED-DOCKER.md) — Zed IDE integration under Docker.
|
||||
|
||||
## comparison/
|
||||
|
||||
- [OMNIROUTE_VS_ALTERNATIVES.md](comparison/OMNIROUTE_VS_ALTERNATIVES.md) — how OmniRoute compares to alternatives.
|
||||
|
||||
## ops/
|
||||
|
||||
Release, deployment, proxies, tunnels, coverage.
|
||||
Release, deployment, proxies, tunnels, coverage, database, monitoring.
|
||||
|
||||
- [RELEASE_CHECKLIST.md](ops/RELEASE_CHECKLIST.md) — release flow checklist.
|
||||
- [RELEASE_GREEN.md](ops/RELEASE_GREEN.md) — keeping the PR queue and release branch green.
|
||||
- [QUALITY_GATE_PLAYBOOK.md](ops/QUALITY_GATE_PLAYBOOK.md) — quality-gate playbook.
|
||||
- [BRANCH_PROTECTION_MAIN.md](ops/BRANCH_PROTECTION_MAIN.md) — `main` branch protection.
|
||||
- [COVERAGE_PLAN.md](ops/COVERAGE_PLAN.md) — test coverage plan.
|
||||
- [DATABASE_GUIDE.md](ops/DATABASE_GUIDE.md) — DB schema and operations.
|
||||
- [SQLITE_RUNTIME.md](ops/SQLITE_RUNTIME.md) — SQLite driver resolution chain.
|
||||
- [MONITORING_GUIDE.md](ops/MONITORING_GUIDE.md) — monitoring & observability.
|
||||
- [FLY_IO_DEPLOYMENT_GUIDE.md](ops/FLY_IO_DEPLOYMENT_GUIDE.md) — Fly.io deployment.
|
||||
- [VM_DEPLOYMENT_GUIDE.md](ops/VM_DEPLOYMENT_GUIDE.md) — generic VM deployment.
|
||||
- [PROXY_GUIDE.md](ops/PROXY_GUIDE.md) — upstream proxy configuration.
|
||||
@@ -114,7 +172,7 @@ Release, deployment, proxies, tunnels, coverage.
|
||||
|
||||
## diagrams/
|
||||
|
||||
Mermaid sources and exported SVG/PNG diagrams referenced from the docs above. Populated incrementally — see [diagrams/README.md](diagrams/README.md).
|
||||
Mermaid sources and exported SVG/PNG diagrams referenced from the docs above. See [diagrams/README.md](diagrams/README.md).
|
||||
|
||||
## i18n/
|
||||
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
# Submitting a Pull Request
|
||||
|
||||
Step-by-step for contributors who already have a fork and a working fix.
|
||||
|
||||
---
|
||||
|
||||
## 1 — Set up your fork
|
||||
|
||||
```bash
|
||||
# Clone your fork
|
||||
git clone https://github.com/<your-username>/OmniRoute.git
|
||||
cd OmniRoute
|
||||
|
||||
# Add the upstream repo so you can sync
|
||||
git remote add upstream https://github.com/diegosouzapw/OmniRoute.git
|
||||
|
||||
# Install dependencies (.env is auto-created from .env.example)
|
||||
npm install
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2 — Sync with the current release branch
|
||||
|
||||
PRs go to the **current release branch** — `release/v3.8.24` at the time of writing. Always target the latest `release/v*` branch (the repository's active development branch), never `main`. Check the repo's branch list or `CONTRIBUTING.md` if a newer release cycle has opened.
|
||||
|
||||
```bash
|
||||
git fetch upstream
|
||||
git checkout -b fix/your-description upstream/release/v3.8.24
|
||||
```
|
||||
|
||||
If you already made your changes on another branch, rebase on top of it:
|
||||
|
||||
```bash
|
||||
git fetch upstream
|
||||
git rebase upstream/release/v3.8.24
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3 — Branch naming
|
||||
|
||||
| Prefix | Use for |
|
||||
| ----------- | --------------------------------------- |
|
||||
| `feat/` | new feature |
|
||||
| `fix/` | bug fix |
|
||||
| `refactor/` | code restructuring (no behavior change) |
|
||||
| `docs/` | documentation only |
|
||||
| `test/` | tests only |
|
||||
| `chore/` | tooling, deps, CI |
|
||||
|
||||
Examples: `fix/codex-token-refresh`, `feat/provider-xyz`, `docs/update-readme`
|
||||
|
||||
---
|
||||
|
||||
## 4 — Validate before committing
|
||||
|
||||
```bash
|
||||
npm run lint # must pass (0 errors)
|
||||
npm run typecheck:core # must pass
|
||||
npm run test:unit # must pass
|
||||
npm run test:coverage # coverage gate: 60/60/60/60 (statements/lines/functions/branches)
|
||||
```
|
||||
|
||||
If you changed production code in `src/`, `open-sse/`, `electron/`, or `bin/`, include or update tests in the same PR.
|
||||
|
||||
---
|
||||
|
||||
## 5 — Commit
|
||||
|
||||
Follow [Conventional Commits](https://www.conventionalcommits.org/):
|
||||
|
||||
```
|
||||
feat(dashboard): add provider search filter
|
||||
fix(combo): resolve pending request leak on timeout
|
||||
docs(readme): update installation steps
|
||||
test(auth): add JWT expiry edge case
|
||||
```
|
||||
|
||||
Common scopes: `api`, `dashboard`, `db`, `sse`, `oauth`, `providers`, `combo`, `mcp`, `cli`, `i18n`
|
||||
|
||||
---
|
||||
|
||||
## 6 — Push and open the PR
|
||||
|
||||
```bash
|
||||
git push -u origin fix/your-description
|
||||
```
|
||||
|
||||
Then open a PR on GitHub targeting **`diegosouzapw/OmniRoute`** → **`release/v3.8.24`**.
|
||||
|
||||
PR description checklist:
|
||||
|
||||
- [ ] What the change does (1–3 bullets)
|
||||
- [ ] How to test it
|
||||
- [ ] Test files added or updated (if production code changed)
|
||||
|
||||
---
|
||||
|
||||
## 7 — After opening the PR
|
||||
|
||||
- CI runs lint + typecheck + tests automatically.
|
||||
- Address review comments with new commits (do not force-push after review starts).
|
||||
- If the base branch advances, sync with:
|
||||
|
||||
```bash
|
||||
git fetch upstream
|
||||
git rebase upstream/release/v3.8.24
|
||||
git push --force-with-lease
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick reference
|
||||
|
||||
```bash
|
||||
# Full validation in one command
|
||||
npm run lint && npm run typecheck:core && npm run test:coverage
|
||||
|
||||
# Run a single test file
|
||||
node --import tsx/esm --test tests/unit/your-file.test.ts
|
||||
|
||||
# Start dev server
|
||||
npm run dev # http://localhost:20128
|
||||
```
|
||||
|
||||
For the full contributor guide see [CONTRIBUTING.md](../CONTRIBUTING.md).
|
||||
@@ -1,346 +0,0 @@
|
||||
# Threat Model — OmniRoute (2026-06-18)
|
||||
|
||||
**Status**: Authoritative. Supersedes any inline threat notes in code comments.
|
||||
**Methodology**: STRIDE (Spoofing, Tampering, Repudiation, Information
|
||||
Disclosure, Denial of Service, Elevation of Privilege) per endpoint.
|
||||
**Scope**: v1 client API + management surface + agent dispatch + relay.
|
||||
**Re-evaluation cadence**: every 90 days OR on any new public route
|
||||
addition / new auth tier change.
|
||||
**Owner**: security-circle lead.
|
||||
**Related**: `SECURITY.md` (disclosure policy), `authz-inventory` (live
|
||||
tier classification), `docs/openapi.yaml` (API surface), `docs/architecture/`
|
||||
(security boundaries).
|
||||
|
||||
---
|
||||
|
||||
## 1. Trust boundaries
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────┐
|
||||
│ Public Internet │
|
||||
│ └── TLS termination (Caddy / cloud LB) │
|
||||
│ │ │
|
||||
│ ▼ ◀── Trust boundary 1: edge ↔ application │
|
||||
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Next.js App Router (3 replicas behind Caddy LB) │ │
|
||||
│ │ ┌──────────────────────────────────────────────┐ │ │
|
||||
│ │ │ per-route authz guards (routeGuard.ts) │ │ │
|
||||
│ │ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌──────┐ │ │ │
|
||||
│ │ │ │ PUBLIC │ │CLIENT │ │MANAGE │ │ALWAYS│ │ │ │
|
||||
│ │ │ │ /health│ │ /v1/* │ │/settn │ │PROT │ │ │ │
|
||||
│ │ │ └────────┘ └────┬───┘ └────┬───┘ └──────┘ │ │ │
|
||||
│ │ └──────────────────┼─────────┼──────────────┘ │ │
|
||||
│ │ ▼ ▼ │ │
|
||||
│ │ ┌─────────────────────────┐ ┌──────────────────────────┐ │ │
|
||||
│ │ │ src/lib/sse/handlers │ │ src/lib/localDb │ │ │
|
||||
│ │ │ (translator + chat) │ │ (sql.js encrypted store)│ │ │
|
||||
│ │ └──────────┬──────────────┘ └────────────┬─────────────┘ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ ▼ ◀── Trust boundary 2: app ↔ upstream │ │
|
||||
│ └───────────────┼──────────────────────────────┼─────────────────┘ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌──────────────────────────┐ ┌────────────────────────────┐ │
|
||||
│ │ Provider APIs │ │ Filesystem (DB + secrets) │ │
|
||||
│ │ (OpenAI, Anthropic, …) │ │ + Redis (session/quota) │ │
|
||||
│ └──────────────────────────┘ └────────────────────────────┘ │
|
||||
└────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Boundary 1** (edge ↔ app): Mitigated by TLS at the edge + per-route auth gates
|
||||
(`src/server/authz/routeGuard.ts` + `extractApiKey`/`isValidApiKey` in each
|
||||
`route.ts`; there is no global Next.js middleware). Anything past this boundary is fully authenticated
|
||||
unless explicitly in the PUBLIC tier.
|
||||
|
||||
**Boundary 2** (app ↔ upstream): Mitigated by per-provider key storage
|
||||
in `src/lib/vault/` and the per-(token,IP) relay rate limit.
|
||||
|
||||
**Local-only boundary** (loopback-only routes): documented in
|
||||
`src/server/authz/routeGuard.ts` as `LOCAL_ONLY_API_PREFIXES`; known
|
||||
CVE class is "spawn capable route exposed to non-local traffic"
|
||||
(GHSA-fhh6-4qxv-rpqj).
|
||||
|
||||
---
|
||||
|
||||
## 2. STRIDE legend
|
||||
|
||||
| Letter | Threat | Question it answers |
|
||||
| ------ | ---------------------- | ----------------------------------------------------- |
|
||||
| **S** | Spoofing | Can an attacker pretend to be a legitimate principal? |
|
||||
| **T** | Tampering | Can an attacker modify data in transit or at rest? |
|
||||
| **R** | Repudiation | Can a principal deny an action they took? |
|
||||
| **I** | Information disclosure | Can an attacker read data they shouldn't? |
|
||||
| **D** | Denial of service | Can an attacker degrade or block service? |
|
||||
| **E** | Elevation of privilege | Can an attacker gain more access than intended? |
|
||||
|
||||
Severity scale: **L**ow (paper), **M**edium (real exploit possible), **H**igh
|
||||
(active exploitation likely or already seen in the wild).
|
||||
|
||||
---
|
||||
|
||||
## 3. Per-endpoint STRIDE analysis (top-20 highest-risk routes)
|
||||
|
||||
The 50 v1 route handlers are scored below. Top-20 highest-risk routes
|
||||
get a full STRIDE row; the remaining 30 are covered by a tier-level
|
||||
summary in § 4.
|
||||
|
||||
### 3.1 `/api/v1/responses` — POST (primary inference)
|
||||
|
||||
| STRIDE | Risk | Mitigation |
|
||||
| ------ | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **S** | M — Bearer key stolen → caller identity spoofed | Keys hashed (SHA-256) at rest in `src/lib/db/apiKeys.ts`; revocation list in `src/lib/db/apiKeys.ts#revokeKey` |
|
||||
| **T** | M — Request body modified in transit | TLS at edge; body re-validated against `ResponsesRequest` schema in `open-sse/handlers/responseSanitizer.ts` |
|
||||
| **R** | L — Caller denies a request | `src/lib/audit/` writes append-only row with `key_id`, `request_hash`, `model`, `tokens`; hash-chain integrity (DEBT-051 follow-up) |
|
||||
| **I** | M — Provider API key leaked via response | `open-sse/handlers/responseTranslator.ts` strips `x-api-key` headers before proxying; response redaction middleware |
|
||||
| **D** | H — High-cost model invocation = $$ DoS | Per-key rate limit (`open-sse/services/rateLimitManager.ts`); per-tenant quota pool (`src/lib/db/quotaPools.ts`); DEBT-001 TPM/TPD reservoir is **not** yet enforced |
|
||||
| **E** | L — Bearer scope escalation | API keys carry explicit `scopes` array; routes require specific scopes; `requireManagementAuth` enforces management tier |
|
||||
|
||||
### 3.2 `/api/v1/relay/chat/completions` — POST (serverless relay)
|
||||
|
||||
| STRIDE | Risk | Mitigation |
|
||||
| ------ | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **S** | M — Relay token theft | Tokens hashed with SHA-256 + per-token ID column; per-(token,IP) rate limit in `src/app/api/v1/relay/chat/completions/route.ts:38-58` |
|
||||
| **T** | M — Token rotation bypass | Token records have `revoked_at`; check happens before each request |
|
||||
| **R** | L — Forensic headers sanitized | `sanitizeForensicHeader` strips CR/LF; length cap 256; IP/UA never echoed back to client |
|
||||
| **I** | H — IP-bucket map leaks | In-memory only, per-instance, capped at 10,000 entries; not persisted; see DEBT-038 (in-memory state loss on restart) |
|
||||
| **D** | M — Per-(token,IP) abuse | `RELAY_IP_PER_MINUTE=30` default; attacker rotates IPs to spread, hits per-token wall |
|
||||
| **E** | M — Token scope creep | Tokens carry `allowedModels[]`; reject on model-not-in-list |
|
||||
|
||||
### 3.3 `/api/v1/embeddings` — POST
|
||||
|
||||
| STRIDE | Risk | Mitigation |
|
||||
| ------ | ----------------------------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| **S** | L — Same as 3.1 | Same auth + key model |
|
||||
| **T** | L — Inputs are vectors; tampered embedding not useful | Re-encode on receive; reject dimension mismatch |
|
||||
| **R** | L | `src/lib/audit/` row written |
|
||||
| **I** | M — Embedding vectors can leak training data | Reject requests with PII patterns (regex) before forwarding; DEBT-052 follow-up |
|
||||
| **D** | L — Embeddings cheaper than chat | Quota pool cap |
|
||||
| **E** | L | Bearer scope |
|
||||
|
||||
### 3.4 `/api/v1/rerank` — POST
|
||||
|
||||
| STRIDE | Risk | Mitigation |
|
||||
| ------ | ---------------------------------- | ------------------------------------------------------------------ |
|
||||
| **S** | L | Same auth |
|
||||
| **T** | L | |
|
||||
| **R** | L | |
|
||||
| **I** | M — Documents contain user content | Per-tenant key isolation; audit log captures doc hash, not content |
|
||||
| **D** | L | |
|
||||
| **E** | L | |
|
||||
|
||||
### 3.5 `/api/v1/moderations` — POST
|
||||
|
||||
| STRIDE | Risk | Mitigation |
|
||||
| ------ | ------------------------------------------------ | -------------------------------------------------------------------- |
|
||||
| **S** | L | |
|
||||
| **T** | L | |
|
||||
| **R** | L | |
|
||||
| **I** | H — Moderation inputs are sensitive user content | Encrypted in transit (TLS); not logged; flag results cached with TTL |
|
||||
| **D** | L | |
|
||||
| **E** | L | |
|
||||
|
||||
### 3.6 `/api/v1/audio/speech` — POST (TTS)
|
||||
|
||||
| STRIDE | Risk | Mitigation |
|
||||
| ------ | ----------------------------------- | ------------------------------------------ |
|
||||
| **S** | L | |
|
||||
| **T** | L | |
|
||||
| **R** | L | |
|
||||
| **I** | L — Audio is synthesised from input | Same as 3.1 |
|
||||
| **D** | M — Long TTS = bandwidth burn | `input` cap 4096 chars; per-key rate limit |
|
||||
| **E** | L | |
|
||||
|
||||
### 3.7 `/api/v1/audio/transcriptions` — POST (STT, multipart)
|
||||
|
||||
| STRIDE | Risk | Mitigation |
|
||||
| ------ | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **S** | L | |
|
||||
| **T** | M — Multipart body tampered | `multipart-parser` validates Content-Length + boundary; size cap 25 MB |
|
||||
| **R** | L | |
|
||||
| **I** | H — Audio contains user PII / secrets | TTL cache; never written to disk by default; redaction of credit-card-like sequences in `open-sse/executors/chatgpt-web.ts` (per-file TODO, see DEBT-019) |
|
||||
| **D** | M — Large audio = bandwidth + provider cost | 25 MB cap; rate limit |
|
||||
| **E** | L | |
|
||||
|
||||
### 3.8 `/api/v1/images/generations` — POST
|
||||
|
||||
| STRIDE | Risk | Mitigation |
|
||||
| ------ | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **S** | L | |
|
||||
| **T** | L | |
|
||||
| **R** | L | |
|
||||
| **I** | L — Generated images are public URLs | URLs are signed with TTL; provider-controlled CDN |
|
||||
| **D** | M — Image gen is $expensive | Per-key USD spend limits enforced in the request pipeline (`src/lib/db/tokenLimits.ts`, surfaced via `src/app/api/usage/budget/route.ts`) |
|
||||
| **E** | L | |
|
||||
|
||||
### 3.9 `/api/v1/videos/generations` — POST (async)
|
||||
|
||||
| STRIDE | Risk | Mitigation |
|
||||
| ------ | --------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
||||
| **S** | L | |
|
||||
| **T** | L | |
|
||||
| **R** | M — Long-running async job, hard to attribute | Job records include `key_id`, `request_id`, `created_at`; result includes signed URL with TTL |
|
||||
| **I** | M — Generated video may contain PII | Same as 3.8 |
|
||||
| **D** | M — Video gen is very $expensive | Cost-budget middleware; max 10-second durations |
|
||||
| **E** | L | |
|
||||
|
||||
### 3.10 `/api/v1/files` — POST (multipart upload)
|
||||
|
||||
| STRIDE | Risk | Mitigation |
|
||||
| ------ | ----------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| **S** | M — Uploaded file attributed to wrong key | `key_id` captured at upload; immutable record |
|
||||
| **T** | H — Malicious file (zip bomb, polyglot) | Size cap 25 MB; MIME sniff + magic-byte check; AV scan is **not** performed (DEBT-053 follow-up) |
|
||||
| **R** | L | |
|
||||
| **I** | H — Files contain user PII | Encrypted at rest in `src/lib/vault/`; per-tenant access control |
|
||||
| **D** | M — Large uploads | Size cap + rate limit |
|
||||
| **E** | L | |
|
||||
|
||||
### 3.11 `/api/v1/files/{id}/content` — GET
|
||||
|
||||
| STRIDE | Risk | Mitigation |
|
||||
| ------ | ----------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| **S** | H — IDOR (file belonging to other tenant) | Per-tenant scope check; `requireTenantScope` middleware (added in v3.8.25) |
|
||||
| **T** | L | |
|
||||
| **R** | L | |
|
||||
| **I** | H — Tenant isolation breach = data leak | Per-tenant prefix in file storage; cross-tenant read attempt logged + 403 |
|
||||
| **D** | L | |
|
||||
| **E** | L | |
|
||||
|
||||
### 3.12 `/api/v1/batches` — POST (create batch)
|
||||
|
||||
| STRIDE | Risk | Mitigation |
|
||||
| ------ | ------------------------------------------ | -------------------------------------------------------- |
|
||||
| **S** | L | |
|
||||
| **T** | M — Batch input file modified after upload | Content hash stored at upload; mismatch on read = reject |
|
||||
| **R** | L | |
|
||||
| **I** | M — Input file contains PII | Same as 3.10 |
|
||||
| **D** | M — Large batch | 50k row cap per batch |
|
||||
| **E** | L | |
|
||||
|
||||
### 3.13 `/api/v1/batches/delete-completed` — POST
|
||||
|
||||
| STRIDE | Risk | Mitigation |
|
||||
| ------ | --------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| **S** | L | |
|
||||
| **T** | H — Mass delete | Requires manage-scope auth; `requireManagementAuth` enforced; audit log captures `key_id` |
|
||||
| **R** | L | |
|
||||
| **I** | M — Output files may leak | Soft-delete with 7-day retention before GC |
|
||||
| **D** | L | |
|
||||
| **E** | M — A misconfigured client could trigger this | Auth gate prevents; no anonymous access |
|
||||
|
||||
### 3.14 `/api/v1/agents/tasks` — POST (cloud-agent dispatch)
|
||||
|
||||
| STRIDE | Risk | Mitigation |
|
||||
| ------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **S** | L | |
|
||||
| **T** | H — Prompt injection in task `prompt` | `withInjectionGuard` middleware (per `src/app/api/v1/responses/route.ts:6`); pattern blocklist + sandboxed tool execution |
|
||||
| **R** | M — Long-running agent task, attribution | Task ID + `key_id` + `created_at` recorded; agent result signed by provider |
|
||||
| **I** | H — Agent reads user repo / credentials | Provider-scoped credentials; least-privilege OAuth; agent result sandboxed before return |
|
||||
| **D** | M — Long-running agent = $$ burn | 10-minute default timeout; configurable `timeoutSeconds` |
|
||||
| **E** | M — Agent tool calls could exceed scope | Tool allowlist per provider; rejection in `open-sse/executors/` |
|
||||
|
||||
### 3.15 `/api/v1/agents/credentials` — GET (list stored creds)
|
||||
|
||||
| STRIDE | Risk | Mitigation |
|
||||
| ------ | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| **S** | L | |
|
||||
| **T** | L | |
|
||||
| **R** | L | |
|
||||
| **I** | H — Leaked credential metadata enables targeted phishing | Returns only `provider`, `hasValue`, `updatedAt` (NEVER the value); requires manage-scope auth |
|
||||
| **D** | L | |
|
||||
| **E** | L | |
|
||||
|
||||
### 3.16 `/api/v1/me/status` — GET (caller introspection)
|
||||
|
||||
| STRIDE | Risk | Mitigation |
|
||||
| ------ | ---------------------------------------------------- | ---------------------------------------------- |
|
||||
| **S** | L | |
|
||||
| **T** | L | |
|
||||
| **R** | L | |
|
||||
| **I** | M — Reveals quota usage; could enable timing attacks | Rate-limited; per-key only; no cross-key reads |
|
||||
| **D** | L | |
|
||||
| **E** | L | |
|
||||
|
||||
### 3.17 `/api/v1/providers/{provider}/models` — GET
|
||||
|
||||
| STRIDE | Risk | Mitigation |
|
||||
| ------ | ------------------------------ | ------------------------------------- |
|
||||
| **S** | L | |
|
||||
| **T** | L | |
|
||||
| **R** | L | |
|
||||
| **I** | M — Reveals provider allowlist | Required for client UX; per-key scope |
|
||||
| **D** | L | |
|
||||
| **E** | L | |
|
||||
|
||||
### 3.18 `/api/v1/web/fetch` — POST (server-side fetch)
|
||||
|
||||
| STRIDE | Risk | Mitigation |
|
||||
| ------ | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| **S** | L | |
|
||||
| **T** | L | |
|
||||
| **R** | L | |
|
||||
| **I** | H — Fetched content could be exfiltrated via prompt injection | Content goes through `withInjectionGuard`; explicit extract modes; URL allowlist (default-deny, see DEBT-054) |
|
||||
| **D** | M — Recursive fetch = bandwidth burn | Max redirects 3; max 5 MB response; timeout 10s |
|
||||
| **E** | L | |
|
||||
|
||||
### 3.19 `/api/v1/vscode/{token}/v1/chat/completions` — POST (VSCode shim)
|
||||
|
||||
| STRIDE | Risk | Mitigation |
|
||||
| ------ | ------------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| **S** | M — VSCode token theft = long-lived impersonation | Tokens are 256-bit, single-purpose, scoped to VSCode-CLI; revokable from `/api/keys/{id}` |
|
||||
| **T** | L | |
|
||||
| **R** | L | |
|
||||
| **I** | M — Token in URL = logged in access logs | Token treated as secret; logs hash + last-4 only |
|
||||
| **D** | M — Per-token rate limit (lower than API key) | `VSCODECLI_RATE_LIMIT=10` req/min default |
|
||||
| **E** | L | |
|
||||
|
||||
### 3.20 `/api/v1/ws` — WebSocket
|
||||
|
||||
| STRIDE | Risk | Mitigation |
|
||||
| ------ | ----------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
||||
| **S** | M — WS upgrade lacks standard Bearer header | Subprotocol-based auth: `Sec-WebSocket-Protocol: bearer, <token>`; per-connection scope check |
|
||||
| **T** | M — WS message frames tampered | TLS-only (`wss://`); reject `ws://` in prod |
|
||||
| **R** | L | Connection ID + auth principal recorded at handshake |
|
||||
| **I** | H — Long-lived connection = larger blast radius | 1-hour max connection; heartbeat every 30s; auto-disconnect on auth revoke |
|
||||
| **D** | H — Many WS = connection table exhaustion | Per-IP WS cap 5; per-key cap 20; rate limit per message |
|
||||
| **E** | L | |
|
||||
|
||||
---
|
||||
|
||||
## 4. Tier-level summary (remaining 30 routes)
|
||||
|
||||
Routes in the same tier inherit the same baseline mitigations. Per-route
|
||||
deltas are documented in `docs/openapi.yaml` (request schema constraints).
|
||||
|
||||
| Tier | Count | Baseline mitigations | Residual risks |
|
||||
| ----------------------------------------------------------------------------------------------------------------------------------- | ----- | ---------------------------------------------------------- | ------------------------------------ |
|
||||
| PUBLIC (`/api/health`, `/api/monitoring/health`, `/_next/*`) | 3 | No auth; rate limit; static response | DoS via flooding (mitigated at LB) |
|
||||
| CLIENT_API inference (`/v1/audio`, `/v1/moderations`, `/v1/rerank`, `/v1/search`, `/v1/embeddings`, `/v1/responses`, `/v1/relay/*`) | 8 | Bearer auth + per-key rate limit + audit log + cost budget | TPM/TPD fairness (DEBT-001) |
|
||||
| CLIENT_API files (`/v1/files`, `/v1/files/{id}`, `/v1/batches/*`) | 5 | Bearer auth + per-tenant prefix + AV scan absent | DEBT-053 (AV) |
|
||||
| CLIENT_API combos/me/providers | 3 | Bearer auth + cached | None material |
|
||||
| CLIENT_API vscode shim | 13 | Token-scoped + per-token rate limit | Token in URL log noise |
|
||||
| MANAGEMENT (`/api/settings/*`, `/api/keys/*`, `/api/quota/*`, `/api/usage/*`) | ~60 | Manage-scope + dashboard session + 2FA suggested | See SECURITY.md for P0 items |
|
||||
| ALWAYS_PROTECTED (shutdown, DB settings) | ~5 | Auth + re-auth for security-impacting changes | None material |
|
||||
| LOCAL_ONLY (loopback spawn routes) | ~8 | Loopback-only + bypass-constant gated by manage-scope | GHSA-fhh6-4qxv-rpqj class if exposed |
|
||||
|
||||
---
|
||||
|
||||
## 5. Mitigations to add (next quarter)
|
||||
|
||||
| ID | Mitigation | Pillar target | Effort |
|
||||
| ----- | ---------------------------------------------------------------- | ------------- | ------ |
|
||||
| TM-01 | DEBT-001: TPM/TPD token-bucket fairness for relay + inference | D, E (cost) | M |
|
||||
| TM-02 | DEBT-051: Hash-chain integrity for `src/lib/audit/` rows | R | M |
|
||||
| TM-03 | DEBT-053: AV scan on `/v1/files` upload (ClamAV or external API) | T, I | L |
|
||||
| TM-04 | DEBT-054: URL allowlist for `/v1/web/fetch` | I, D | M |
|
||||
| TM-05 | OIDC/SAML SSO for management tier (audit L46) | S | L |
|
||||
| TM-06 | TOTP MFA for manage-scope users (audit L49) | S, E | M |
|
||||
| TM-07 | Field-level encryption for PII columns (audit L47) | I | L |
|
||||
|
||||
---
|
||||
|
||||
## 6. Review log
|
||||
|
||||
| Date | Reviewer | Change |
|
||||
| -------------------- | ------------------------- | ---------------------------------------------------------------------- |
|
||||
| 2026-06-18 | @KooshaPari/core (L5-118) | Initial STRIDE per endpoint, top-20 highest-risk routes + tier summary |
|
||||
| 2026-06-25 (planned) | security-circle | Re-score after DEBT-001, DEBT-051 close-outs |
|
||||
| 2026-09-18 (planned) | security-circle | Quarterly full re-review; refresh mitigations list |
|
||||
@@ -1,14 +1,14 @@
|
||||
---
|
||||
title: "OmniRoute Architecture"
|
||||
version: 3.8.2
|
||||
lastUpdated: 2026-05-13
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# OmniRoute Architecture
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](./ARCHITECTURE.md) | 🇧🇷 [Português (Brasil)](../i18n/pt-BR/docs/architecture/ARCHITECTURE.md) | 🇪🇸 [Español](../i18n/es/docs/architecture/ARCHITECTURE.md) | 🇫🇷 [Français](../i18n/fr/docs/architecture/ARCHITECTURE.md) | 🇮🇹 [Italiano](../i18n/it/docs/architecture/ARCHITECTURE.md) | 🇷🇺 [Русский](../i18n/ru/docs/architecture/ARCHITECTURE.md) | 🇨🇳 [中文 (简体)](../i18n/zh-CN/docs/architecture/ARCHITECTURE.md) | 🇩🇪 [Deutsch](../i18n/de/docs/architecture/ARCHITECTURE.md) | 🇮🇳 [हिन्दी](../i18n/in/docs/architecture/ARCHITECTURE.md) | 🇹🇭 [ไทย](../i18n/th/docs/architecture/ARCHITECTURE.md) | 🇺🇦 [Українська](../i18n/uk-UA/docs/architecture/ARCHITECTURE.md) | 🇸🇦 [العربية](../i18n/ar/docs/architecture/ARCHITECTURE.md) | 🇯🇵 [日本語](../i18n/ja/docs/architecture/ARCHITECTURE.md) | 🇻🇳 [Tiếng Việt](../i18n/vi/docs/architecture/ARCHITECTURE.md) | 🇧🇬 [Български](../i18n/bg/docs/architecture/ARCHITECTURE.md) | 🇩🇰 [Dansk](../i18n/da/docs/architecture/ARCHITECTURE.md) | 🇫🇮 [Suomi](../i18n/fi/docs/architecture/ARCHITECTURE.md) | 🇮🇱 [עברית](../i18n/he/docs/architecture/ARCHITECTURE.md) | 🇭🇺 [Magyar](../i18n/hu/docs/architecture/ARCHITECTURE.md) | 🇮🇩 [Bahasa Indonesia](../i18n/id/docs/architecture/ARCHITECTURE.md) | 🇰🇷 [한국어](../i18n/ko/docs/architecture/ARCHITECTURE.md) | 🇲🇾 [Bahasa Melayu](../i18n/ms/docs/architecture/ARCHITECTURE.md) | 🇳🇱 [Nederlands](../i18n/nl/docs/architecture/ARCHITECTURE.md) | 🇳🇴 [Norsk](../i18n/no/docs/architecture/ARCHITECTURE.md) | 🇵🇹 [Português (Portugal)](../i18n/pt/docs/architecture/ARCHITECTURE.md) | 🇷🇴 [Română](../i18n/ro/docs/architecture/ARCHITECTURE.md) | 🇵🇱 [Polski](../i18n/pl/docs/architecture/ARCHITECTURE.md) | 🇸🇰 [Slovenčina](../i18n/sk/docs/architecture/ARCHITECTURE.md) | 🇸🇪 [Svenska](../i18n/sv/docs/architecture/ARCHITECTURE.md) | 🇵🇭 [Filipino](../i18n/phi/docs/architecture/ARCHITECTURE.md) | 🇨🇿 [Čeština](../i18n/cs/docs/architecture/ARCHITECTURE.md)
|
||||
|
||||
_Last updated: 2026-05-13_
|
||||
_Last updated: 2026-06-28_
|
||||
|
||||
## Executive Summary
|
||||
|
||||
@@ -17,13 +17,13 @@ It provides a single OpenAI-compatible endpoint (`/v1/*`) and routes traffic acr
|
||||
|
||||
Core capabilities:
|
||||
|
||||
- OpenAI-compatible API surface for CLI/tools (226 providers, 60 executors)
|
||||
- OpenAI-compatible API surface for CLI/tools (237 providers, 68 executors)
|
||||
- Request/response translation across provider formats
|
||||
- Model combo fallback (multi-model sequence)
|
||||
- Structured combo steps (`provider + model + connection`) with runtime ordering by `compositeTiers`
|
||||
- Account-level fallback (multi-account per provider)
|
||||
- Quota preflight and quota-aware P2C account selection in the main chat path
|
||||
- OAuth + API-key provider connection management (16 OAuth modules)
|
||||
- OAuth + API-key provider connection management (17 OAuth provider modules)
|
||||
- Embedding generation via `/v1/embeddings` (6 providers, 9 models)
|
||||
- Image generation via `/v1/images/generations` (10+ providers, 20+ models)
|
||||
- Audio transcription via `/v1/audio/transcriptions` (7 providers)
|
||||
@@ -912,7 +912,6 @@ Each provider has a specialized executor extending `BaseExecutor` (in `open-sse/
|
||||
| `CommandCodeExecutor` | Command Code | OAuth + per-session header rotation |
|
||||
| `CursorExecutor` | Cursor IDE | ConnectRPC protocol, Protobuf encoding, request signing via checksum |
|
||||
| `DevinCliExecutor` | Devin CLI | Devin task lifecycle bridging via cloud agent module |
|
||||
| `GeminiCLIExecutor` | Gemini CLI | Google OAuth token refresh cycle |
|
||||
| `GithubExecutor` | GitHub Copilot | Copilot token refresh, VSCode-mimicking headers |
|
||||
| `GitlabExecutor` | GitLab Duo | GitLab OAuth + project-scoped routing |
|
||||
| `GlmExecutor` | Z.AI GLM (incl. `glmt` preset) | Thinking-budget aware, GLMT preset constants |
|
||||
@@ -934,7 +933,7 @@ All other providers (including custom compatible nodes) use the `DefaultExecutor
|
||||
|
||||
## Provider Compatibility Matrix
|
||||
|
||||
> **Note:** The matrix below is a representative sample of the 226 registered providers in
|
||||
> **Note:** The matrix below is a representative sample of the 237 registered providers in
|
||||
> OmniRoute v3.8.0. For the canonical and continuously-updated list, refer to
|
||||
> [`docs/reference/PROVIDER_REFERENCE.md`](../reference/PROVIDER_REFERENCE.md) (auto-generated) or the source of
|
||||
> truth at `src/shared/constants/providers.ts` (Zod-validated at load).
|
||||
@@ -943,7 +942,6 @@ All other providers (including custom compatible nodes) use the `DefaultExecutor
|
||||
| ----------------- | ---------------- | --------------------- | ---------------- | ---------- | ------------- | ------------------ |
|
||||
| Claude | claude | API Key / OAuth | ✅ | ✅ | ✅ | ⚠️ Admin only |
|
||||
| Gemini | gemini | API Key / OAuth | ✅ | ✅ | ✅ | ⚠️ Cloud Console |
|
||||
| Gemini CLI | gemini-cli | OAuth | ✅ | ✅ | ✅ | ⚠️ Cloud Console |
|
||||
| Antigravity | antigravity | OAuth | ✅ | ✅ | ✅ | ✅ Full quota API |
|
||||
| OpenAI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Codex | openai-responses | OAuth | ✅ forced | ❌ | ✅ | ✅ Rate limits |
|
||||
@@ -1014,7 +1012,7 @@ Target formats include:
|
||||
|
||||
- OpenAI chat/Responses
|
||||
- Claude
|
||||
- Gemini/Gemini-CLI/Antigravity envelope
|
||||
- Gemini/Antigravity envelope
|
||||
- Kiro
|
||||
- Cursor
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
title: "Authorization Guide"
|
||||
version: 3.8.2
|
||||
lastUpdated: 2026-06-05
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Authorization Guide
|
||||
|
||||
> **Source of truth:** `src/server/authz/`, `src/shared/constants/publicApiRoutes.ts`, `src/lib/api/requireManagementAuth.ts`, `src/shared/utils/apiAuth.ts`
|
||||
> **Last updated:** 2026-06-05 — v3.8.2
|
||||
> **Last updated:** 2026-06-28 — v3.8.40
|
||||
|
||||
OmniRoute has a route-aware authorization pipeline that gates every API request. Classification is **deterministic** and **fail-closed** — anything that cannot be classified ends up as `MANAGEMENT` and demands a session or management-grade token. This page explains the model for engineers maintaining routes or designing new endpoints.
|
||||
|
||||
@@ -43,11 +43,11 @@ Some management routes accept **either** mode: cookie OR `Bearer <key>` when the
|
||||
|
||||
`src/server/authz/types.ts` defines three classes; any route that cannot be classified deterministically falls back to `MANAGEMENT`.
|
||||
|
||||
| Class | Description | Auth required |
|
||||
| ------------ | ---------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| `PUBLIC` | Explicitly safe routes — login, logout, status, init, health, onboarding bootstrap. | None |
|
||||
| `CLIENT_API` | Model-serving endpoints — `/api/v1/*`, plus aliases `/v1/*`, `/chat/completions`, `/responses`, `/models`, `/codex/*`. | Bearer key when the effective `REQUIRE_API_KEY` feature flag is enabled |
|
||||
| `MANAGEMENT` | Dashboard pages, settings, providers, keys, admin and diagnostics endpoints. | Dashboard session OR Bearer with `manage` scope |
|
||||
| Class | Description | Auth required |
|
||||
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| `PUBLIC` | Explicitly safe routes — login, logout, status, init, health, onboarding bootstrap. | None |
|
||||
| `CLIENT_API` | Model-serving endpoints — `/api/v1/*`, `/api/v1beta/*`, plus aliases `/v1/*`, `/v1beta/*`, `/chat/completions`, `/responses`, `/models`, `/codex/*`. | Bearer key when the effective `REQUIRE_API_KEY` feature flag is enabled |
|
||||
| `MANAGEMENT` | Dashboard pages, settings, providers, keys, admin and diagnostics endpoints. | Dashboard session OR Bearer with `manage` scope |
|
||||
|
||||
## Pipeline
|
||||
|
||||
@@ -73,7 +73,7 @@ Trusted internal headers (defined in `src/server/authz/headers.ts`) are **stripp
|
||||
Each route class has a policy in `src/server/authz/policies/`:
|
||||
|
||||
- **`publicPolicy`** (`policies/public.ts`) — always returns `allow({ kind: "anonymous", id: "anonymous" })`.
|
||||
- **`clientApiPolicy`** (`policies/clientApi.ts`) — extracts Bearer, validates via `validateApiKey()`. Falls through to anonymous only when the effective `REQUIRE_API_KEY` feature flag is disabled. The effective flag is resolved through `isRequireApiKeyEnabled()` (`DB feature flag override > process.env.REQUIRE_API_KEY > default`) so Dashboard Feature Flags and environment variables govern `/api/v1/*` and aliases consistently; resolver failures fail closed. Allows dashboard-session requests on client API routes (including `/api/v1/models`, used by the dashboard model catalog).
|
||||
- **`clientApiPolicy`** (`policies/clientApi.ts`) — extracts Bearer, validates via `validateApiKey()`. Falls through to anonymous only when the effective `REQUIRE_API_KEY` feature flag is disabled. The effective flag is resolved through `isRequireApiKeyEnabled()` (`DB feature flag override > process.env.REQUIRE_API_KEY > default`) so Dashboard Feature Flags and environment variables govern `/api/v1/*`, `/api/v1beta/*`, and aliases consistently; resolver failures fail closed. Allows dashboard-session requests on client API routes (including `/api/v1/models`, used by the dashboard model catalog).
|
||||
- **`managementPolicy`** (`policies/management.ts`) — accepts dashboard session, internal model-sync requests (matched against `/api/providers/[name]/(sync-models|models)`), or skips entirely if `isAuthRequired()` returns false. Returns 403 (`AUTH_001`) when a Bearer token is present but invalid, 401 otherwise. Also enforces the route-guard tiers (LOCAL_ONLY / ALWAYS_PROTECTED) before any auth branch — see [Route Guard Tiers](../security/ROUTE_GUARD_TIERS.md). LOCAL_ONLY paths in `LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES` (today: `/api/mcp/`) may be accessed from non-loopback when the Bearer key carries the `manage` scope; all other LOCAL_ONLY paths remain strict-loopback regardless of scope.
|
||||
|
||||
A successful policy returns `AuthSubject` with `kind ∈ { client_api_key, dashboard_session, management_key, anonymous }`. Downstream handlers can read it via `assertAuth(request, "CLIENT_API")` in `src/server/authz/assertAuth.ts` instead of re-running auth logic.
|
||||
@@ -99,13 +99,13 @@ PUBLIC_READONLY_API_ROUTE_PREFIXES = ["/api/monitoring/health", "/api/settings/r
|
||||
PUBLIC_READONLY_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
||||
```
|
||||
|
||||
Read-only prefixes are public **only** for safe methods. Note: `classifyRoute()` excludes `/api/v1/*` from the PUBLIC fall-through — those are always `CLIENT_API` so the Bearer-key policy still applies.
|
||||
Read-only prefixes are public **only** for safe methods. Note: `classifyRoute()` excludes `/api/v1/*` and `/api/v1beta/*` from the PUBLIC fall-through — those are always `CLIENT_API` so the Bearer-key policy still applies.
|
||||
|
||||
## Adding a New Route
|
||||
|
||||
### Pattern 1 — Public client API endpoint (Bearer-auth)
|
||||
|
||||
Routes under `/api/v1/` are classified `CLIENT_API` automatically. The middleware enforces the Bearer check; route handlers don't need to redo it but can read the subject if useful.
|
||||
Routes under `/api/v1/` and `/api/v1beta/` are classified `CLIENT_API` automatically. The middleware enforces the Bearer check; route handlers don't need to redo it but can read the subject if useful.
|
||||
|
||||
```typescript
|
||||
// src/app/api/v1/your-route/route.ts
|
||||
@@ -173,7 +173,7 @@ Preset bundles (`MCP_SCOPE_PRESETS`): `readonly`, `full`, `monitor`, `agent`. Us
|
||||
- No password configured **and** no `INITIAL_PASSWORD` env var → bootstrap mode allows the onboarding wizard and loopback requests, but exposed network requests still need credentials.
|
||||
- Any DB error → fails closed (secure-by-default).
|
||||
|
||||
Client API key enforcement uses `isRequireApiKeyEnabled()` in `src/shared/utils/featureFlags.ts`, not a direct `process.env.REQUIRE_API_KEY` read. This matters for deployed instances: toggling `REQUIRE_API_KEY` in Dashboard → Feature Flags stores a DB override and immediately affects `/v1/*`, `/models`, `/responses`, `/chat/completions`, `/codex/*`, and other client-API auth checks that share this helper. If the feature flag store cannot be read, client API auth fails closed and requires a key.
|
||||
Client API key enforcement uses `isRequireApiKeyEnabled()` in `src/shared/utils/featureFlags.ts`, not a direct `process.env.REQUIRE_API_KEY` read. This matters for deployed instances: toggling `REQUIRE_API_KEY` in Dashboard → Feature Flags stores a DB override and immediately affects `/v1/*`, `/v1beta/*`, `/models`, `/responses`, `/chat/completions`, `/codex/*`, and other client-API auth checks that share this helper. If the feature flag store cannot be read, client API auth fails closed and requires a key.
|
||||
|
||||
## Breaking Change — v3.8.0
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
title: "OmniRoute Codebase Documentation"
|
||||
version: 3.8.2
|
||||
lastUpdated: 2026-05-13
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# OmniRoute Codebase Documentation
|
||||
|
||||
> **Version:** v3.8.0
|
||||
> **Last updated:** 2026-05-13
|
||||
> **Last updated:** 2026-06-28
|
||||
> **Audience:** Engineers contributing to OmniRoute or building integrations on top of it.
|
||||
>
|
||||
> For high-level architecture diagrams and the reasoning behind each subsystem, read
|
||||
@@ -24,10 +24,10 @@ without inventing new modules.
|
||||
## 1. Tech Stack
|
||||
|
||||
| Concern | Choice |
|
||||
| ------------- | ------------------------------------------------------------------------------------------------------------------------ | --- | ------------- | --- | ------------------------------------ |
|
||||
| ------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Web framework | **Next.js 16** (App Router, standalone output, no global middleware) |
|
||||
| Language | **TypeScript 5.9+** — target `ES2022`, `module: esnext`, `moduleResolution: bundler`, `strict: false` |
|
||||
| Runtime | **Node.js** `>=20.20.2 <21 | | >=22.22.2 <23 | | >=24.0.0 <27`(enforced via`engines`) |
|
||||
| Language | **TypeScript 6.0+** — target `ES2022`, `module: esnext`, `moduleResolution: bundler`, `strict: false` |
|
||||
| Runtime | **Node.js** `>=22.22.2 <23` or `>=24.0.0 <27` (enforced via `engines` + `SUPPORTED_NODE_RANGE`) |
|
||||
| Database | **SQLite** via `better-sqlite3` (singleton, WAL journaling) |
|
||||
| Desktop | **Electron 41** + `electron-builder` 26.10 (separate workspace at `electron/`) |
|
||||
| Tests | **Node native test runner** (unit/integration), **Vitest** (MCP, autoCombo, cache), **Playwright** (e2e + protocols-e2e) |
|
||||
@@ -457,7 +457,7 @@ open-sse/
|
||||
├── transformer/ Responses API ↔ Chat Completions stream transformer
|
||||
├── services/ 80+ service modules (combos, fallback, quotas, identity, …)
|
||||
├── utils/ Streaming helpers, TLS client, AWS SigV4, proxy fetch, …
|
||||
└── mcp-server/ MCP server (3 transports, 30 scopes, 87 tools)
|
||||
└── mcp-server/ MCP server (3 transports, 30 scopes, 94 tools)
|
||||
```
|
||||
|
||||
### 4.1 `open-sse/handlers/`
|
||||
@@ -482,17 +482,16 @@ open-sse/
|
||||
|
||||
### 4.2 `open-sse/executors/`
|
||||
|
||||
45 provider executors, each extending `BaseExecutor` (`base.ts`):
|
||||
68 provider executors, each extending `BaseExecutor` (`base.ts`):
|
||||
|
||||
`antigravity`, `azure-openai`, `blackbox-web`, `chatgpt-web`, `cliproxyapi`,
|
||||
`cloudflare-ai`, `codex`, `commandCode`, `cursor`, `default`, `devin-cli`,
|
||||
`gemini-cli`, `github`, `gitlab`, `glm`, `grok-web`, `kie`, `kiro`,
|
||||
`muse-spark-web`, `nlpcloud`, `opencode`, `perplexity-web`, `petals`,
|
||||
`pollinations`, `puter`, `qoder`, `vertex`, `windsurf`, plus `claudeIdentity.ts`
|
||||
(shared identity helper) and `index.ts` (registry).
|
||||
|
||||
> Note: providers not listed here are served by `default.ts` using the generic
|
||||
> OpenAI-compatible executor. The full provider catalog (226+ entries) lives in
|
||||
> OpenAI-compatible executor. The full provider catalog (237 entries) lives in
|
||||
> `src/shared/constants/providers.ts`.
|
||||
|
||||
### 4.3 `open-sse/translator/`
|
||||
@@ -503,7 +502,7 @@ Hub-and-spoke translation (OpenAI is the hub).
|
||||
`antigravity-to-openai`, `claude-to-gemini`, `claude-to-openai`,
|
||||
`gemini-to-openai`, `openai-responses`, `openai-to-claude`,
|
||||
`openai-to-cursor`, `openai-to-gemini`, `openai-to-kiro`.
|
||||
- **8 response translators** (`translator/response/`):
|
||||
- **9 response translators** (`translator/response/`):
|
||||
`claude-to-openai`, `cursor-to-openai`, `gemini-to-claude`, `gemini-to-openai`,
|
||||
`kiro-to-openai`, `openai-responses`, `openai-to-antigravity`,
|
||||
`openai-to-claude`.
|
||||
@@ -525,11 +524,10 @@ Highlights (full list under `open-sse/services/`):
|
||||
|
||||
| Concern | Files |
|
||||
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Combo routing | `combo.ts` (15 strategies), `comboConfig.ts`, `comboMetrics.ts`, `comboManifestMetrics.ts`, `comboAgentMiddleware.ts` |
|
||||
| Combo routing | `combo.ts` (17 strategies), `comboConfig.ts`, `comboMetrics.ts`, `comboManifestMetrics.ts`, `comboAgentMiddleware.ts` |
|
||||
| Auto Combo engine | `autoCombo/` — `engine.ts`, `scoring.ts`, `taskFitness.ts`, `virtualFactory.ts`, `modePacks.ts`, `autoPrefix.ts`, `persistence.ts`, `providerDiversity.ts`, `providerRegistryAccessor.ts`, `routerStrategy.ts`, `selfHealing.ts`, `index.ts` |
|
||||
| Resilience | `accountFallback.ts` (cooldown + lockout), `errorClassifier.ts`, `emergencyFallback.ts`, `rateLimitManager.ts`, `rateLimitSemaphore.ts`, `accountSemaphore.ts`, `accountSelector.ts` |
|
||||
| Quotas | `quotaMonitor.ts`, `quotaPreflight.ts`, `bailianQuotaFetcher.ts`, `codexQuotaFetcher.ts`, `deepseekQuotaFetcher.ts`, `crofUsageFetcher.ts`, `antigravityCredits.ts` |
|
||||
| Provider-specific shaping | `claudeCodeCCH.ts`, `claudeCodeCompatible.ts`, `claudeCodeConstraints.ts`, `claudeCodeExtraRemap.ts`, `claudeCodeFingerprint.ts`, `claudeCodeObfuscation.ts`, `claudeCodeToolRemapper.ts`, `cloudCodeHeaders.ts`, `cloudCodeThinking.ts`, `geminiCliHeaders.ts`, `geminiThoughtSignatureStore.ts`, `gigachatAuth.ts`, `antigravityHeaders.ts`, `antigravityHeaderScrub.ts`, `antigravityIdentity.ts`, `antigravityObfuscation.ts`, `antigravityVersion.ts`, `antigravity429Engine.ts`, `chatgptTlsClient.ts`, `chatgptImageCache.ts`, `cursorSessionManager.ts`, `qoderCli.ts`, `qwenThinking.ts`, `modelscopePolicy.ts` |
|
||||
| Caching | `reasoningCache.ts`, `searchCache.ts`, `signatureCache.ts`, `requestDedup.ts` |
|
||||
| Routing intelligence | `intentClassifier.ts`, `taskAwareRouter.ts`, `backgroundTaskDetector.ts`, `volumeDetector.ts`, `wildcardRouter.ts`, `workflowFSM.ts`, `specificityDetector.ts`, `specificityRules.ts`, `specificityTypes.ts` |
|
||||
| Model handling | `modelCapabilities.ts`, `modelDeprecation.ts`, `modelFamilyFallback.ts`, `modelStrip.ts`, `model.ts`, `provider.ts`, `providerRequestDefaults.ts`, `providerCostData.ts`, `payloadRules.ts` |
|
||||
@@ -636,17 +634,17 @@ Two binaries are exposed in `package.json` → `bin`:
|
||||
|
||||
## 7. `tests/`
|
||||
|
||||
| Directory | Type |
|
||||
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
|
||||
| `tests/unit/` | Unit tests via Node native test runner (506 files, plus `api/`, `auth/`, `authz/` subdirs) |
|
||||
| `tests/integration/` | Cross-module + DB-state tests |
|
||||
| `tests/e2e/` | Playwright UI tests |
|
||||
| `tests/protocols-e2e/` | MCP/A2A protocol e2e |
|
||||
| `tests/translator/` | Translator-specific tests |
|
||||
| `tests/security/` | Security regressions |
|
||||
| `tests/load/` | Load / stress tests |
|
||||
| `tests/golden-set/` | Reference outputs for translator regressions |
|
||||
| `tests/helpers/`, `tests/fixtures/`, `tests/manual/`, `tests/scratch_test.mjs` | Support |
|
||||
| Directory | Type |
|
||||
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- |
|
||||
| `tests/unit/` | Unit tests via Node native test runner (1821 files, plus `api/`, `auth/`, `authz/` subdirs) |
|
||||
| `tests/integration/` | Cross-module + DB-state tests |
|
||||
| `tests/e2e/` | Playwright UI tests |
|
||||
| `tests/protocols-e2e/` | MCP/A2A protocol e2e |
|
||||
| `tests/translator/` | Translator-specific tests |
|
||||
| `tests/security/` | Security regressions |
|
||||
| `tests/load/` | Load / stress tests |
|
||||
| `tests/golden-set/` | Reference outputs for translator regressions |
|
||||
| `tests/helpers/`, `tests/fixtures/`, `tests/manual/`, `tests/scratch_test.mjs` | Support |
|
||||
|
||||
Common commands:
|
||||
|
||||
@@ -807,7 +805,7 @@ See [A2A-SERVER.md § Adding a New Skill](../frameworks/A2A-SERVER.md). Skills l
|
||||
- **Branches**: prefixes `feat/`, `fix/`, `refactor/`, `docs/`, `test/`,
|
||||
`chore/`. Never commit directly to `main`.
|
||||
- **Husky**: pre-commit runs `lint-staged` + `check:docs-sync` +
|
||||
`check:any-budget:t11`; pre-push runs `npm run test:unit`.
|
||||
`check:any-budget:t11`; pre-push runs `check:any-budget:t11` + `check:tracked-artifacts` (fast gates; excludes `test:unit`).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Monitoring & Costs — Navigation Structure"
|
||||
version: 3.8.6
|
||||
lastUpdated: 2026-05-28
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Monitoring & Costs — Navigation Structure
|
||||
@@ -32,13 +32,13 @@ Monitoring ← REORGANIZED (Group B, plan 16)
|
||||
|
||||
Path prefix: `/dashboard/costs/`
|
||||
|
||||
| Item | URL | Description |
|
||||
|------|-----|-------------|
|
||||
| Overview | `/dashboard/costs` | Aggregated cost dashboard (moved from Analytics) |
|
||||
| Pricing | `/dashboard/costs/pricing` | Per-model pricing table |
|
||||
| Budget | `/dashboard/costs/budget` | Budget thresholds + alerts |
|
||||
| Quota Sharing | `/dashboard/costs/quota-share` | Quota Share pools + usage |
|
||||
| Plan Config | `/dashboard/costs/quota-share/plans` | Per-provider plan overrides |
|
||||
| Item | URL | Description |
|
||||
| ------------- | ------------------------------------ | ------------------------------------------------ |
|
||||
| Overview | `/dashboard/costs` | Aggregated cost dashboard (moved from Analytics) |
|
||||
| Pricing | `/dashboard/costs/pricing` | Per-model pricing table |
|
||||
| Budget | `/dashboard/costs/budget` | Budget thresholds + alerts |
|
||||
| Quota Sharing | `/dashboard/costs/quota-share` | Quota Share pools + usage |
|
||||
| Plan Config | `/dashboard/costs/quota-share/plans` | Per-provider plan overrides |
|
||||
|
||||
**Rationale**: Pricing, Budget, and Quota Sharing were previously under
|
||||
`Monitoring > Costs Parameters`. Moving them to a dedicated top-level section
|
||||
@@ -68,11 +68,11 @@ Monitoring
|
||||
|
||||
### What changed from the old structure
|
||||
|
||||
| Before | After |
|
||||
|--------|-------|
|
||||
| Activity = tab inside Logs that rendered the Audit Log | Activity = dedicated feed (`/dashboard/activity`) |
|
||||
| Costs Parameters group in Monitoring | Moved to Costs section |
|
||||
| Flat list: Logs, Activity (logs), Audit, Health, Runtime, Pricing, Budget, Quota | Structured 3-group + dedicated Costs section |
|
||||
| Before | After |
|
||||
| -------------------------------------------------------------------------------- | ------------------------------------------------- |
|
||||
| Activity = tab inside Logs that rendered the Audit Log | Activity = dedicated feed (`/dashboard/activity`) |
|
||||
| Costs Parameters group in Monitoring | Moved to Costs section |
|
||||
| Flat list: Logs, Activity (logs), Audit, Health, Runtime, Pricing, Budget, Quota | Structured 3-group + dedicated Costs section |
|
||||
|
||||
---
|
||||
|
||||
@@ -80,15 +80,15 @@ Monitoring
|
||||
|
||||
These two are now distinct:
|
||||
|
||||
| Dimension | Activity (`/dashboard/activity`) | Audit Log (`/dashboard/audit`) |
|
||||
|-----------|----------------------------------|-------------------------------|
|
||||
| **Purpose** | User-facing event feed ("what happened recently") | Compliance / security log |
|
||||
| **Data source** | `GET /api/compliance/audit-log?level=high` | `GET /api/compliance/audit-log?level=all` |
|
||||
| **Format** | Timeline, grouped by day, human-readable verbs + icons | Dense paginaged table, 50/page |
|
||||
| **Filters** | Event type category | Action, severity, actor, date range |
|
||||
| **Export** | Not available | JSON export |
|
||||
| **Actor filter** | Not applicable | Filterable by actor |
|
||||
| **Events shown** | High-level actions only (allowlist) | All audit events |
|
||||
| Dimension | Activity (`/dashboard/activity`) | Audit Log (`/dashboard/audit`) |
|
||||
| ---------------- | ------------------------------------------------------ | ----------------------------------------- |
|
||||
| **Purpose** | User-facing event feed ("what happened recently") | Compliance / security log |
|
||||
| **Data source** | `GET /api/compliance/audit-log?level=high` | `GET /api/compliance/audit-log?level=all` |
|
||||
| **Format** | Timeline, grouped by day, human-readable verbs + icons | Dense paginaged table, 50/page |
|
||||
| **Filters** | Event type category | Action, severity, actor, date range |
|
||||
| **Export** | Not available | JSON export |
|
||||
| **Actor filter** | Not applicable | Filterable by actor |
|
||||
| **Events shown** | High-level actions only (allowlist) | All audit events |
|
||||
|
||||
### High-Level Actions allowlist
|
||||
|
||||
@@ -133,14 +133,14 @@ reference the old ID.
|
||||
|
||||
Namespaces added by Group B:
|
||||
|
||||
| Namespace key | Covers |
|
||||
|---------------|--------|
|
||||
| `sidebar.costsSection` | Costs section label |
|
||||
| `sidebar.activity` | Activity sidebar item |
|
||||
| `sidebar.logsGroup` | Logs subgroup label |
|
||||
| `sidebar.systemGroup` | System subgroup label |
|
||||
| `sidebar.costsOverview` | Costs overview item |
|
||||
| `activity.*` | All Activity page strings (title, verbs, filters, empty state) |
|
||||
| Namespace key | Covers |
|
||||
| ----------------------- | -------------------------------------------------------------- |
|
||||
| `sidebar.costsSection` | Costs section label |
|
||||
| `sidebar.activity` | Activity sidebar item |
|
||||
| `sidebar.logsGroup` | Logs subgroup label |
|
||||
| `sidebar.systemGroup` | System subgroup label |
|
||||
| `sidebar.costsOverview` | Costs overview item |
|
||||
| `activity.*` | All Activity page strings (title, verbs, filters, empty state) |
|
||||
|
||||
Source-of-truth locales: `pt-BR` and `en`. All other 39 locales fall back to
|
||||
English via the `next-intl` fallback mechanism (configured in `src/i18n/config.ts`).
|
||||
|
||||
@@ -13,7 +13,7 @@ in `CLAUDE.md`.
|
||||
|
||||
---
|
||||
|
||||
## Gate Inventory (~48 scripts)
|
||||
## Gate Inventory (~50 scripts)
|
||||
|
||||
Scripts live under `scripts/check/` (policy gates) and `scripts/quality/` (ratchet engine).
|
||||
The CI source of truth is `.github/workflows/ci.yml`.
|
||||
@@ -31,7 +31,7 @@ Runs on every PR to `main`. Blocks merge on failure.
|
||||
| `check:provider-consistency` | Every provider in `providers.ts` has a matching entry in `providerRegistry.ts` (and vice-versa, within the allowlist) | Yes |
|
||||
| `check:fetch-targets` | Every `fetch("/api/...")` in client-side `src/` resolves to a real `route.ts` | Yes |
|
||||
| `check:deps` | All `npm install`-able deps across every `package.json` in the repo are in `dependency-allowlist.json`; new unpinned or slopsquatted packages flagged | Yes |
|
||||
| `audit:deps` | `npm audit` (root + electron) — no high/critical advisories (overlaps osv `check:vuln-ratchet`; see Rationalization Backlog) | Yes |
|
||||
| `audit:deps` | `npm audit` (root + electron) — no high/critical advisories (overlaps osv `check:vuln-ratchet`; see Rationalization Backlog) | Yes |
|
||||
| `check:lockfile` | `package-lock.json` integrity — https registry, integrity hashes, no host overrides | Yes |
|
||||
| `check:licenses` | SPDX license allowlist for production dependencies | Yes |
|
||||
| `check:tracked-artifacts` | No build artifacts / committed `node_modules` symlinks (also runs in husky pre-push) | Yes |
|
||||
@@ -51,16 +51,16 @@ Runs on every PR to `main`. Blocks merge on failure.
|
||||
|
||||
Runs after `test-coverage`. Blocks merge on failure.
|
||||
|
||||
| Script | Validates | Blocking |
|
||||
| ------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------- |
|
||||
| `quality:collect` | Emits `quality-metrics.json` (ESLint warning count, coverage from merged shard report) | Yes (upstream of ratchet) |
|
||||
| `quality:ratchet` | Each metric in `quality-baseline.json` has not regressed (ESLint warnings ≤ baseline; coverage ≥ baseline) | Yes |
|
||||
| `check:duplication` | Code duplication (jscpd@4) does not exceed baseline in `quality-baseline.json` | Yes |
|
||||
| `check:complexity` | File-level cyclomatic complexity does not exceed the cap (core ESLint `complexity` + `max-lines-per-function`) | Yes |
|
||||
| `check:cognitive-complexity` | Cognitive complexity ratchet (`eslint-plugin-sonarjs`) — separate ESLint pass; mergeable with `check:complexity` (see Backlog) | Yes |
|
||||
| `check:dead-code` | Unused exports / files ratchet (knip) does not regress vs baseline | Yes |
|
||||
| `check:type-coverage` | Percent-typed ratchet (`type-coverage`) does not regress; largely subsumes `typecheck:noimplicit:core` | Yes |
|
||||
| `check:codeql-ratchet` | Open CodeQL alert count does not regress (reads via `gh api`; graceful-skip without token) | Yes |
|
||||
| Script | Validates | Blocking |
|
||||
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------- |
|
||||
| `quality:collect` | Emits `quality-metrics.json` (ESLint warning count, coverage from merged shard report) | Yes (upstream of ratchet) |
|
||||
| `quality:ratchet` | Each metric in `quality-baseline.json` has not regressed (ESLint warnings ≤ baseline; coverage ≥ baseline) | Yes |
|
||||
| `check:duplication` | Code duplication (jscpd@4) does not exceed baseline in `quality-baseline.json` | Yes |
|
||||
| `check:complexity` | File-level cyclomatic complexity does not exceed the cap (core ESLint `complexity` + `max-lines-per-function`) | Yes |
|
||||
| `check:cognitive-complexity` | Cognitive complexity ratchet (`eslint-plugin-sonarjs`) — separate ESLint pass; mergeable with `check:complexity` (see Backlog) | Yes |
|
||||
| `check:dead-code` | Unused exports / files ratchet (knip) does not regress vs baseline | Yes |
|
||||
| `check:type-coverage` | Percent-typed ratchet (`type-coverage`) does not regress; largely subsumes `typecheck:noimplicit:core` | Yes |
|
||||
| `check:codeql-ratchet` | Open CodeQL alert count does not regress (reads via `gh api`; graceful-skip without token) | Yes |
|
||||
|
||||
### Job: `quality-extended`
|
||||
|
||||
@@ -68,13 +68,13 @@ Entire job is advisory (`continue-on-error: true`). The npm-based ratchets run f
|
||||
real; the external scanners install via `gh release download` and self-skip (exit 0)
|
||||
when a binary is still absent.
|
||||
|
||||
| Script | Validates | Blocking |
|
||||
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | ------------ |
|
||||
| `check:circular-deps` | No circular dependencies (dpdm) | **Advisory** |
|
||||
| `check:bundle-size` | Bundle size does not exceed the cap | **Advisory** |
|
||||
| `check:secrets` | Secret scanning (gitleaks) — skips if binary absent | **Advisory** |
|
||||
| `check:vuln-ratchet` | Dependency vulnerabilities (osv-scanner) do not regress — skips if binary absent | **Advisory** |
|
||||
| `check:workflows` | Workflow lint (actionlint + zizmor) — skips if binaries absent | **Advisory** |
|
||||
| Script | Validates | Blocking |
|
||||
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------ |
|
||||
| `check:circular-deps` | No circular dependencies (dpdm) | **Advisory** |
|
||||
| `check:bundle-size` | Bundle size does not exceed the cap | **Advisory** |
|
||||
| `check:secrets` | Secret scanning (gitleaks) — skips if binary absent | **Advisory** |
|
||||
| `check:vuln-ratchet` | Dependency vulnerabilities (osv-scanner) do not regress — skips if binary absent | **Advisory** |
|
||||
| `check:workflows` | Workflow lint (actionlint + zizmor) — skips if binaries absent | **Advisory** |
|
||||
| `check:openapi-breaking` | Breaking changes to the public API contract (`openapi.yaml`) vs the base branch (oasdiff) — emits `openapiBreaking=N`; skips if oasdiff absent or base spec unresolvable | **Advisory** |
|
||||
|
||||
### Job: `docs-sync-strict`
|
||||
@@ -127,19 +127,19 @@ Runs after `build`. Blocks merge on failure.
|
||||
|
||||
| Suite | Validates | Blocking |
|
||||
| ---------------- | ------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `test:vitest` | MCP server (87 tools), autoCombo, cache — vitest runner | Yes |
|
||||
| `test:vitest` | MCP server (94 tools), autoCombo, cache — vitest runner | Yes |
|
||||
| `test:vitest:ui` | UI component tests — vitest runner | **Advisory** (`continue-on-error: true`) — failing until Fase 6A UI triage |
|
||||
|
||||
### Nightly workflows (scheduled, advisory)
|
||||
|
||||
These run on a cron schedule (and `workflow_dispatch`), never on PRs. All are advisory.
|
||||
|
||||
| Workflow | Validates | Blocking |
|
||||
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------ |
|
||||
| `nightly-property` | fast-check property tests with a random seed + high run count | **Advisory** |
|
||||
| `nightly-resilience` | heap-growth gate, chaos fault-injection, k6 load/soak | **Advisory** |
|
||||
| `nightly-llm-security` | promptfoo injection guard (block mode) + garak probes (skipped without a provider secret) | **Advisory** |
|
||||
| `nightly-schemathesis` | OpenAPI contract fuzzing (schemathesis) against a live OmniRoute using `docs/openapi.yaml` — surfaces spec violations / unhandled 500s (Fase 8 B.4) | **Advisory** |
|
||||
| Workflow | Validates | Blocking |
|
||||
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ |
|
||||
| `nightly-property` | fast-check property tests with a random seed + high run count | **Advisory** |
|
||||
| `nightly-resilience` | heap-growth gate, chaos fault-injection, k6 load/soak | **Advisory** |
|
||||
| `nightly-llm-security` | promptfoo injection guard (block mode) + garak probes (skipped without a provider secret) | **Advisory** |
|
||||
| `nightly-schemathesis` | OpenAPI contract fuzzing (schemathesis) against a live OmniRoute using `docs/openapi.yaml` — surfaces spec violations / unhandled 500s (Fase 8 B.4) | **Advisory** |
|
||||
|
||||
---
|
||||
|
||||
@@ -226,7 +226,7 @@ allowlist is a false sense of quality.
|
||||
## Agent tooling: LSP-in-the-loop (opt-in)
|
||||
|
||||
Beyond the CI gates, OmniRoute ships an **opt-in** `agent-lsp` scaffold
|
||||
([`.mcp.json.example`](../../.mcp.json.example), Fase 7 Task 15). Copy it to `.mcp.json`
|
||||
(a project-level `.mcp.json`, Fase 7 Task 15). Create `.mcp.json`
|
||||
to expose a TypeScript language server to coding agents, so they resolve symbols /
|
||||
diagnostics **before** writing code — a compile-before-claim companion to
|
||||
`typecheck:core` that cuts "invented symbol" errors at the source. It is intentionally
|
||||
@@ -257,7 +257,7 @@ Each candidate was validated against the live gate state on 2026-06-17 (trust-bu
|
||||
several "obvious" merges turned out to hide debt and are **not** clean drop-ins.
|
||||
|
||||
- **`check:docs-sync` runs twice** — standalone in the `lint` job and again inside `check:docs-all` (`docs-sync-strict`) and the husky pre-commit hook. ✅ **DONE** — standalone `lint` invocation removed.
|
||||
- **CVE scanning** — ❌ **NOT a clean merge.** `audit:deps` hard-fails on any high/critical CVE; `check:vuln-ratchet` (osv) only fails on a *regression* vs baseline (currently 1 MODERATE). Different semantics — dropping `audit:deps` would lose the absolute high/critical gate. Keep both.
|
||||
- **CVE scanning** — ❌ **NOT a clean merge.** `audit:deps` hard-fails on any high/critical CVE; `check:vuln-ratchet` (osv) only fails on a _regression_ vs baseline (currently 1 MODERATE). Different semantics — dropping `audit:deps` would lose the absolute high/critical gate. Keep both.
|
||||
- **Cycle detection** — ❌ **NOT a clean merge.** `check:circular-deps` (dpdm) reports **91 cycles** (that is why it is advisory); it cannot be promoted to blocking without first resolving them, and it has a broader scope than the green, curated `check:cycles`. Keep `check:cycles` blocking; resolving the 91 dpdm cycles is its own backlog.
|
||||
- **Complexity** — ⏳ valid but real surgery. `check:complexity` (core ESLint) + `check:cognitive-complexity` (sonarjs) are two ESLint passes over `src` + `open-sse`; merging into one config emitting both metrics needs careful ratchet re-wiring. Deferred.
|
||||
- **`/api` anti-hallucination** — ⏳ valid but script surgery. `check:openapi-routes` (spec→route) + `check:docs-symbols` (prose→route) share resolution logic; collapsing them is a non-trivial script change. Deferred.
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
title: "Repository Map"
|
||||
version: 3.8.26
|
||||
lastUpdated: 2026-06-15
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Repository Map
|
||||
|
||||
> **One-line description for every directory and root file.**
|
||||
> Last updated: 2026-06-15 — OmniRoute v3.8.26
|
||||
> Last updated: 2026-06-28 — OmniRoute v3.8.40
|
||||
>
|
||||
> Use this map to navigate the codebase quickly. For deep dives, follow links to dedicated docs.
|
||||
|
||||
@@ -110,7 +110,6 @@ OmniRoute/
|
||||
> **Moved out of the root in v3.8.26 (declutter):**
|
||||
>
|
||||
> - **→ `config/quality/`:** `quality-baseline.json`, `complexity-baseline.json`, `duplication-baseline.json`, `file-size-baseline.json`, `test-discovery-baseline.json`, `dependency-allowlist.json`, `.license-allowlist.json`, and the generated `quality-metrics.json` (gitignored). See [`## config/`](#config--static-configs--quality-gate-state).
|
||||
> - **→ `docs/ops/`:** `DOCUMENTATION_AUDIT_REPORT.md`.
|
||||
|
||||
---
|
||||
|
||||
@@ -210,7 +209,7 @@ src/
|
||||
| `cacheLayer.ts`, `idempotencyLayer.ts` | Request caching + idempotency |
|
||||
| (~30 more top-level files) | Specialized helpers (logEnv, modelsDevSync, piiSanitizer, etc.) |
|
||||
|
||||
### `src/db/` — Database (45+ modules + 55 migrations)
|
||||
### `src/db/` — Database (94 modules + 106 migrations)
|
||||
|
||||
| Subdir | Purpose |
|
||||
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
@@ -245,9 +244,9 @@ src/
|
||||
|
||||
| Module | Purpose |
|
||||
| -------------------------------- | ---------------------------------------------------------------------- |
|
||||
| `constants/providers.ts` | **226 providers** with Zod validation (source of truth) |
|
||||
| `constants/providers.ts` | **236 providers** with Zod validation (source of truth) |
|
||||
| `constants/cliTools.ts` | External CLI tool registry |
|
||||
| `constants/routingStrategies.ts` | **14 routing strategies** with priorities |
|
||||
| `constants/routingStrategies.ts` | **17 routing strategies** with priorities |
|
||||
| `constants/publicApiRoutes.ts` | Routes that require Bearer (vs management) auth |
|
||||
| `constants/upstreamHeaders.ts` | Header denylist for upstream requests |
|
||||
| `validation/schemas.ts` | ~80 Zod schemas (single source of truth for API contracts) |
|
||||
@@ -266,12 +265,12 @@ Separate npm workspace (`@omniroute/open-sse`). Handles request processing + pro
|
||||
|
||||
```
|
||||
open-sse/
|
||||
├── handlers/ # 15 files (11 handlers + 4 helpers): chatCore, responsesHandler, embeddings, audio, image, video, music, rerank, moderations, search, etc.
|
||||
├── executors/ # 31 provider-specific executors (extend BaseExecutor)
|
||||
├── translator/ # Format converters (9 request, 8 response, 9 helpers)
|
||||
├── handlers/ # 16 files (12 handlers + 4 helpers): chatCore, responsesHandler, embeddings, audio, image, video, music, rerank, moderations, search, etc.
|
||||
├── executors/ # 67 provider-specific executors (extend BaseExecutor)
|
||||
├── translator/ # Format converters (9 request, 9 response, 9 helpers)
|
||||
├── transformer/ # Responses API ↔ Chat Completions (TransformStream)
|
||||
├── services/ # ~80+ service modules (combo, accountFallback, autoCombo, reasoningCache, claude code/chatgpt stealth, modelDeprecation, taskAwareRouter, workflowFSM, etc.)
|
||||
├── mcp-server/ # MCP server (87 tools, 3 transports, 30 scopes)
|
||||
├── mcp-server/ # MCP server (94 tools, 3 transports, 30 scopes)
|
||||
├── config/ # Provider/model registries, header config, model aliases
|
||||
├── utils/ # TLS client, proxy fetch/dispatcher, network helpers
|
||||
├── index.ts # Workspace entry
|
||||
@@ -290,7 +289,7 @@ open-sse/
|
||||
| `scopeEnforcement.ts` | Per-tool scope validation |
|
||||
| `runtimeHeartbeat.ts` | Health heartbeat to `DATA_DIR/runtime/mcp-heartbeat.json` |
|
||||
| `descriptionCompressor.ts` | Compress tool description metadata to save context |
|
||||
| `schemas/tools.ts` | 30 base tool definitions + scopes |
|
||||
| `schemas/tools.ts` | 34 base tool definitions + scopes |
|
||||
| `tools/advancedTools.ts` | Advanced tool implementations |
|
||||
| `tools/memoryTools.ts` | 3 memory tools (search/add/clear) |
|
||||
| `tools/skillTools.ts` | 4 skill tools (list/enable/execute/executions) |
|
||||
@@ -401,13 +400,13 @@ open-sse/
|
||||
| `CLI-TOOLS.md` | External CLI integrations + Internal OmniRoute CLI |
|
||||
| `I18N.md` | i18n architecture, adding a language, 30 locales |
|
||||
| `UNINSTALL.md` | Clean uninstall steps |
|
||||
| `PROVIDER_REFERENCE.md` | **Auto-generated** catalog of 226 providers (regen: `npm run gen:provider-reference`) |
|
||||
| `PROVIDER_REFERENCE.md` | **Auto-generated** catalog of 236 providers (regen: `npm run gen:provider-reference`) |
|
||||
|
||||
### Subsystem deep-dives
|
||||
|
||||
| Doc | Purpose |
|
||||
| -------------------------- | ------------------------------------------------------------------- |
|
||||
| `MCP-SERVER.md` | MCP server: 87 tools, 3 transports, 30 scopes, REST endpoints |
|
||||
| `MCP-SERVER.md` | MCP server: 94 tools, 3 transports, 30 scopes, REST endpoints |
|
||||
| `A2A-SERVER.md` | A2A v0.3: JSON-RPC, 5 skills, REST helpers, agent card |
|
||||
| `AGENT_PROTOCOLS_GUIDE.md` | Unified guide: A2A vs ACP vs Cloud Agents |
|
||||
| `CLOUD_AGENT.md` | Codex Cloud / Devin / Jules orchestration |
|
||||
@@ -448,12 +447,12 @@ open-sse/
|
||||
|
||||
### Subdirectories
|
||||
|
||||
| Subdir | Purpose |
|
||||
| ------------------------- | ------------------------------------------------------------------------------------- |
|
||||
| `docs/archive/` | Archived/historical docs (e.g., `RFC-AUTO-ASSESSMENT-DRAFT.md` — superseded by EVALS) |
|
||||
| `docs/i18n/` | Localized doc translations (~42 locales) |
|
||||
| `docs/screenshots/` | Image assets for guides |
|
||||
| `docs/superpowers/plans/` | Implementation plans (generated by `superpowers:writing-plans` skill) |
|
||||
| Subdir | Purpose |
|
||||
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `docs/archive/` | Archived/historical docs (e.g., `RFC-AUTO-ASSESSMENT-DRAFT.md` — superseded by EVALS) |
|
||||
| `docs/i18n/` | Localized doc translations (~42 locales) |
|
||||
| `docs/screenshots/` | Image assets for guides |
|
||||
| `_tasks/superpowers/` | Plans/specs from superpowers (`writing-plans`/`brainstorming`) + research — isolated, separately-versioned repo, gitignored by the main tree. See CLAUDE.md → "Planning & Research Artifacts". |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Resilience Guide"
|
||||
version: 3.8.31
|
||||
lastUpdated: 2026-06-20
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Resilience Guide
|
||||
@@ -124,18 +124,18 @@ opt-in feature with its own settings card and a self-healing recovery path.
|
||||
**Settings card:** Settings → Model Lockout
|
||||
(`src/app/(dashboard)/dashboard/settings/components/ModelLockoutCard.tsx`).
|
||||
This is **distinct** from the read-only `ModelCooldownsCard` above (which only
|
||||
*lists* active lockouts) — the new card *configures the parameters*. Defaults
|
||||
_lists_ active lockouts) — the new card _configures the parameters_. Defaults
|
||||
live in `DEFAULT_MODEL_LOCKOUT_SETTINGS`
|
||||
(`src/lib/resilience/modelLockoutSettings.ts`):
|
||||
|
||||
| Setting | Default | Meaning |
|
||||
| ----------------------- | -------------------------------- | --------------------------------------------------------------- |
|
||||
| `enabled` | `false` | Master toggle — model lockout is **off by default**. |
|
||||
| `errorCodes` | `[403, 404, 429, 502, 503, 504]` | Upstream statuses that count as a model-scoped failure. |
|
||||
| `baseCooldownMs` | `120_000` (120 s) | Initial lockout duration for the first failure. |
|
||||
| `maxCooldownMs` | `1_800_000` (30 min) | Cap on the escalated cooldown. |
|
||||
| `maxBackoffSteps` | `10` | Max exponential-backoff escalation steps. |
|
||||
| `useExponentialBackoff` | `true` | Whether repeated failures escalate the cooldown exponentially. |
|
||||
| Setting | Default | Meaning |
|
||||
| ----------------------- | -------------------------------- | -------------------------------------------------------------- |
|
||||
| `enabled` | `false` | Master toggle — model lockout is **off by default**. |
|
||||
| `errorCodes` | `[403, 404, 429, 502, 503, 504]` | Upstream statuses that count as a model-scoped failure. |
|
||||
| `baseCooldownMs` | `120_000` (120 s) | Initial lockout duration for the first failure. |
|
||||
| `maxCooldownMs` | `1_800_000` (30 min) | Cap on the escalated cooldown. |
|
||||
| `maxBackoffSteps` | `10` | Max exponential-backoff escalation steps. |
|
||||
| `useExponentialBackoff` | `true` | Whether repeated failures escalate the cooldown exponentially. |
|
||||
|
||||
Settings persist through the normal settings store and validate via the
|
||||
resilience settings schema; the card clamps `baseCooldownMs`/`maxCooldownMs`
|
||||
@@ -154,8 +154,8 @@ either path can re-enable a model.
|
||||
|
||||
**State:** lockouts are held **in-memory** (per-process `Map`s of
|
||||
`ModelLockoutEntry` keyed by `provider:connectionId:model`), not persisted to
|
||||
the DB — they are lost on restart. The *settings* are persisted; the active
|
||||
lockout *state* is ephemeral.
|
||||
the DB — they are lost on restart. The _settings_ are persisted; the active
|
||||
lockout _state_ is ephemeral.
|
||||
|
||||
---
|
||||
|
||||
@@ -185,7 +185,7 @@ concurrency** (`resilienceSettings.quotaShareConcurrencyLimit.enabled`, default
|
||||
on). Without a `max_concurrent` cap the behavior is unchanged.
|
||||
|
||||
> The quota-share routing gate (`selectQuotaShareTarget`, DRR + P2C) is itself
|
||||
> fail-open and only *deprioritizes* an at-cap connection — with a
|
||||
> fail-open and only _deprioritizes_ an at-cap connection — with a
|
||||
> single-connection pool it cannot hard-limit, so this semaphore is what actually
|
||||
> contains the flood.
|
||||
|
||||
@@ -230,11 +230,11 @@ Provider-specific stealth (JA3/JA4, CCH, obfuscation) is separately documented
|
||||
Além dos unit tests da lógica de resiliência, três testes exercitam o runtime sob
|
||||
estresse/falha real (todos integração/nightly — nenhum bloqueia PR):
|
||||
|
||||
| Teste | O quê | Rodar |
|
||||
|---|---|---|
|
||||
| Chaos | Fake-upstream node injeta latência/reset/timeout/503 reais; valida que o circuit breaker abre/recupera e `checkFallbackError` classifica 503 como fallback recuperável. | `RUN_CHAOS_INT=1 npm run test:chaos` |
|
||||
| Heap-growth | ~500 streams por `createSSEStream` sob `--expose-gc`; falha se o heap crescer além do teto (guarda OOM #3069). | `npm run test:heap` |
|
||||
| k6 soak | Carga sustentada contra `/api/monitoring/health`; thresholds p95/erro. | `k6 run tests/load/k6-soak.js` (nightly) |
|
||||
| Teste | O quê | Rodar |
|
||||
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
|
||||
| Chaos | Fake-upstream node injeta latência/reset/timeout/503 reais; valida que o circuit breaker abre/recupera e `checkFallbackError` classifica 503 como fallback recuperável. | `RUN_CHAOS_INT=1 npm run test:chaos` |
|
||||
| Heap-growth | ~500 streams por `createSSEStream` sob `--expose-gc`; falha se o heap crescer além do teto (guarda OOM #3069). | `npm run test:heap` |
|
||||
| k6 soak | Carga sustentada contra `/api/monitoring/health`; thresholds p95/erro. | `k6 run tests/load/k6-soak.js` (nightly) |
|
||||
|
||||
Orquestrados por `.github/workflows/nightly-resilience.yml` (cron + dispatch). No
|
||||
`test:integration` default, chaos e heap se auto-skipam (sem `RUN_CHAOS_INT`/`--expose-gc`).
|
||||
@@ -245,4 +245,4 @@ Orquestrados por `.github/workflows/nightly-resilience.yml` (cron + dispatch). N
|
||||
|
||||
- [Architecture Guide](./ARCHITECTURE.md) — System architecture and internals
|
||||
- [User Guide](../guides/USER_GUIDE.md) — Providers, combos, CLI integration
|
||||
- [Auto-Combo Engine](../routing/AUTO-COMBO.md) — 6-factor scoring, mode packs
|
||||
- [Auto-Combo Engine](../routing/AUTO-COMBO.md) — 12-factor scoring, mode packs
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Cluster Decisions"
|
||||
version: 3.8.32
|
||||
lastUpdated: 2026-06-20
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Cluster Decisions — Optional Sidecar Profiles
|
||||
@@ -31,15 +31,15 @@ The two profiles here are **scale-out options for deployments that hit the SQLit
|
||||
|
||||
**When to flip on:**
|
||||
|
||||
- >1M embeddings per deployment (sqlite-vec starts to slow at scale).
|
||||
- > 1M embeddings per deployment (sqlite-vec starts to slow at scale).
|
||||
- Multi-replica deployment that needs shared vector state across `omniroute-1/2/3`.
|
||||
- You already have an external Qdrant cluster (Qdrant Cloud, on-prem).
|
||||
|
||||
**What it adds:**
|
||||
|
||||
| Service | Image | Ports | Notes |
|
||||
| --------------- | ------------------------ | ----------- | ---------------------------------------------------------------------- |
|
||||
| `qdrant` | `qdrant/qdrant:v1.12.4` | `6333` HTTP | HNSW index; persistent volume `omniroute_qdrant_data` |
|
||||
| Service | Image | Ports | Notes |
|
||||
| -------- | ----------------------- | ----------- | ----------------------------------------------------- |
|
||||
| `qdrant` | `qdrant/qdrant:v1.12.4` | `6333` HTTP | HNSW index; persistent volume `omniroute_qdrant_data` |
|
||||
|
||||
**Activation:** flip `qdrantEnabled = true` in the Settings UI **or** set `QDRANT_HOST=qdrant` env. See [`src/lib/memory/qdrant.ts:60`](../../src/lib/memory/qdrant.ts) for the precedence rules (settings table → env var → default).
|
||||
|
||||
@@ -55,9 +55,9 @@ The two profiles here are **scale-out options for deployments that hit the SQLit
|
||||
|
||||
**What it adds:**
|
||||
|
||||
| Service | Image | Ports | Notes |
|
||||
| --------- | ------------------------------------------- | ---------- | -------------------------------------------------------------------------- |
|
||||
| `bifrost` | `ghcr.io/maximhq/bifrost:1.5.21` | `8080` | Go-based Tier-1 router; persistent logs volume `omniroute_bifrost_logs` |
|
||||
| Service | Image | Ports | Notes |
|
||||
| --------- | -------------------------------- | ------ | ----------------------------------------------------------------------- |
|
||||
| `bifrost` | `ghcr.io/maximhq/bifrost:1.5.21` | `8080` | Go-based Tier-1 router; persistent logs volume `omniroute_bifrost_logs` |
|
||||
|
||||
**Activation:** set `BIFROST_BASE_URL=http://bifrost:8080` in `.env.example`. The existing sidecar proxy route at [`src/app/api/v1/relay/chat/completions/bifrost/route.ts`](../../src/app/api/v1/relay/chat/completions/bifrost/route.ts) (added in PR #4381) will pick this up automatically.
|
||||
|
||||
@@ -67,15 +67,15 @@ The two profiles here are **scale-out options for deployments that hit the SQLit
|
||||
|
||||
The original issue thread floated a larger cluster rewrite. After auditing the actual workload shape, the following are **rejected** for the reasons given:
|
||||
|
||||
| Component | Verdict | Reason |
|
||||
| --------------- | --------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| **Dragonfly** | **DROP** | `redis:7-alpine` is already fine for the rate-limit workload at production scale; no ceiling to break. |
|
||||
| **NATS** | **DROP** | Each `omniroute` replica is a single Node.js process; no multi-process pub/sub workload exists. |
|
||||
| **PostgreSQL** | **DROP** | SQLite + sqlite-vec + FTS5 cover all 3 use cases; 97 migrations + Electron packaging block migration. |
|
||||
| **Neo4j** | **DROP** | Routing is a 5-table join; recursive CTE on SQLite is sufficient. |
|
||||
| **MinIO** | **DROP** | No multi-MB blob workload; images/audio are passthrough proxies. |
|
||||
| **pgvector / pg_ai / pg_textsearch** | **DROP** | Same SQLite-ceiling reason as PostgreSQL; pgvector ecosystem fragmented. |
|
||||
| **HAProxy / Envoy** | **DROP** | Caddy already does LB + TLS; both were explicitly rejected as Tier-1 routers (see `AGENTS.md`). |
|
||||
| Component | Verdict | Reason |
|
||||
| ------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| **Dragonfly** | **DROP** | `redis:7-alpine` is already fine for the rate-limit workload at production scale; no ceiling to break. |
|
||||
| **NATS** | **DROP** | Each `omniroute` replica is a single Node.js process; no multi-process pub/sub workload exists. |
|
||||
| **PostgreSQL** | **DROP** | SQLite + sqlite-vec + FTS5 cover all 3 use cases; 97 migrations + Electron packaging block migration. |
|
||||
| **Neo4j** | **DROP** | Routing is a 5-table join; recursive CTE on SQLite is sufficient. |
|
||||
| **MinIO** | **DROP** | No multi-MB blob workload; images/audio are passthrough proxies. |
|
||||
| **pgvector / pg_ai / pg_textsearch** | **DROP** | Same SQLite-ceiling reason as PostgreSQL; pgvector ecosystem fragmented. |
|
||||
| **HAProxy / Envoy** | **DROP** | Caddy already does LB + TLS; both were explicitly rejected as Tier-1 routers (see `AGENTS.md`). |
|
||||
|
||||
If a future use case proves out one of these, this doc is the place to amend.
|
||||
|
||||
@@ -88,14 +88,14 @@ If a future use case proves out one of these, this doc is the place to amend.
|
||||
|
||||
## Files changed in this PR
|
||||
|
||||
| File | Change |
|
||||
| ----------------------------------------------------------- | --------------------------------------------------------------------- |
|
||||
| `docker-compose.yml` | +30 lines: `memory` profile (Qdrant), `bifrost` profile (Bifrost), persistent volumes, healthchecks. |
|
||||
| `.env.example` | +24 lines: `QDRANT_*` (6 vars), `BIFROST_*` (4 vars). |
|
||||
| `docs/reference/ENVIRONMENT.md` | +6 rows in section 25 for the `QDRANT_*` env vars. |
|
||||
| `src/lib/memory/qdrant.ts` | +33 lines: env-var fallback chain (settings → env → default) for `QDRANT_HOST`/`QDRANT_PORT`/`QDRANT_API_KEY`/`QDRANT_COLLECTION`/`QDRANT_VECTOR_SIZE`/`QDRANT_HNSW_EF_CONSTRUCT`/`QDRANT_EMBEDDING_MODEL`. |
|
||||
| `src/lib/memory/__tests__/qdrant-wiring.test.ts` | +88 lines: 9 new test cases pinning the env-var fallback precedence. |
|
||||
| `docs/architecture/cluster-decisions.md` (this file) | NEW — decision record for the opt-in profiles. |
|
||||
| `AGENTS.md` | +1 line: pointer to this doc in the reference documentation table. |
|
||||
| File | Change |
|
||||
| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `docker-compose.yml` | +30 lines: `memory` profile (Qdrant), `bifrost` profile (Bifrost), persistent volumes, healthchecks. |
|
||||
| `.env.example` | +24 lines: `QDRANT_*` (6 vars), `BIFROST_*` (4 vars). |
|
||||
| `docs/reference/ENVIRONMENT.md` | +6 rows in section 25 for the `QDRANT_*` env vars. |
|
||||
| `src/lib/memory/qdrant.ts` | +33 lines: env-var fallback chain (settings → env → default) for `QDRANT_HOST`/`QDRANT_PORT`/`QDRANT_API_KEY`/`QDRANT_COLLECTION`/`QDRANT_VECTOR_SIZE`/`QDRANT_HNSW_EF_CONSTRUCT`/`QDRANT_EMBEDDING_MODEL`. |
|
||||
| `src/lib/memory/__tests__/qdrant-wiring.test.ts` | +88 lines: 9 new test cases pinning the env-var fallback precedence. |
|
||||
| `docs/architecture/cluster-decisions.md` (this file) | NEW — decision record for the opt-in profiles. |
|
||||
| `AGENTS.md` | +1 line: pointer to this doc in the reference documentation table. |
|
||||
|
||||
**Net touched code:** 4 production files (`docker-compose.yml`, `qdrant.ts`, `.env.example`, `ENVIRONMENT.md`), 1 test file (`qdrant-wiring.test.ts`), 2 doc files (`cluster-decisions.md`, `AGENTS.md`).
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "OmniRoute vs Alternatives"
|
||||
version: 3.8.2
|
||||
lastUpdated: 2026-05-15
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# OmniRoute vs Alternatives
|
||||
@@ -64,4 +64,4 @@ Objective feature comparison vs popular open-source AI routers.
|
||||
|
||||
---
|
||||
|
||||
_Last updated: 2026-05-15. Submit corrections via PR to keep this table accurate._
|
||||
_Last updated: 2026-06-28. Submit corrections via PR to keep this table accurate._
|
||||
|
||||
5
docs/comparison/meta.json
Normal file
5
docs/comparison/meta.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "Comparison",
|
||||
"description": "How OmniRoute compares to alternatives",
|
||||
"pages": ["OMNIROUTE_VS_ALTERNATIVES"]
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
---
|
||||
title: "Unified Compression Config Panel — Design"
|
||||
version: 3.8.32
|
||||
lastUpdated: 2026-06-20
|
||||
---
|
||||
|
||||
# Unified Compression Config Panel — Design
|
||||
|
||||
**Status:** approved direction (2026-06-20), pending spec review
|
||||
**Base branch:** `release/v3.8.31`
|
||||
**Goal:** Make `/dashboard/context/settings` the single management panel for the master
|
||||
on/off and each compression engine's on/off + level, deriving the default pipeline from
|
||||
those toggles. Keep the Combos page solely for *chaining* (ordered named pipelines) +
|
||||
selecting the globally-active profile, and the per-engine pages solely for *detailed*
|
||||
config. Plan for (but phase) multiple named profiles with an active selector and a
|
||||
per-request override header.
|
||||
|
||||
---
|
||||
|
||||
## 1. Background — current state (the problem)
|
||||
|
||||
Compression on/off + level is split across **two stores and several UIs**, with real
|
||||
duplication:
|
||||
|
||||
- **`/api/settings/compression`** (DB `key_value` ns=`compression`, via `src/lib/db/compression.ts`)
|
||||
holds: `enabled` (master), `defaultMode`, `autoTriggerTokens`, `cavemanConfig`,
|
||||
`cavemanOutputMode`, `rtkConfig`, `aggressive`, `ultra`, language config, etc.
|
||||
- **`/api/context/combos/default`** (the *default combo pipeline*) holds the per-engine
|
||||
`enabled` + config for the **structural** engines (lite, headroom, session-dedup, ccr,
|
||||
llmlingua, aggressive, ultra) as pipeline steps. The per-engine detail pages
|
||||
(`EngineConfigPage.tsx`) read/write here via `setEngineInDefaultCombo`.
|
||||
- **`compression_combos`** table holds *named* pipelines assigned to routing combos.
|
||||
|
||||
Concrete duplications (from the UI audit):
|
||||
- Caveman on/off in **3** places (TokenSaverCard, CompressionSettingsTab, CavemanContextPageClient).
|
||||
- Caveman intensity in **3**; RTK intensity in **2**; Caveman output mode in **2**.
|
||||
- `lite/session-dedup/headroom/ccr/llmlingua` on/off **absent** from the central settings —
|
||||
only on their per-engine page (which writes the *default combo*, not the central config).
|
||||
|
||||
So "is engine X on?" is answered inconsistently (central config for caveman/rtk; default
|
||||
combo for the structural engines), and the **Combos page edits the same default pipeline**
|
||||
that a central panel would — the conceptual overlap the redesign must remove.
|
||||
|
||||
Out of scope as a *config* surface: `dashboard/compression/studio` is the Compression
|
||||
Studio (waterfall/cockpit analytics), not toggles — untouched.
|
||||
|
||||
Engines (the 10 stackable units): `lite, caveman, aggressive, ultra, rtk, headroom,
|
||||
session-dedup, ccr, llmlingua` (registered `CompressionEngine`s) + `mcpAccessibility`
|
||||
(separate path: compresses MCP tool-result outputs, not the chat pipeline). Each engine's
|
||||
`stackPriority` defines automatic ordering (session-dedup 3, ccr 4, lite 5, rtk 10,
|
||||
headroom 15, caveman 20, aggressive 30, llmlingua 35, ultra 40).
|
||||
|
||||
---
|
||||
|
||||
## 2. Design principles — the boundary
|
||||
|
||||
| Surface | Concept | Owns | Never does |
|
||||
|---|---|---|---|
|
||||
| **Panel** (`context/settings`) | "My **Default**: what is on + level" | master on/off; per-engine on/off + level | ordering/chaining; per-route assignment |
|
||||
| **Combos** (`context/combos`, menu #2) | "Named **profiles**: chaining + which is active + per-route assignment" | create/edit ordered named pipelines; pick the globally-active profile; assign to routing combos | engine on/off (inherits the active profile's membership) |
|
||||
| **Per-engine pages** (menu, after combos) | "Deep config of one engine" | filters, rules, language packs, thresholds | on/off; level (those live in the panel) |
|
||||
|
||||
**No duplication rule:** the **Panel owns the Default profile** (membership + level, order
|
||||
auto-derived by `stackPriority`). A **Combo is a *named alternative* profile** (membership
|
||||
+ level + *explicit order*). Different scopes (the one default vs named alternatives) — not
|
||||
the same object edited twice. The editable "default combo" store is **removed**; the
|
||||
default pipeline becomes **derived** from the panel.
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecture
|
||||
|
||||
### 3.1 Resolution model (per request, most-specific wins)
|
||||
|
||||
```
|
||||
x-omniroute-compression header (per-request override)
|
||||
→ routing-combo override (comboOverrides[comboId]) (per-route)
|
||||
→ active profile (activeComboId: "default" | <comboId>) (global)
|
||||
→ Default = derived from panel engines map
|
||||
→ off (master disabled, or zero engines on)
|
||||
```
|
||||
|
||||
A single `resolveCompressionPlan(config, { comboId, header })` produces the effective
|
||||
`{ mode, stackedPipeline }` fed to the existing `applyCompressionAsync`. It supersedes
|
||||
today's `getEffectiveMode` (which only does combo-override → autoTrigger → defaultMode).
|
||||
|
||||
### 3.2 Data model (Model A — single source + derived pipeline)
|
||||
|
||||
**CompressionConfig gains an engines map** (the single source for the Default's on/off +
|
||||
level), replacing the editable default-combo store:
|
||||
|
||||
```ts
|
||||
interface EngineToggle {
|
||||
enabled: boolean;
|
||||
level?: string; // caveman/cavemanOutput: lite|full|ultra ; rtk: minimal|standard|aggressive ; others: undefined
|
||||
}
|
||||
interface CompressionConfig {
|
||||
enabled: boolean; // master
|
||||
engines: Record<CompressionEngineId, EngineToggle>; // NEW — the Default profile
|
||||
activeComboId: string | null; // NEW — null/"default" = derived default; else a compression_combos id
|
||||
autoTriggerTokens: number;
|
||||
autoTriggerMode?: CompressionMode; // kept (auto-trigger still selects a profile/mode on large prompts)
|
||||
preserveSystemPrompt: boolean;
|
||||
comboOverrides: Record<string, CompressionMode>; // existing per-routing-combo override
|
||||
// detailed per-engine config keeps living in its existing sub-objects
|
||||
// (cavemanConfig, rtkConfig, aggressive, ultra, …) — edited by the per-engine pages.
|
||||
}
|
||||
```
|
||||
|
||||
**Deriving the Default pipeline** from `engines` (pure function `deriveDefaultPlan`):
|
||||
- master `enabled === false` → `off`.
|
||||
- 0 engines enabled → `off`.
|
||||
- exactly 1 enabled and it is a single-mode engine (`lite|caveman|aggressive|ultra|rtk`) →
|
||||
that mode (single path — reuses today's `applyCompression(mode)`).
|
||||
- otherwise → `stacked` with the enabled engines in `stackPriority` order, each step's
|
||||
`intensity` from its `level` (the global `stackedPipeline` already accepts all 9 engines
|
||||
after the B-PIPELINE-DIVERGENCE fix).
|
||||
|
||||
The **stored `defaultMode` field** and the editable default combo
|
||||
(`/api/context/combos/default`, `setEngineInDefaultCombo`) are **removed**; the default is
|
||||
derived from `engines`. (The `CompressionMode` *type* persists — `comboOverrides`,
|
||||
`autoTriggerMode`, and the resolver's output still use it; only the stored `defaultMode`
|
||||
field is dropped.) A DB migration backfills `engines` from the current `defaultMode` +
|
||||
default-combo steps + caveman/rtk config so existing installs keep their behavior.
|
||||
|
||||
**Named combos** (`compression_combos`, existing table): N ordered pipelines. `activeComboId`
|
||||
selects which profile is globally active (`Default` or a named combo). Per-routing-combo
|
||||
assignment stays in `comboOverrides`.
|
||||
|
||||
`mcpAccessibility` keeps its own store (`/api/settings/compression/mcp-accessibility`,
|
||||
migration 056) — surfaced in the panel as a row that writes there, with a scope note.
|
||||
|
||||
### 3.3 The per-request header
|
||||
|
||||
`x-omniroute-compression: <value>` (mirrors the `x-omniroute-no-memory`/`no-cache` pattern,
|
||||
PR #4290; parsed in the request pipeline alongside the other omniroute headers). Values:
|
||||
- `off` → no compression for this request.
|
||||
- `default` → the derived Default profile.
|
||||
- `<combo-name|id>` → that named combo.
|
||||
- `engine:<id>` → a single engine (if that engine is enabled in the Default), e.g. `engine:rtk`.
|
||||
|
||||
Invalid/unknown value → ignored (falls through to the normal resolution); never errors the
|
||||
request. Header parsing + validation has a unit test asserting each form and the
|
||||
fall-through.
|
||||
|
||||
---
|
||||
|
||||
## 4. Screens
|
||||
|
||||
### 4.1 Panel — `context/settings` (engine grid)
|
||||
|
||||
A single client component (replacing the scattered `CompressionSettingsTab` +
|
||||
`CompressionTokenSaverCard` toggles):
|
||||
- **Master** on/off at top.
|
||||
- **Engine grid** (one row per engine, ordered by `stackPriority` so the row order mirrors
|
||||
run order): `[engine name + short desc] [on/off toggle] [level selector if applicable]
|
||||
[→ detail page]`. Level selector appears only for engines with levels
|
||||
(caveman lite|full|ultra, rtk minimal|standard|aggressive; caveman output mode as its own row).
|
||||
- **General** (auto-trigger tokens, preserve-system-prompt) below the grid.
|
||||
- Reads/writes the `engines` map + master via `GET/PUT /api/settings/compression` (single
|
||||
endpoint). The displayed default pipeline (derived) is shown read-only ("runs: rtk →
|
||||
caveman → …") so the user sees the effect without editing order here.
|
||||
|
||||
### 4.2 Combos — `context/combos` (menu #2)
|
||||
|
||||
- List of named combos; create/edit an **ordered** pipeline (drag/reorder + per-step level)
|
||||
— the only place for explicit chaining.
|
||||
- **Active profile selector**: `Default (panel)` | `<combo>` → writes `activeComboId`.
|
||||
- Assign a combo to a routing combo (existing `comboOverrides`).
|
||||
- Reuses the existing `CompressionCombosPageClient` / `comboFlowModel`; the `CompressionHub`
|
||||
"master mode selector" is removed (mode is now derived; the Hub becomes a read-only
|
||||
overview or is folded into the panel).
|
||||
|
||||
### 4.3 Per-engine pages (menu, after combos)
|
||||
|
||||
`EngineConfigPage` + the caveman/rtk custom pages: **lose** the on/off + level controls
|
||||
(moved to the panel) and keep only **detailed** config (filters, rules, language packs,
|
||||
thresholds, preview). They **stop writing** `/api/context/combos/default`; detailed config
|
||||
writes to its own sub-object in `/api/settings/compression` (or the existing
|
||||
caveman/rtk facade routes, which already proxy to it).
|
||||
|
||||
### 4.4 Navigation
|
||||
|
||||
`COMPRESSION_CONTEXT_GROUP` (`src/shared/constants/sidebarVisibility.ts`) reordered:
|
||||
**Settings (panel) → Combos → per-engine pages → Studio (analytics)**.
|
||||
|
||||
---
|
||||
|
||||
## 5. Consolidation / migration
|
||||
|
||||
- `CompressionTokenSaverCard` quick toggles → **absorbed into the panel; the card is removed**.
|
||||
- Duplicate caveman/rtk on/off + intensity in `CompressionSettingsTab` → removed.
|
||||
- `EngineConfigPage` → on/off+level removed; stops writing the default combo.
|
||||
- DB migration: backfill `engines` map + `activeComboId="default"` from current state
|
||||
(defaultMode + default-combo steps + caveman/rtk/ultra/aggressive enabled), so live
|
||||
installs preserve behavior. The editable default-combo route (`PUT /api/context/combos/default`)
|
||||
becomes a **read-only shim for one release** (returns the derived default; rejects writes
|
||||
with a deprecation note), then is removed.
|
||||
|
||||
---
|
||||
|
||||
## 6. Phasing
|
||||
|
||||
Each phase is its own implementation plan + PR, independently shippable and TDD'd
|
||||
(Hard Rule #18). `writing-plans` will author **Phase 1 first**; Phases 2–3 get their own
|
||||
plans later.
|
||||
|
||||
- **Phase 1 (core consolidation):** the `engines` map + `deriveDefaultPlan` + migration;
|
||||
the engine-grid panel; remove scattered/duplicate toggles; per-engine pages lose on/off;
|
||||
menu reorder. Delivers the single-panel goal. *No behavior change for existing installs
|
||||
(migration backfills).*
|
||||
- **Phase 2 (profiles):** multiple named combos + the `activeComboId` active-profile selector.
|
||||
- **Phase 3 (header):** the `x-omniroute-compression` per-request override.
|
||||
|
||||
Phases 2–3 reuse the Phase-1 resolution model (`resolveCompressionPlan`), which is built
|
||||
header/active-aware from the start so later phases only wire UI + header parsing.
|
||||
|
||||
---
|
||||
|
||||
## 7. Testing
|
||||
|
||||
- **Unit:** `deriveDefaultPlan` (every engines-map shape → expected mode/pipeline);
|
||||
migration backfill (old config → equivalent engines map); `resolveCompressionPlan`
|
||||
precedence (header > comboOverride > active > default > off); header parsing (each form +
|
||||
fall-through); panel reducer (toggle/level edits → config patch).
|
||||
- **Component (vitest):** the panel renders all engines, toggles persist, derived pipeline
|
||||
preview updates.
|
||||
- **Integration:** a config with engines `{rtk,caveman}` on → `applyCompressionAsync` runs the
|
||||
derived stacked pipeline; single engine on → single-mode path; equivalence with the old
|
||||
defaultMode behavior for the same logical config.
|
||||
- Both runners green (`test:unit` + `test:vitest`); typecheck:core clean; lint 0 errors.
|
||||
|
||||
---
|
||||
|
||||
## 8. Non-goals (YAGNI)
|
||||
|
||||
- No new compression engines in this work (the model just makes adding them trivial later).
|
||||
- No change to the engines' internal algorithms (the recent fixes are separate).
|
||||
- The Compression Studio (analytics) is not restructured.
|
||||
- Phases 2–3 UI polish (combo templates, sharing) is out of scope.
|
||||
@@ -1,495 +0,0 @@
|
||||
---
|
||||
title: "Unified Compression Config Panel — Phase 1 Implementation Plan"
|
||||
version: 3.8.32
|
||||
lastUpdated: 2026-06-20
|
||||
---
|
||||
|
||||
# Unified Compression Config Panel — Phase 1 Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make `/dashboard/context/settings` the single source for the master + per-engine on/off + level, with the default compression pipeline DERIVED from those toggles, removing the scattered/duplicate toggles — with zero behavior change for existing installs (a migration backfills).
|
||||
|
||||
**Architecture:** Add an `engines` map (+`activeComboId`) to `CompressionConfig` as the single source. A pure `deriveDefaultPlan(engines)` turns it into `{mode, stackedPipeline}`; a pure `resolveCompressionPlan(config, ctx)` applies precedence (header→combo-override→active-profile→derived-default→off) and feeds the existing `applyCompressionAsync`. A DB migration backfills the map. The engine-grid panel reads/writes the map via the single `/api/settings/compression` endpoint; per-engine pages lose on/off+level; the editable default-combo route becomes a read-only shim.
|
||||
|
||||
**Tech Stack:** TypeScript, Next.js 16 App Router, Zod, SQLite (better-sqlite3), Node test runner + Vitest (component), React.
|
||||
|
||||
**Base:** worktree `feat/compression-config-panel-v3831` off `release/v3.8.31`. Spec: `docs/compression/2026-06-20-unified-compression-config-panel-design.md`.
|
||||
|
||||
**Conventions:** unit tests run `node --import tsx/esm --test tests/unit/<f>.test.ts`; component tests run `npm run test:vitest`. Each task ends by running the FULL compression suite (`node --import tsx/esm --test tests/unit/compression/*.test.ts`) + `npm run typecheck:core` before commit. Never `--no-verify`.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
**Create:**
|
||||
- `open-sse/services/compression/engineCatalog.ts` — pure metadata: per-engine `{ id, label, stackPriority, levels?, isSingleMode }`. One source of truth for "which engines exist, which have levels, which can be a standalone mode".
|
||||
- `open-sse/services/compression/deriveDefaultPlan.ts` — pure: `engines` map → `{ mode, stackedPipeline }`.
|
||||
- `open-sse/services/compression/resolveCompressionPlan.ts` — pure: precedence resolver.
|
||||
- `src/lib/db/migrations/102_compression_engines_map.sql` — backfill `engines` + `activeComboId`.
|
||||
- `src/app/(dashboard)/dashboard/context/settings/CompressionPanel.tsx` — the engine-grid panel.
|
||||
- `tests/unit/compression/engine-catalog.test.ts`, `derive-default-plan.test.ts`, `resolve-compression-plan.test.ts`, `compression-engines-map-migration.test.ts`.
|
||||
- `tests/unit/ui/compressionPanel.test.tsx` (vitest).
|
||||
|
||||
**Modify:**
|
||||
- `open-sse/services/compression/types.ts` — add `EngineToggle`, `CompressionConfig.engines`, `activeComboId`; keep `CompressionMode` type; drop stored `defaultMode` usage (derive).
|
||||
- `src/lib/db/compression.ts` — normalize/persist `engines` + `activeComboId`.
|
||||
- `open-sse/services/compression/strategySelector.ts` — `selectCompressionStrategy`/`getEffectiveMode` delegate to `resolveCompressionPlan`.
|
||||
- `open-sse/handlers/chatCore.ts` — call `resolveCompressionPlan`.
|
||||
- `src/app/api/settings/compression/route.ts` — accept/return `engines` + `activeComboId`.
|
||||
- `src/app/api/context/combos/default/route.ts` — read-only shim (reject writes).
|
||||
- `src/app/(dashboard)/dashboard/context/settings/page.tsx` — render `CompressionPanel` (not the old tab).
|
||||
- `src/shared/components/compression/EngineConfigPage.tsx` + caveman/rtk client pages — remove on/off+level; stop writing default combo.
|
||||
- `src/shared/constants/sidebarVisibility.ts` — reorder `COMPRESSION_CONTEXT_GROUP`.
|
||||
|
||||
**Engine reference (`stackPriority`, levels, single-mode):**
|
||||
`session-dedup`(3,—,no) `ccr`(4,—,no) `lite`(5,—,yes) `rtk`(10,minimal|standard|aggressive,yes) `headroom`(15,—,no) `caveman`(20,lite|full|ultra,yes) `aggressive`(30,—,yes) `llmlingua`(35,—,no) `ultra`(40,—,yes). Plus `cavemanOutput`(intensity lite|full|ultra, separate from input caveman) and `mcpAccessibility`(no level, separate store).
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Engine catalog (single source of engine metadata)
|
||||
|
||||
**Files:**
|
||||
- Create: `open-sse/services/compression/engineCatalog.ts`
|
||||
- Test: `tests/unit/compression/engine-catalog.test.ts`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```ts
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { ENGINE_CATALOG, engineMeta, ENGINE_IDS } from "@omniroute/open-sse/services/compression/engineCatalog.ts";
|
||||
|
||||
test("catalog lists every engine with stackPriority", () => {
|
||||
for (const id of ["session-dedup","ccr","lite","rtk","headroom","caveman","aggressive","llmlingua","ultra"]) {
|
||||
assert.ok(engineMeta(id), `${id} present`);
|
||||
assert.equal(typeof engineMeta(id).stackPriority, "number");
|
||||
}
|
||||
});
|
||||
test("levels + single-mode flags are correct", () => {
|
||||
assert.deepEqual(engineMeta("rtk").levels, ["minimal","standard","aggressive"]);
|
||||
assert.deepEqual(engineMeta("caveman").levels, ["lite","full","ultra"]);
|
||||
assert.equal(engineMeta("headroom").levels, undefined);
|
||||
assert.equal(engineMeta("caveman").isSingleMode, true);
|
||||
assert.equal(engineMeta("headroom").isSingleMode, false);
|
||||
});
|
||||
test("ENGINE_IDS is ordered by stackPriority", () => {
|
||||
const ps = ENGINE_IDS.map((id) => engineMeta(id).stackPriority);
|
||||
assert.deepEqual(ps, [...ps].sort((a,b)=>a-b));
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run → FAIL** (`node --import tsx/esm --test tests/unit/compression/engine-catalog.test.ts`) — module not found.
|
||||
|
||||
- [ ] **Step 3: Implement** `engineCatalog.ts`:
|
||||
|
||||
```ts
|
||||
export interface EngineMeta {
|
||||
id: string;
|
||||
label: string;
|
||||
stackPriority: number;
|
||||
levels?: string[]; // intensity options; undefined = no level selector
|
||||
isSingleMode: boolean; // can be the effective mode when it is the only engine on
|
||||
description: string;
|
||||
}
|
||||
export const ENGINE_CATALOG: Record<string, EngineMeta> = {
|
||||
"session-dedup": { id:"session-dedup", label:"Session Dedup", stackPriority:3, isSingleMode:false, description:"Cross-turn block deduplication." },
|
||||
ccr: { id:"ccr", label:"CCR (Retrieval)", stackPriority:4, isSingleMode:false, description:"Content-addressed retrieval markers." },
|
||||
lite: { id:"lite", label:"Lite", stackPriority:5, isSingleMode:true, description:"Whitespace/format cleanup." },
|
||||
rtk: { id:"rtk", label:"RTK", stackPriority:10, levels:["minimal","standard","aggressive"], isSingleMode:true, description:"Command-output filtering." },
|
||||
headroom: { id:"headroom", label:"Headroom", stackPriority:15, isSingleMode:false, description:"Tabular JSON compaction." },
|
||||
caveman: { id:"caveman", label:"Caveman", stackPriority:20, levels:["lite","full","ultra"], isSingleMode:true, description:"Rule-based prose compression." },
|
||||
aggressive: { id:"aggressive", label:"Aggressive", stackPriority:30, isSingleMode:true, description:"Summarize + age old turns." },
|
||||
llmlingua: { id:"llmlingua", label:"LLMLingua (SLM)", stackPriority:35, isSingleMode:false, description:"Semantic pruning (ONNX)." },
|
||||
ultra: { id:"ultra", label:"Ultra", stackPriority:40, isSingleMode:true, description:"Heuristic token pruning (+ optional SLM)." },
|
||||
};
|
||||
export const ENGINE_IDS: string[] = Object.values(ENGINE_CATALOG).sort((a,b)=>a.stackPriority-b.stackPriority).map((e)=>e.id);
|
||||
export function engineMeta(id: string): EngineMeta { return ENGINE_CATALOG[id]; }
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run → PASS.**
|
||||
- [ ] **Step 5: Commit** `git add open-sse/services/compression/engineCatalog.ts tests/unit/compression/engine-catalog.test.ts && git commit -m "feat(compression): engine catalog metadata (levels, single-mode, order)"`
|
||||
|
||||
---
|
||||
|
||||
## Task 2: `EngineToggle` + `engines` map + `activeComboId` on the config type
|
||||
|
||||
**Files:**
|
||||
- Modify: `open-sse/services/compression/types.ts` (the `CompressionConfig` interface + `DEFAULT_COMPRESSION_CONFIG`)
|
||||
- Test: `tests/unit/compression/engine-catalog.test.ts` (extend) — assert the default config shape.
|
||||
|
||||
- [ ] **Step 1: Add the test** (append):
|
||||
|
||||
```ts
|
||||
import { DEFAULT_COMPRESSION_CONFIG } from "@omniroute/open-sse/services/compression/types.ts";
|
||||
test("default config has an engines map + activeComboId", () => {
|
||||
assert.equal(typeof DEFAULT_COMPRESSION_CONFIG.engines, "object");
|
||||
assert.equal(DEFAULT_COMPRESSION_CONFIG.activeComboId, null);
|
||||
// default-off: every engine disabled by default (opt-in preserved)
|
||||
for (const id of ENGINE_IDS) assert.equal(DEFAULT_COMPRESSION_CONFIG.engines[id]?.enabled, false);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run → FAIL.**
|
||||
- [ ] **Step 3: Implement** in `types.ts`: add `export interface EngineToggle { enabled: boolean; level?: string }`; add to `CompressionConfig`: `engines: Record<string, EngineToggle>;` and `activeComboId: string | null;`. In `DEFAULT_COMPRESSION_CONFIG` add `engines: Object.fromEntries(ENGINE_IDS.map((id)=>[id,{enabled:false}]))` (import `ENGINE_IDS`) and `activeComboId: null`. Keep `defaultMode` field for now (removed in Task 9 once derive is wired) to avoid breaking compilation.
|
||||
|
||||
- [ ] **Step 4: Run → PASS + `npm run typecheck:core`.**
|
||||
- [ ] **Step 5: Commit** `... -m "feat(compression): add engines map + activeComboId to CompressionConfig"`
|
||||
|
||||
---
|
||||
|
||||
## Task 3: `deriveDefaultPlan` (the heart — engines map → mode/pipeline)
|
||||
|
||||
**Files:**
|
||||
- Create: `open-sse/services/compression/deriveDefaultPlan.ts`
|
||||
- Test: `tests/unit/compression/derive-default-plan.test.ts`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```ts
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { deriveDefaultPlan } from "@omniroute/open-sse/services/compression/deriveDefaultPlan.ts";
|
||||
|
||||
const on = (level) => ({ enabled: true, ...(level?{level}:{}) });
|
||||
test("master off / empty / none-on => off", () => {
|
||||
assert.deepEqual(deriveDefaultPlan({}, false), { mode:"off", stackedPipeline:[] });
|
||||
assert.deepEqual(deriveDefaultPlan({}, true), { mode:"off", stackedPipeline:[] });
|
||||
assert.deepEqual(deriveDefaultPlan({ rtk:{enabled:false} }, true), { mode:"off", stackedPipeline:[] });
|
||||
});
|
||||
test("exactly one single-mode engine => that mode", () => {
|
||||
assert.deepEqual(deriveDefaultPlan({ caveman: on("full") }, true), { mode:"standard", stackedPipeline:[] });
|
||||
assert.deepEqual(deriveDefaultPlan({ rtk: on("minimal") }, true), { mode:"rtk", stackedPipeline:[] });
|
||||
assert.deepEqual(deriveDefaultPlan({ lite: on() }, true), { mode:"lite", stackedPipeline:[] });
|
||||
});
|
||||
test("one non-single-mode engine => stacked with that engine", () => {
|
||||
const p = deriveDefaultPlan({ headroom: on() }, true);
|
||||
assert.equal(p.mode, "stacked");
|
||||
assert.deepEqual(p.stackedPipeline, [{ engine:"headroom" }]);
|
||||
});
|
||||
test("multiple engines => stacked in stackPriority order, levels as intensity", () => {
|
||||
const p = deriveDefaultPlan({ caveman: on("full"), rtk: on("standard"), headroom: on() }, true);
|
||||
assert.equal(p.mode, "stacked");
|
||||
assert.deepEqual(p.stackedPipeline, [
|
||||
{ engine:"rtk", intensity:"standard" }, // pri 10
|
||||
{ engine:"headroom" }, // pri 15
|
||||
{ engine:"caveman", intensity:"full" }, // pri 20
|
||||
]);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run → FAIL.**
|
||||
- [ ] **Step 3: Implement** `deriveDefaultPlan.ts`:
|
||||
|
||||
```ts
|
||||
import { ENGINE_CATALOG, engineMeta } from "./engineCatalog.ts";
|
||||
import type { EngineToggle } from "./types.ts";
|
||||
|
||||
const SINGLE_MODE_OF: Record<string,string> = { lite:"lite", caveman:"standard", aggressive:"aggressive", ultra:"ultra", rtk:"rtk" };
|
||||
|
||||
export interface DerivedPlan { mode: string; stackedPipeline: Array<{ engine: string; intensity?: string }>; }
|
||||
|
||||
export function deriveDefaultPlan(engines: Record<string, EngineToggle>, masterEnabled: boolean): DerivedPlan {
|
||||
if (!masterEnabled) return { mode:"off", stackedPipeline:[] };
|
||||
const onIds = Object.keys(ENGINE_CATALOG).filter((id) => engines[id]?.enabled === true);
|
||||
if (onIds.length === 0) return { mode:"off", stackedPipeline:[] };
|
||||
if (onIds.length === 1 && engineMeta(onIds[0]).isSingleMode) {
|
||||
return { mode: SINGLE_MODE_OF[onIds[0]], stackedPipeline:[] };
|
||||
}
|
||||
const ordered = onIds.sort((a,b)=>engineMeta(a).stackPriority-engineMeta(b).stackPriority);
|
||||
const stackedPipeline = ordered.map((id) => {
|
||||
const level = engines[id]?.level;
|
||||
return level ? { engine:id, intensity:level } : { engine:id };
|
||||
});
|
||||
return { mode:"stacked", stackedPipeline };
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run → PASS.**
|
||||
- [ ] **Step 5: Commit** `... -m "feat(compression): deriveDefaultPlan (engines map -> mode/pipeline)"`
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Migration 102 — backfill `engines` + `activeComboId`
|
||||
|
||||
**Files:**
|
||||
- Create: `src/lib/db/migrations/102_compression_engines_map.sql`
|
||||
- Test: `tests/unit/compression/compression-engines-map-migration.test.ts`
|
||||
|
||||
- [ ] **Step 1: Write the failing test** (uses `resetDbInstance()` + closes handles in `test.after`, per repo rule):
|
||||
|
||||
```ts
|
||||
import { test, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { getDbInstance, resetDbInstance } from "@/lib/db/core.ts";
|
||||
import { getCompressionSettings } from "@/lib/db/compression.ts";
|
||||
|
||||
after(() => resetDbInstance());
|
||||
test("migration backfills engines map from prior defaultMode + default combo", () => {
|
||||
resetDbInstance();
|
||||
const db = getDbInstance(); // runs migrations incl. 102
|
||||
// simulate a pre-102 install: master on, defaultMode 'standard', caveman enabled
|
||||
db.prepare("INSERT OR REPLACE INTO key_value(namespace,key,value) VALUES('compression','enabled','true')").run();
|
||||
db.prepare("INSERT OR REPLACE INTO key_value(namespace,key,value) VALUES('compression','defaultMode','\"standard\"')").run();
|
||||
db.prepare("INSERT OR REPLACE INTO key_value(namespace,key,value) VALUES('compression','cavemanConfig','{\"enabled\":true}')").run();
|
||||
const cfg = getCompressionSettings();
|
||||
assert.equal(cfg.engines.caveman.enabled, true);
|
||||
assert.equal(cfg.activeComboId, null);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run → FAIL.**
|
||||
- [ ] **Step 3: Implement** the SQL migration (idempotent; the row backfill of derived `engines` is done in `normalizeCompressionSettings` read-path — the SQL only seeds `activeComboId` default and a marker). Migration `102_compression_engines_map.sql`:
|
||||
|
||||
```sql
|
||||
-- Phase 1 of the unified compression panel: the engines map + activeComboId become the
|
||||
-- single source. The engines map is DERIVED on read (normalizeCompressionSettings) from the
|
||||
-- legacy defaultMode + default-combo steps + caveman/rtk/ultra/aggressive config, so existing
|
||||
-- installs keep their behavior. Here we only ensure activeComboId defaults to NULL ("default").
|
||||
INSERT OR IGNORE INTO key_value (namespace, key, value) VALUES ('compression', 'activeComboId', 'null');
|
||||
```
|
||||
|
||||
(The read-path derivation in Task 5 is what makes `getCompressionSettings().engines` correct; the migration just guarantees `activeComboId` exists.)
|
||||
|
||||
- [ ] **Step 4: Run → PASS** (after Task 5's normalize is in — if running 4 before 5, expect the engines assertion to fail; do Task 5 then re-run). Commit after Task 5.
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Persist + normalize `engines` / `activeComboId` (read-path derivation)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/lib/db/compression.ts` (`getCompressionSettings`/`normalizeCompressionSettings`/`updateCompressionSettings`)
|
||||
- Test: reuse Task 4's migration test + add a round-trip test in the same file.
|
||||
|
||||
- [ ] **Step 1: Add round-trip test**:
|
||||
|
||||
```ts
|
||||
import { updateCompressionSettings } from "@/lib/db/compression.ts";
|
||||
test("engines map persists round-trip + activeComboId", () => {
|
||||
resetDbInstance(); getDbInstance();
|
||||
updateCompressionSettings({ enabled:true, engines:{ rtk:{enabled:true,level:"standard"}, caveman:{enabled:true,level:"full"} }, activeComboId:null });
|
||||
const cfg = getCompressionSettings();
|
||||
assert.equal(cfg.engines.rtk.enabled, true);
|
||||
assert.equal(cfg.engines.rtk.level, "standard");
|
||||
assert.equal(cfg.engines.caveman.level, "full");
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run → FAIL.**
|
||||
- [ ] **Step 3: Implement** in `compression.ts`:
|
||||
- In `normalizeCompressionSettings`: read stored `engines` if present; ELSE derive it from legacy fields — `engines[id].enabled` from: caveman/rtk/ultra/aggressive `*.enabled`; structural engines from the default-combo steps (read the default combo); single-modes from `defaultMode`. Levels from `cavemanConfig.intensity`/`rtkConfig.intensity`. Read `activeComboId` (default null).
|
||||
- In `updateCompressionSettings`: accept `engines` (validate each value `{enabled:boolean, level?:string}`) + `activeComboId` and persist as `key_value` rows (`engines` as one JSON row).
|
||||
- Add a Zod sub-schema `engineToggleSchema = z.object({ enabled: z.boolean(), level: z.string().optional() })` and `engines: z.record(engineToggleSchema).optional()`, `activeComboId: z.string().nullable().optional()`.
|
||||
|
||||
- [ ] **Step 4: Run → PASS** (Task 4 + Task 5 tests). `npm run typecheck:core`.
|
||||
- [ ] **Step 5: Commit** `git add src/lib/db/migrations/102_compression_engines_map.sql src/lib/db/compression.ts tests/unit/compression/compression-engines-map-migration.test.ts && git commit -m "feat(compression): persist+backfill engines map and activeComboId (migration 102)"`
|
||||
|
||||
---
|
||||
|
||||
## Task 6: `resolveCompressionPlan` (precedence resolver)
|
||||
|
||||
**Files:**
|
||||
- Create: `open-sse/services/compression/resolveCompressionPlan.ts`
|
||||
- Test: `tests/unit/compression/resolve-compression-plan.test.ts`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```ts
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { resolveCompressionPlan } from "@omniroute/open-sse/services/compression/resolveCompressionPlan.ts";
|
||||
|
||||
const base = { enabled:true, engines:{ caveman:{enabled:true,level:"full"} }, activeComboId:null, comboOverrides:{} };
|
||||
test("derived default when no override/active/header", () => {
|
||||
assert.deepEqual(resolveCompressionPlan(base, {}), { mode:"standard", stackedPipeline:[] });
|
||||
});
|
||||
test("routing-combo override wins over default", () => {
|
||||
const cfg = { ...base, comboOverrides:{ cmb:"aggressive" } };
|
||||
assert.equal(resolveCompressionPlan(cfg, { comboId:"cmb" }).mode, "aggressive");
|
||||
});
|
||||
test("active named combo wins over default (Phase 2 wiring uses combos table; here pass it in)", () => {
|
||||
const cfg = { ...base, activeComboId:"c1" };
|
||||
const combos = { c1: [{ engine:"rtk", intensity:"standard" }] };
|
||||
const plan = resolveCompressionPlan(cfg, { combos });
|
||||
assert.equal(plan.mode, "stacked");
|
||||
assert.deepEqual(plan.stackedPipeline, [{ engine:"rtk", intensity:"standard" }]);
|
||||
});
|
||||
test("master off => off regardless", () => {
|
||||
assert.equal(resolveCompressionPlan({ ...base, enabled:false }, {}).mode, "off");
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run → FAIL.**
|
||||
- [ ] **Step 3: Implement** `resolveCompressionPlan.ts`:
|
||||
|
||||
```ts
|
||||
import { deriveDefaultPlan, type DerivedPlan } from "./deriveDefaultPlan.ts";
|
||||
|
||||
export interface ResolveCtx {
|
||||
comboId?: string | null;
|
||||
header?: string | null; // x-omniroute-compression (Phase 3 parses+passes; Phase 1 callers pass undefined)
|
||||
combos?: Record<string, Array<{ engine:string; intensity?:string }>>; // named combo pipelines by id
|
||||
}
|
||||
export function resolveCompressionPlan(config: any, ctx: ResolveCtx): DerivedPlan {
|
||||
if (config?.enabled === false) return { mode:"off", stackedPipeline:[] };
|
||||
// 1. header (Phase 3 supplies parsed value; here it composes if present)
|
||||
if (ctx.header) {
|
||||
if (ctx.header === "off") return { mode:"off", stackedPipeline:[] };
|
||||
if (ctx.header !== "default") {
|
||||
const fromHeader = headerToPlan(ctx.header, config, ctx);
|
||||
if (fromHeader) return fromHeader; // unknown => fall through
|
||||
}
|
||||
}
|
||||
// 2. routing-combo override
|
||||
const ov = ctx.comboId ? config?.comboOverrides?.[ctx.comboId] : undefined;
|
||||
if (ov) return modeToPlan(ov, config);
|
||||
// 3. active named combo
|
||||
if (config?.activeComboId && ctx.combos?.[config.activeComboId]) {
|
||||
return { mode:"stacked", stackedPipeline: ctx.combos[config.activeComboId] };
|
||||
}
|
||||
// 4. derived default
|
||||
return deriveDefaultPlan(config?.engines ?? {}, config?.enabled !== false);
|
||||
}
|
||||
function modeToPlan(mode: string, config: any): DerivedPlan {
|
||||
return mode === "stacked"
|
||||
? { mode:"stacked", stackedPipeline: config?.stackedPipeline ?? [] }
|
||||
: { mode, stackedPipeline:[] };
|
||||
}
|
||||
function headerToPlan(h: string, config: any, ctx: ResolveCtx): DerivedPlan | null {
|
||||
if (h.startsWith("engine:")) { const id = h.slice(7); return config?.engines?.[id]?.enabled ? deriveDefaultPlan({ [id]: config.engines[id] }, true) : null; }
|
||||
if (ctx.combos?.[h]) return { mode:"stacked", stackedPipeline: ctx.combos[h] };
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run → PASS.**
|
||||
- [ ] **Step 5: Commit** `... -m "feat(compression): resolveCompressionPlan precedence resolver (header>override>active>default)"`
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Wire the resolver into strategy selection + chatCore
|
||||
|
||||
**Files:**
|
||||
- Modify: `open-sse/services/compression/strategySelector.ts` (`selectCompressionStrategy`)
|
||||
- Modify: `open-sse/handlers/chatCore.ts` (the compression call site)
|
||||
- Test: `tests/unit/compression/strategySelector.test.ts` (extend with an engines-map case)
|
||||
|
||||
- [ ] **Step 1: Add test** asserting `selectCompressionStrategy` with `engines:{rtk:{enabled:true}}` + master on returns mode `rtk`; with `{rtk,caveman}` returns `stacked`. (Use the existing test's import + harness.)
|
||||
- [ ] **Step 2: Run → FAIL.**
|
||||
- [ ] **Step 3: Implement** — `selectCompressionStrategy` calls `resolveCompressionPlan(config, { comboId, combos })` and returns its `mode` (and expose the `stackedPipeline` so `applyCompressionAsync` uses the derived pipeline when mode==="stacked"). Load named `combos` from the combos DB module. In `chatCore.ts`, pass the active combo set; keep the `header` arg `undefined` (Phase 3 fills it). Keep `autoTriggerMode` behavior (auto-trigger still overrides to its mode on large prompts — apply BEFORE step 4 default).
|
||||
- [ ] **Step 4: Run → PASS** + full compression suite + typecheck.
|
||||
- [ ] **Step 5: Commit** `... -m "feat(compression): selectCompressionStrategy uses resolveCompressionPlan"`
|
||||
|
||||
---
|
||||
|
||||
## Task 8: API — `/api/settings/compression` carries `engines` + `activeComboId`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app/api/settings/compression/route.ts`
|
||||
- Test: `tests/unit/api/compression/compression-api.test.ts` (extend)
|
||||
|
||||
- [ ] **Step 1: Add test**: `PUT` with `{engines:{rtk:{enabled:true,level:"standard"}}}` then `GET` returns it; error body has no stack (`!body.error?.message?.includes("at /")`).
|
||||
- [ ] **Step 2: Run → FAIL.**
|
||||
- [ ] **Step 3: Implement** — extend the route's Zod body schema with `engines` + `activeComboId` (reuse the db sub-schema); GET returns them; errors via `buildErrorBody`.
|
||||
- [ ] **Step 4: Run → PASS** + vitest if the route is covered there.
|
||||
- [ ] **Step 5: Commit** `... -m "feat(api): settings/compression carries engines map + activeComboId"`
|
||||
|
||||
---
|
||||
|
||||
## Task 9: Remove stored `defaultMode` write-path + default-combo editable route → shim
|
||||
|
||||
**Files:**
|
||||
- Modify: `open-sse/services/compression/types.ts` (drop `defaultMode` from the persisted shape; keep `CompressionMode` type)
|
||||
- Modify: `src/app/api/context/combos/default/route.ts` (PUT → 410/deprecation; GET → derived default read-only)
|
||||
- Test: `tests/unit/api/...` for the shim (PUT rejected, GET returns derived).
|
||||
|
||||
- [ ] **Step 1: Add test**: `PUT /api/context/combos/default` returns a deprecation error (not 200); `GET` returns the derived default pipeline.
|
||||
- [ ] **Step 2: Run → FAIL.**
|
||||
- [ ] **Step 3: Implement** — `setEngineInDefaultCombo` no longer the write path; PUT route returns `buildErrorBody` "deprecated: edit engines in /api/settings/compression"; GET returns `deriveDefaultPlan(config.engines, config.enabled)`. Remove remaining reads of stored `defaultMode` (derive).
|
||||
- [ ] **Step 4: Run → PASS** + full suite + typecheck.
|
||||
- [ ] **Step 5: Commit** `... -m "refactor(compression): derive default; default-combo route is a read-only shim"`
|
||||
|
||||
---
|
||||
|
||||
## Task 10: The engine-grid panel UI
|
||||
|
||||
**Files:**
|
||||
- Create: `src/app/(dashboard)/dashboard/context/settings/CompressionPanel.tsx`
|
||||
- Modify: `src/app/(dashboard)/dashboard/context/settings/page.tsx` (render `CompressionPanel`)
|
||||
- Test: `tests/unit/ui/compressionPanel.test.tsx` (vitest, `createRoot`+`act`)
|
||||
|
||||
- [ ] **Step 1: Write the failing component test**: render `CompressionPanel` with a stubbed `fetch` returning `{enabled:true, engines:{rtk:{enabled:true,level:"standard"}}}`; assert it renders a row per `ENGINE_IDS`, the rtk level shows "standard", toggling caveman issues a `PUT` with `engines.caveman.enabled:true`, and the derived-pipeline preview text appears.
|
||||
- [ ] **Step 2: Run → FAIL** (`npm run test:vitest`).
|
||||
- [ ] **Step 3: Implement** `CompressionPanel.tsx`: master toggle; map `ENGINE_IDS` → a row component `[label+desc][Toggle][LevelSelect if meta.levels][Link → /dashboard/context/<id>]`; a derived-pipeline preview computed client-side via `deriveDefaultPlan` (import the pure fn); a `cavemanOutput` row; an `mcpAccessibility` row (writes its own endpoint, with a "MCP tool outputs" note); general settings (auto-trigger, preserve system prompt). Save via `PUT /api/settings/compression` (debounced, merge-patch like the existing `save()` pattern in `CompressionSettingsTab`). Reuse existing primitives (`Toggle`, segmented control) from the current cards.
|
||||
- [ ] **Step 4:** Update `page.tsx` to render `<CompressionPanel/>`. Run → PASS.
|
||||
- [ ] **Step 5: Commit** `... -m "feat(dashboard): engine-grid compression panel (single source for on/off + level)"`
|
||||
|
||||
---
|
||||
|
||||
## Task 11: Consolidate — remove scattered/duplicate toggles + per-engine on/off
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app/(dashboard)/dashboard/settings/components/CompressionTokenSaverCard.tsx` (remove; or strip to a read-only summary linking to the panel)
|
||||
- Modify: `CompressionSettingsTab.tsx` (remove duplicate caveman/rtk on/off + intensity sections; keep only things not in the panel, or delete if fully superseded)
|
||||
- Modify: `src/shared/components/compression/EngineConfigPage.tsx` + `CavemanContextPageClient.tsx` + `RtkContextPageClient.tsx` (remove on/off + level; keep detailed config; stop writing `/api/context/combos/default`)
|
||||
- Test: vitest render tests for the per-engine pages assert NO enabled toggle is present; the existing tests updated to the new shape (alignment, not masking).
|
||||
|
||||
- [ ] **Step 1:** Update the affected render tests to expect the new (toggle-free) shape; run → FAIL on the still-present toggles.
|
||||
- [ ] **Step 2:** Implement the removals; per-engine detail save writes detailed config to its facade route (caveman/rtk) or the settings sub-object.
|
||||
- [ ] **Step 3:** Run vitest + full compression suite → PASS.
|
||||
- [ ] **Step 4: Commit** `... -m "refactor(dashboard): remove duplicate compression toggles; per-engine pages keep only detailed config"`
|
||||
|
||||
---
|
||||
|
||||
## Task 12: Menu reorder + integration + full validation
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/shared/constants/sidebarVisibility.ts` (`COMPRESSION_CONTEXT_GROUP`: Settings → Combos → per-engine → Studio)
|
||||
- Test: `tests/unit/...` sidebar order test (if one exists) + an integration test.
|
||||
|
||||
- [ ] **Step 1:** Add an integration test: build a config with `engines:{rtk:{enabled:true},caveman:{enabled:true,level:"full"}}`, call `selectCompressionStrategy` + `applyCompressionAsync` on a realistic body, assert the derived stacked pipeline ran (engineBreakdown has rtk+caveman) and equals the behavior of an explicit `[rtk,caveman]` stacked config. Run → (write fails first if any wiring gap).
|
||||
- [ ] **Step 2:** Reorder the sidebar group; update any sidebar order test (alignment).
|
||||
- [ ] **Step 3:** FULL validation: `npm run typecheck:core` (clean) · `npm run lint` (0 errors) · `node --import tsx/esm --test tests/unit/compression/*.test.ts` (green) · `npm run test:vitest` (green) · the api/integration compression tests.
|
||||
- [ ] **Step 4: Commit** `... -m "feat(compression): unified panel menu order + integration coverage"`
|
||||
|
||||
---
|
||||
|
||||
## Self-review notes (done while writing)
|
||||
- Spec coverage: panel (T10), combos boundary (default derived T3/T9; named/active resolver T6 — UI for active selection is Phase 2), per-engine detail-only (T11), header (resolver is header-aware T6; parsing+wiring is Phase 3), migration (T4/T5), menu (T12). ✓
|
||||
- Type consistency: `EngineToggle` (T2) used by `deriveDefaultPlan` (T3), `resolveCompressionPlan` (T6), normalize (T5), panel (T10). `DerivedPlan` shape consistent T3↔T6↔T7. ✓
|
||||
- No placeholders: each task has concrete code/tests. UI tasks (T10/T11) specify exact behavior + assertions; final per-line component code is produced at execution following the existing card patterns.
|
||||
|
||||
---
|
||||
|
||||
# 📌 RESUMO — o que ESTE plano (Fase 1) entrega
|
||||
|
||||
1. **Catálogo de engines** (`engineCatalog.ts`) — fonte única de metadados (níveis, single-mode, ordem).
|
||||
2. **Modelo de dados Fase A** — `engines` map + `activeComboId` em `CompressionConfig`, com migração 102 + backfill (zero mudança de comportamento).
|
||||
3. **`deriveDefaultPlan`** — pipeline default DERIVADO dos toggles (0/1/N engines → off/modo/stacked).
|
||||
4. **`resolveCompressionPlan`** — resolvedor de precedência (header > override por-rota > perfil ativo > default derivado > off), já header/active-aware.
|
||||
5. **Fiação no runtime** — `selectCompressionStrategy`/`chatCore` usam o resolvedor.
|
||||
6. **API** — `/api/settings/compression` carrega `engines` + `activeComboId`; rota `combos/default` vira shim read-only; `defaultMode` armazenado removido (derivado).
|
||||
7. **Painel engine-grid** — `CompressionPanel.tsx` como fonte única de master + on/off + nível, com preview do pipeline derivado.
|
||||
8. **Consolidação** — remove toggles duplicados (TokenSaverCard, CompressionSettingsTab) e tira on/off+nível das páginas por-engine (que ficam só com config detalhada).
|
||||
9. **Menu** — Settings → Combos → páginas por-engine → Studio.
|
||||
|
||||
---
|
||||
|
||||
# ⏳ PENDÊNCIAS — a fazer DEPOIS deste plano (cada uma vira seu próprio plano/PR)
|
||||
|
||||
### Fase 2 — Perfis nomeados + seletor de ativo
|
||||
- **UI de combos como perfis**: a página `context/combos` lista N combos nomeados, edita pipeline **ordenado** (drag/reorder + nível por step), e tem o seletor **"perfil ativo"** (`Default` | `<combo>`) gravando `activeComboId`. O resolvedor (Fase 1) já consome `activeComboId`; falta a UI + o carregamento dos combos nomeados no `selectCompressionStrategy`. Remover o "master mode selector" do `CompressionHub` (modo agora é derivado).
|
||||
|
||||
### Fase 3 — Header por-request `x-omniroute-compression`
|
||||
- **Parsing + wiring do header**: ler `x-omniroute-compression` no pipeline (espelhando `x-omniroute-no-memory`, PR #4290), passar como `ctx.header` ao `resolveCompressionPlan` (que já trata `off`/`default`/`<combo>`/`engine:<id>`). Doc no `API_REFERENCE` + teste de fetch-capture provando precedência por-request.
|
||||
|
||||
### Itens de compressão deferidos (do ciclo de fixes, independentes deste painel)
|
||||
- **B-OBSERVABILITY** (telemetria): engines no-op somem do `engineBreakdown` (não dá pra distinguir "rodou 0%" de "pulou"). Exige um refactor do modelo de breakdown (campo `ran`/`skipped`) que toca a UI Studio + testes que asseram `.length` — deferido conscientemente.
|
||||
- **B-CAVEMAN-PACKS**: `de`/`fr`/`ja` sem `dedup.json`+`ultra.json` (ultra==full nessas línguas). Conteúdo linguístico — adicionar os packs (ou contribuição), **sem** fallback EN que mutilaria.
|
||||
- **js-tiktoken 1.0.21→1.0.22**: bump trivial (o range `^1.0.20` já permite; só pinar o lockfile num install real).
|
||||
|
||||
### Decisão operacional pendente (não-código)
|
||||
- **Ligar o SLM (tier ultra) em produção**: validado ao vivo (49,4% real), mas mantido **OFF** por sua escolha. Quando decidir o trade-off (custo/latência/qualidade do pruning), ligar via o painel (após Fase 1) ou pela escrita de config — começando conservador (≥2000 tok).
|
||||
|
||||
### Portes upstream (opcionais, do audit — fora do escopo deste painel)
|
||||
- headroom *safety-rails*/BM25; filtros novos do rtk/token-savior; conformance GCF v3.1 (já cobrimos o `[..]:`); transformers.js 3.5.2→4.x (arriscado, major).
|
||||
@@ -1,207 +0,0 @@
|
||||
---
|
||||
title: "Compression Config Panel — Phase 2: Named Profiles + Active Selector"
|
||||
version: 3.8.32
|
||||
lastUpdated: 2026-06-21
|
||||
---
|
||||
|
||||
# Compression Config Panel — Phase 2: Named Profiles + Active Selector
|
||||
|
||||
**Status:** approved direction (2026-06-21), pending spec review
|
||||
**Base branch:** `release/v3.8.32` (Phase 1 merged via #4432)
|
||||
**Goal:** Let the operator pick which compression **profile** is globally active — the
|
||||
panel-derived **Default** or one of the existing **named combos** — via a single
|
||||
active-profile selector that writes `CompressionConfig.activeComboId`, and wire that
|
||||
selection into the runtime so the chosen profile actually runs. Remove the now-duplicate
|
||||
"master mode selector" and "Set as Default" controls so there is exactly one concept:
|
||||
the active profile.
|
||||
|
||||
Phase 1 (#4432) already built the `engines` map (the Default), `deriveDefaultPlan`,
|
||||
`resolveCompressionPlan` (header/active-combo-aware), and persisted `activeComboId`.
|
||||
Phase 3 (the `x-omniroute-compression` per-request header) is a separate later plan.
|
||||
|
||||
---
|
||||
|
||||
## 1. Background — current state
|
||||
|
||||
The named-combos infrastructure already exists; Phase 2 wires it up and removes
|
||||
duplication. From the code map:
|
||||
|
||||
- **`compression_combos` table** (migration 042) + **`src/lib/db/compressionCombos.ts`**:
|
||||
full CRUD (`listCompressionCombos`, `createCompressionCombo`, `updateCompressionCombo`,
|
||||
`deleteCompressionCombo`), routing-combo assignments, and an `is_default` flag
|
||||
(`getDefaultCompressionCombo`/`setDefaultCompressionCombo`). A combo's `pipeline` is
|
||||
`CompressionPipelineStep[]` (`{engine, intensity?, config?}`).
|
||||
- **`CompressionCombosPageClient.tsx`** renders two blocks on `context/combos`:
|
||||
- **`CompressionHub`** — today: a Token-Saver (master) toggle, a **Mode selector**
|
||||
(Off/Lite/Standard/…/Stacked), a read-only active-pipeline display (read from the
|
||||
410-shim `/api/context/combos/default`), and reorder buttons.
|
||||
- **`NamedCombosManager`** — full create/list/edit/delete of named combos + pipeline
|
||||
editor + language packs + output mode + routing-combo assignments + a **"Set as
|
||||
Default"** button (sets `is_default`).
|
||||
- **`CompressionConfig.activeComboId`** is persisted (`/api/settings/compression`) but
|
||||
**not wired into dispatch**: `resolveCompressionPlan` has the active-combo branch
|
||||
(`ctx.combos[config.activeComboId]`), but Phase 1 only reaches it from inside
|
||||
`deriveDefaultPlanFromConfig` (gated on `enginesExplicit`) and passes `combos: {}`
|
||||
(empty), so the branch never fires.
|
||||
|
||||
**The reconciliation (decided):** unify under the active profile. The active-profile
|
||||
selector (writing `activeComboId`) is the single user-facing source of "which profile
|
||||
runs". The `is_default` flag stays as a **backend-only** legacy detail (the fallback the
|
||||
chatCore legacy block + `deriveEnginesMap` backfill read for installs that never opted
|
||||
into the panel); it is no longer user-settable. The "Set as Default" button is removed.
|
||||
|
||||
---
|
||||
|
||||
## 2. Architecture — dispatch
|
||||
|
||||
### 2.1 Resolution model (precedence)
|
||||
|
||||
```
|
||||
master off → off
|
||||
→ routing-combo override (config.comboOverrides[comboId]) → that mode
|
||||
→ ACTIVE PROFILE (config.activeComboId set + combo exists) → that combo's pipeline [NEW]
|
||||
→ auto-trigger (estimatedTokens ≥ autoTriggerTokens) → autoTriggerMode
|
||||
→ Default derived (enginesExplicit → engines map; else legacy defaultMode)
|
||||
→ off
|
||||
```
|
||||
|
||||
The active profile is an **explicit operator choice**, so it resolves:
|
||||
- **regardless of `enginesExplicit`** (setting `activeComboId` is itself an explicit
|
||||
opt-in, even on a legacy install), and
|
||||
- **above auto-trigger** (a manually-chosen profile wins over automatic escalation),
|
||||
- but **below a per-routing-combo override** (a route-scoped override is more specific).
|
||||
|
||||
### 2.2 Where it resolves (the Phase-1 gap)
|
||||
|
||||
Phase 1's `resolveBasePlan` (in `strategySelector.ts`) does: master-off → routing-combo
|
||||
override → auto-trigger → `deriveDefaultPlanFromConfig`. The `activeComboId` branch lives
|
||||
inside `resolveCompressionPlan`, which `deriveDefaultPlanFromConfig` only calls when
|
||||
`enginesExplicit`, with `combos: {}`. So Phase 2 **lifts the active-profile resolution
|
||||
into `resolveBasePlan`**, right after the routing-combo override and before auto-trigger:
|
||||
|
||||
```ts
|
||||
// resolveBasePlan, after checkComboOverride, before shouldAutoTrigger:
|
||||
if (config.activeComboId && ctx.combos?.[config.activeComboId]) {
|
||||
return { mode: "stacked", stackedPipeline: ctx.combos[config.activeComboId] };
|
||||
}
|
||||
```
|
||||
|
||||
`ctx.combos` is `Record<comboId, CompressionPipelineStep[]>`. `selectCompressionPlan`/
|
||||
`selectCompressionStrategy` gain a `combos` param (threaded into `ctx`) so callers supply
|
||||
it; existing callers that pass nothing default to `{}` (unchanged behavior).
|
||||
|
||||
### 2.3 Loading the combos (Approach A — in chatCore)
|
||||
|
||||
`chatCore.ts` already loads named combos for routing-combo assignment lookup. Phase 2
|
||||
extends that to build the full `combos` map once and pass it to `selectCompressionPlan`:
|
||||
|
||||
```ts
|
||||
const { listCompressionCombos } = await import("../../src/lib/db/compressionCombos.ts");
|
||||
const combos = Object.fromEntries(listCompressionCombos().map((c) => [c.id, c.pipeline]));
|
||||
// …passed as the new combos arg to selectCompressionStrategy / selectCompressionPlan
|
||||
```
|
||||
|
||||
When the active profile resolves to `stacked`, its pipeline reaches `applyCompressionAsync`
|
||||
the same way Phase 1's engines-map override does — chatCore sets
|
||||
`config.stackedPipeline = ctx.combos[activeComboId]` under the same guard
|
||||
(`!compressionComboApplied && !config.compressionComboId`) so the operator's chosen profile
|
||||
runs instead of the built-in default. `strategySelector` stays pure (no `src/lib/db`
|
||||
import); only chatCore touches the DB.
|
||||
|
||||
### 2.4 What stays untouched (legacy backend)
|
||||
|
||||
`is_default`, `getDefaultCompressionCombo`, `setDefaultCompressionCombo`,
|
||||
`setEngineInDefaultCombo`, the chatCore legacy default-combo block, and the
|
||||
`deriveEnginesMap` backfill are **unchanged**. Setting `activeComboId` never writes
|
||||
`is_default`. The active-profile path resolves *before* (and independently of) the legacy
|
||||
default-combo fallback, so legacy installs that never set `activeComboId` keep their exact
|
||||
behavior; an install that sets `activeComboId` runs that profile.
|
||||
|
||||
---
|
||||
|
||||
## 3. UI
|
||||
|
||||
No new API endpoints or DB migrations: `/api/settings/compression` already carries
|
||||
`activeComboId`, and the combos CRUD API already exists.
|
||||
|
||||
### 3.1 Active-profile selector — top of `CompressionHub`
|
||||
|
||||
```
|
||||
Active profile: [ Default (from panel) ▼ ]
|
||||
• Default (from panel) → activeComboId = null
|
||||
• Standard Savings → a named combo id
|
||||
• <other named combos>
|
||||
```
|
||||
|
||||
On change → `PUT /api/settings/compression { activeComboId }` (null for "Default";
|
||||
debounced/merge-patch like the panel's `save()`). Below it, a **read-only preview** of the
|
||||
active profile's effective pipeline ("runs: rtk → caveman → …"):
|
||||
- Default → `deriveDefaultPlan(config.engines, config.enabled)` (the pure fn, client-side).
|
||||
- Named combo → that combo's pipeline.
|
||||
|
||||
### 3.2 `CompressionHub` becomes a read-only overview — remove duplicates
|
||||
|
||||
- **Remove** the **Mode selector** (Off/Lite/…/Stacked) — the mode is now derived; this is
|
||||
the "master mode selector" the redesign removes.
|
||||
- **Remove** the **Token Saver (master on/off) toggle** — it lives in the Panel
|
||||
(`context/settings`), the single source since Phase 1.
|
||||
- **Remove** the pipeline **reorder buttons** — ordering a named combo is done in the
|
||||
combo editor (`NamedCombosManager`); the Default's order is auto-derived by
|
||||
`stackPriority`.
|
||||
- **Keep** only the active-profile selector + the read-only preview.
|
||||
|
||||
### 3.3 `NamedCombosManager` — one change
|
||||
|
||||
- **Remove** the **"Set as Default"** button (the active selector replaces it).
|
||||
- **Keep** everything else: create/list/edit/delete, the pipeline editor (add/remove steps
|
||||
+ per-step intensity), language packs, output mode, routing-combo assignments.
|
||||
- **Add** an **"● Active"** badge on the card whose id equals `activeComboId`.
|
||||
|
||||
### 3.4 Navigation
|
||||
|
||||
Sidebar order unchanged (Settings → Combos → per-engine pages → Studio).
|
||||
|
||||
---
|
||||
|
||||
## 4. Testing
|
||||
|
||||
Both runners green (`test:unit` node + `test:vitest`); `typecheck:core` + `lint` clean;
|
||||
`check:file-size` (rebaseline `CompressionHub.tsx`/`CompressionCombosPageClient.tsx` if
|
||||
they grow, with a justification key).
|
||||
|
||||
- **Resolver / dispatch (unit, node):** `selectCompressionStrategy`/`resolveBasePlan` with
|
||||
`activeComboId` + `ctx.combos[id]` → resolves to that combo's stacked pipeline,
|
||||
**regardless of `enginesExplicit`**; `activeComboId` null → Default derived;
|
||||
`activeComboId` set but combo missing → graceful fall-through. Precedence:
|
||||
routing-combo override > active profile > auto-trigger > Default derived (one test per
|
||||
boundary).
|
||||
- **chatCore integration:** `activeComboId` set → chatCore loads named combos → the active
|
||||
combo's pipeline runs via `applyCompressionAsync` (engineBreakdown reflects the combo's
|
||||
engines), mirroring Phase 1's `derived-pipeline-integration` test. Plus: setting
|
||||
`activeComboId` does **not** mutate `is_default` (`getDefaultCompressionCombo`
|
||||
unchanged).
|
||||
- **DB + API (unit):** `activeComboId` round-trips in `updateCompressionSettings`/
|
||||
`getCompressionSettings` and `PUT`/`GET /api/settings/compression`, including the
|
||||
"switch combo → null" path.
|
||||
- **UI (vitest component):** `CompressionHub` renders the selector (Default + named combos),
|
||||
changing it issues the right `PUT`, the preview reflects the selection, and the removed
|
||||
controls (mode selector, master toggle, reorder) are **absent** (alignment, not masking).
|
||||
`NamedCombosManager`: "Set as Default" gone; the active combo shows the "● Active" badge.
|
||||
Honor the Phase-1 vitest gotchas (i18n renders English/keys → assert on stable
|
||||
strings/`data-testid`; new `@omniroute/...` imports don't resolve under vitest → use
|
||||
relative/re-export).
|
||||
- **No regression:** `is_default`/`getDefaultCompressionCombo`/legacy chatCore block/
|
||||
backfill tests stay green and untouched.
|
||||
|
||||
---
|
||||
|
||||
## 5. Non-goals (YAGNI)
|
||||
|
||||
- **Phase 3** — the `x-omniroute-compression` per-request header — is a separate plan
|
||||
(the resolver is already header-aware).
|
||||
- No new compression engines; no new named-combo features (templates, sharing, import/
|
||||
export).
|
||||
- No change to the combo CRUD API/DB (already shaped correctly).
|
||||
- The `is_default` flag is kept (legacy backend fallback), not removed — removing it is a
|
||||
larger migration out of scope here.
|
||||
- The Compression Studio (analytics) is untouched.
|
||||
@@ -1,468 +0,0 @@
|
||||
---
|
||||
title: "Compression Phase 2 — Named Profiles + Active Selector: Implementation Plan"
|
||||
version: 3.8.32
|
||||
lastUpdated: 2026-06-21
|
||||
---
|
||||
|
||||
# Compression Phase 2 — Named Profiles + Active Selector Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Wire `CompressionConfig.activeComboId` into the runtime and give the operator a single active-profile selector (Default-from-panel | a named combo) on the Combos page, removing the now-duplicate "master mode selector" and "Set as Default" controls.
|
||||
|
||||
**Architecture:** The resolver already has the model; Phase 2 (a) lifts the active-combo resolution into `resolveBasePlan` (so it applies regardless of `enginesExplicit`, above auto-trigger, below a routing-combo override), threading a `combos` map through `selectCompressionPlan`/`selectCompressionStrategy`; (b) `chatCore` loads named combos via `listCompressionCombos()` and passes them in, and guards the legacy default-combo block so it can't shadow an active profile; (c) the UI adds an active-profile selector + read-only preview to `CompressionHub` (removing the master toggle / mode selector / reorder) and removes "Set as Default" from `NamedCombosManager` (adding an "● Active" badge). No new API endpoints or DB migrations.
|
||||
|
||||
**Tech Stack:** TypeScript, Next.js 16 App Router, SQLite (better-sqlite3), Node test runner + Vitest (component), React.
|
||||
|
||||
**Base:** worktree `feat/compression-phase2-named-profiles` off `release/v3.8.32`. Spec: `docs/compression/2026-06-21-compression-phase2-named-profiles-design.md`.
|
||||
|
||||
**Conventions:** unit tests run `node --import tsx/esm --test tests/unit/<f>.test.ts`; component tests run `npx vitest run <file>`. Each task ends by running the FULL compression suite (`node --import tsx/esm --test tests/unit/compression/*.test.ts`) + `npm run typecheck:core` before commit. Never `--no-verify`. Both UI files (`CompressionHub.tsx`, `CompressionCombosPageClient.tsx`) deliberately use **literal English strings, NOT `useTranslations`** — so vitest component tests can assert on those strings directly.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
**Modify:**
|
||||
- `open-sse/services/compression/strategySelector.ts` — thread a `combos` param through `resolveBasePlan`/`selectCompressionPlan`/`selectCompressionStrategy`/`getEffectiveMode`; resolve the active combo in `resolveBasePlan`; export `activeComboResolves()`.
|
||||
- `open-sse/handlers/chatCore.ts` — load named combos, pass them to the two `selectCompression*` calls, guard the legacy default-combo block with `!activeComboResolves(...)`.
|
||||
- `src/app/(dashboard)/dashboard/context/combos/CompressionHub.tsx` — remove master toggle / mode selector / reorder; add the active-profile selector + preview.
|
||||
- `src/app/(dashboard)/dashboard/context/combos/CompressionCombosPageClient.tsx` — remove `setDefault` + "Set as Default" button; add the "● Active" badge from `activeComboId`.
|
||||
|
||||
**Create (tests):**
|
||||
- `tests/unit/compression/active-combo-dispatch.test.ts`
|
||||
- `tests/unit/compression/active-combo-integration.test.ts`
|
||||
- `tests/unit/ui/compressionHub-active-selector.test.tsx`
|
||||
- `tests/unit/ui/namedCombos-active-badge.test.tsx`
|
||||
|
||||
**Reference types:** `CompressionPipelineStep` = `{ engine: CompressionEngineId; intensity?: CavemanIntensity | RtkIntensity; config?: Record<string, unknown> }` (in `open-sse/services/compression/types.ts`). `DerivedPlan` = `{ mode: string; stackedPipeline: Array<{ engine: string; intensity?: string }> }` (in `deriveDefaultPlan.ts`).
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Dispatch — resolve the active combo + thread `combos`
|
||||
|
||||
**Files:**
|
||||
- Modify: `open-sse/services/compression/strategySelector.ts`
|
||||
- Test: `tests/unit/compression/active-combo-dispatch.test.ts`
|
||||
|
||||
- [ ] **Step 1: Write the failing test** at `tests/unit/compression/active-combo-dispatch.test.ts`
|
||||
|
||||
```ts
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
selectCompressionStrategy,
|
||||
selectCompressionPlan,
|
||||
activeComboResolves,
|
||||
} from "../../../open-sse/services/compression/strategySelector.ts";
|
||||
import {
|
||||
DEFAULT_COMPRESSION_CONFIG,
|
||||
type CompressionConfig,
|
||||
} from "../../../open-sse/services/compression/types.ts";
|
||||
|
||||
const combos = { c1: [{ engine: "rtk", intensity: "standard" }, { engine: "caveman", intensity: "full" }] };
|
||||
|
||||
function cfg(overrides: Partial<CompressionConfig> = {}): CompressionConfig {
|
||||
return { ...DEFAULT_COMPRESSION_CONFIG, enabled: true, ...overrides };
|
||||
}
|
||||
|
||||
describe("active named combo resolution (Phase 2)", () => {
|
||||
it("activeComboId + combo present => that combo's stacked pipeline (regardless of enginesExplicit)", () => {
|
||||
const config = cfg({ activeComboId: "c1", enginesExplicit: false });
|
||||
const plan = selectCompressionPlan(config, null, 0, undefined, undefined, combos);
|
||||
assert.equal(plan.mode, "stacked");
|
||||
assert.deepEqual(plan.stackedPipeline, combos.c1);
|
||||
});
|
||||
it("activeComboId null => falls through to derived default (not the combo)", () => {
|
||||
const config = cfg({ activeComboId: null, enginesExplicit: true, engines: { rtk: { enabled: true } } });
|
||||
assert.equal(selectCompressionStrategy(config, null, 0, undefined, undefined, combos), "rtk");
|
||||
});
|
||||
it("activeComboId set but combo missing => graceful fall-through to default", () => {
|
||||
const config = cfg({ activeComboId: "ghost", defaultMode: "lite", enginesExplicit: false });
|
||||
assert.equal(selectCompressionStrategy(config, null, 0, undefined, undefined, combos), "lite");
|
||||
});
|
||||
it("routing-combo override wins over the active profile", () => {
|
||||
const config = cfg({ activeComboId: "c1", comboOverrides: { "my-combo": "off" } });
|
||||
assert.equal(selectCompressionStrategy(config, "my-combo", 0, undefined, undefined, combos), "off");
|
||||
});
|
||||
it("active profile wins over auto-trigger", () => {
|
||||
const config = cfg({ activeComboId: "c1", autoTriggerTokens: 1000, autoTriggerMode: "aggressive" });
|
||||
assert.equal(selectCompressionStrategy(config, null, 5000, undefined, undefined, combos), "stacked");
|
||||
});
|
||||
});
|
||||
|
||||
describe("activeComboResolves", () => {
|
||||
it("true only when activeComboId is set AND present in combos", () => {
|
||||
assert.equal(activeComboResolves(cfg({ activeComboId: "c1" }), combos), true);
|
||||
assert.equal(activeComboResolves(cfg({ activeComboId: "ghost" }), combos), false);
|
||||
assert.equal(activeComboResolves(cfg({ activeComboId: null }), combos), false);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run → FAIL** (`node --import tsx/esm --test tests/unit/compression/active-combo-dispatch.test.ts`) — `activeComboResolves` not exported / `combos` arg ignored.
|
||||
|
||||
- [ ] **Step 3: Implement** in `strategySelector.ts`. Add the `CompressionPipelineStep` type import (it's already imported from `./types.ts` in most cases — confirm; if not, add it). Define the combos type alias near the top after imports:
|
||||
|
||||
```ts
|
||||
type NamedCombos = Record<string, CompressionPipelineStep[]>;
|
||||
```
|
||||
|
||||
Change `resolveBasePlan` to accept `combos` and resolve the active combo after the routing-combo override, before auto-trigger:
|
||||
|
||||
```ts
|
||||
function resolveBasePlan(
|
||||
config: CompressionConfig,
|
||||
comboId: string | null,
|
||||
estimatedTokens: number,
|
||||
combos: NamedCombos = {}
|
||||
): DerivedPlan {
|
||||
if (!config.enabled) return { mode: "off", stackedPipeline: [] };
|
||||
|
||||
const comboMode = checkComboOverride(config, comboId);
|
||||
if (comboMode) {
|
||||
return resolveCompressionPlan(config, { comboId, combos });
|
||||
}
|
||||
|
||||
// Active profile: an EXPLICIT operator choice. Resolves regardless of enginesExplicit and
|
||||
// above auto-trigger (manual choice beats automatic escalation), but below a routing-combo
|
||||
// override (route-scoped is more specific).
|
||||
if (config.activeComboId && combos[config.activeComboId]) {
|
||||
return { mode: "stacked", stackedPipeline: combos[config.activeComboId] };
|
||||
}
|
||||
|
||||
if (shouldAutoTrigger(config, estimatedTokens)) {
|
||||
const mode = config.autoTriggerMode ?? "lite";
|
||||
return mode === "stacked"
|
||||
? { mode, stackedPipeline: config.stackedPipeline ?? [] }
|
||||
: { mode, stackedPipeline: [] };
|
||||
}
|
||||
|
||||
return deriveDefaultPlanFromConfig(config, comboId, combos);
|
||||
}
|
||||
```
|
||||
|
||||
Add the `combos` param to `deriveDefaultPlanFromConfig` and pass it to its `resolveCompressionPlan` call:
|
||||
|
||||
```ts
|
||||
function deriveDefaultPlanFromConfig(
|
||||
config: CompressionConfig,
|
||||
comboId: string | null,
|
||||
combos: NamedCombos = {}
|
||||
): DerivedPlan {
|
||||
if (config.enginesExplicit) {
|
||||
return resolveCompressionPlan(config, { comboId, combos });
|
||||
}
|
||||
const legacyMode = config.defaultMode;
|
||||
if (legacyMode && legacyMode !== "off") {
|
||||
return legacyMode === "stacked"
|
||||
? { mode: legacyMode, stackedPipeline: config.stackedPipeline ?? [] }
|
||||
: { mode: legacyMode, stackedPipeline: [] };
|
||||
}
|
||||
return { mode: "off", stackedPipeline: [] };
|
||||
}
|
||||
```
|
||||
|
||||
Add `combos` as a trailing param to `getEffectiveMode`, `selectCompressionPlan`, `selectCompressionStrategy` and thread it down. Final signatures + bodies:
|
||||
|
||||
```ts
|
||||
export function getEffectiveMode(
|
||||
config: CompressionConfig,
|
||||
comboId: string | null,
|
||||
estimatedTokens: number,
|
||||
combos: NamedCombos = {}
|
||||
): CompressionMode {
|
||||
return resolveBasePlan(config, comboId, estimatedTokens, combos).mode as CompressionMode;
|
||||
}
|
||||
|
||||
export function selectCompressionPlan(
|
||||
config: CompressionConfig,
|
||||
comboId: string | null,
|
||||
estimatedTokens: number,
|
||||
body?: Record<string, unknown>,
|
||||
context?: CachingDetectionContext,
|
||||
combos: NamedCombos = {}
|
||||
): DerivedPlan {
|
||||
const plan = resolveBasePlan(config, comboId, estimatedTokens, combos);
|
||||
if (body) {
|
||||
const ctx = detectCachingContext(body, context);
|
||||
const cacheAware = getCacheAwareStrategy(plan.mode as CompressionMode, ctx);
|
||||
return { ...plan, mode: cacheAware.strategy as CompressionMode };
|
||||
}
|
||||
return plan;
|
||||
}
|
||||
|
||||
export function selectCompressionStrategy(
|
||||
config: CompressionConfig,
|
||||
comboId: string | null,
|
||||
estimatedTokens: number,
|
||||
body?: Record<string, unknown>,
|
||||
context?: CachingDetectionContext,
|
||||
combos: NamedCombos = {}
|
||||
): CompressionMode {
|
||||
return selectCompressionPlan(config, comboId, estimatedTokens, body, context, combos).mode as CompressionMode;
|
||||
}
|
||||
```
|
||||
|
||||
Add the exported helper (near `enginesMapDerivesStackedPipeline`):
|
||||
|
||||
```ts
|
||||
/**
|
||||
* True when the config has an active named-combo selection that exists in the supplied combos
|
||||
* map. chatCore uses this to keep the legacy default-combo fallback from shadowing the
|
||||
* operator's active profile (same class of bug as the #4023 web-cookie shadowing).
|
||||
*/
|
||||
export function activeComboResolves(config: CompressionConfig, combos: NamedCombos = {}): boolean {
|
||||
return Boolean(config.activeComboId && combos[config.activeComboId]);
|
||||
}
|
||||
```
|
||||
|
||||
If `CompressionPipelineStep` is not already imported in `strategySelector.ts`, add it to the existing `import { ... } from "./types.ts"` (it is a type-only symbol).
|
||||
|
||||
- [ ] **Step 4: Run → PASS** + full compression suite + `npm run typecheck:core`.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add open-sse/services/compression/strategySelector.ts tests/unit/compression/active-combo-dispatch.test.ts
|
||||
git commit -m "feat(compression): resolve active named combo in dispatch (activeComboId)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: chatCore — load named combos + guard the legacy block
|
||||
|
||||
**Files:**
|
||||
- Modify: `open-sse/handlers/chatCore.ts` (compression dispatch block, ~lines 1428–1690)
|
||||
- Test: `tests/unit/compression/active-combo-integration.test.ts`
|
||||
|
||||
- [ ] **Step 1: Write the failing integration test** at `tests/unit/compression/active-combo-integration.test.ts`
|
||||
|
||||
```ts
|
||||
import { test, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-active-combo-"));
|
||||
const ORIGINAL = process.env.DATA_DIR;
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const { getDbInstance, resetDbInstance } = await import("../../../src/lib/db/core.ts");
|
||||
const combosDb = await import("../../../src/lib/db/compressionCombos.ts");
|
||||
const { updateCompressionSettings } = await import("../../../src/lib/db/compression.ts");
|
||||
const { selectCompressionPlan } = await import("../../../open-sse/services/compression/strategySelector.ts");
|
||||
const { DEFAULT_COMPRESSION_CONFIG } = await import("../../../open-sse/services/compression/types.ts");
|
||||
|
||||
after(() => {
|
||||
resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
if (ORIGINAL === undefined) delete process.env.DATA_DIR;
|
||||
else process.env.DATA_DIR = ORIGINAL;
|
||||
});
|
||||
|
||||
test("an active named combo's pipeline is what selectCompressionPlan resolves, fed from the DB combos map", async () => {
|
||||
resetDbInstance();
|
||||
getDbInstance();
|
||||
const created = combosDb.createCompressionCombo({
|
||||
name: "RTK only",
|
||||
pipeline: [{ engine: "rtk", intensity: "standard" }],
|
||||
});
|
||||
await updateCompressionSettings({ enabled: true, activeComboId: created.id });
|
||||
|
||||
// Mirror chatCore's load: build the combos map from the DB.
|
||||
const combos = Object.fromEntries(combosDb.listCompressionCombos().map((c) => [c.id, c.pipeline]));
|
||||
const config = { ...DEFAULT_COMPRESSION_CONFIG, enabled: true, activeComboId: created.id };
|
||||
const plan = selectCompressionPlan(config, null, 5000, undefined, undefined, combos);
|
||||
assert.equal(plan.mode, "stacked");
|
||||
assert.deepEqual(plan.stackedPipeline, [{ engine: "rtk", intensity: "standard" }]);
|
||||
|
||||
// Setting activeComboId did NOT change which combo is is_default (legacy untouched).
|
||||
const def = combosDb.getDefaultCompressionCombo();
|
||||
assert.notEqual(def?.id, created.id);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run → FAIL** (the combos map is correct but confirms the wiring contract; if `createCompressionCombo`'s default seeding makes the new combo default, adjust the assertion to read the seeded `default-caveman` id — verify by reading `compressionCombos.ts`). Run: `node --import tsx/esm --test tests/unit/compression/active-combo-integration.test.ts`.
|
||||
|
||||
- [ ] **Step 3: Implement** in `chatCore.ts`. In the compression block, AFTER `config` is loaded and BEFORE the first `selectCompressionStrategy` call (line ~1602), load the named combos once:
|
||||
|
||||
```ts
|
||||
let namedCombos: Record<string, CompressionPipelineStep[]> = {};
|
||||
try {
|
||||
const { listCompressionCombos } = await import("../../src/lib/db/compressionCombos.ts");
|
||||
namedCombos = Object.fromEntries(listCompressionCombos().map((c) => [c.id, c.pipeline]));
|
||||
} catch (err) {
|
||||
log?.debug?.(
|
||||
"COMPRESSION",
|
||||
"Named combos load skipped: " + (err instanceof Error ? err.message : String(err))
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Pass `namedCombos` as the 6th arg to BOTH `selectCompressionStrategy` (line ~1602) and `selectCompressionPlan` (line ~1668):
|
||||
|
||||
```ts
|
||||
const modeBeforeOutputTransform = selectCompressionStrategy(
|
||||
config,
|
||||
compressionComboKey,
|
||||
estimatedTokens,
|
||||
body as Record<string, unknown>,
|
||||
{ provider, targetFormat, model: effectiveModel },
|
||||
namedCombos
|
||||
);
|
||||
```
|
||||
```ts
|
||||
const compressionPlan = selectCompressionPlan(
|
||||
config,
|
||||
compressionComboKey,
|
||||
estimatedTokens,
|
||||
compressionInputBody,
|
||||
{ provider, targetFormat, model: effectiveModel },
|
||||
namedCombos
|
||||
);
|
||||
```
|
||||
|
||||
Guard the legacy default-combo block (the `if (modeBeforeOutputTransform === "stacked" && ... && !enginesMapDerivesStackedPipeline(config))` at line ~1609) so it does NOT fire when the active profile resolves — add `enginesMapDerivesStackedPipeline` is already imported; add `activeComboResolves` to the same import block (line ~1430) and add the guard term:
|
||||
|
||||
```ts
|
||||
const {
|
||||
selectCompressionStrategy,
|
||||
selectCompressionPlan,
|
||||
enginesMapDerivesStackedPipeline,
|
||||
activeComboResolves, // ← add
|
||||
applyCompressionAsync,
|
||||
resolveCacheAwareConfig,
|
||||
} = await import("../services/compression/strategySelector.ts");
|
||||
```
|
||||
```ts
|
||||
if (
|
||||
modeBeforeOutputTransform === "stacked" &&
|
||||
!compressionComboApplied &&
|
||||
!config.compressionComboId &&
|
||||
isBuiltinStackedPipeline(config.stackedPipeline) &&
|
||||
!enginesMapDerivesStackedPipeline(config) &&
|
||||
!activeComboResolves(config, namedCombos) // ← add: never shadow the active profile
|
||||
) {
|
||||
```
|
||||
|
||||
The existing engines-map feed (line ~1680, `if (mode === "stacked" && compressionPlan.stackedPipeline.length > 0 && !compressionComboApplied && !config.compressionComboId)`) now ALSO feeds the active combo's pipeline into `config.stackedPipeline` (because `compressionPlan.stackedPipeline` is the active combo's when activeComboId resolves) — no change needed there. Confirm `CompressionPipelineStep` is imported in chatCore.ts (it is used by `config.stackedPipeline`); if not, add the type import.
|
||||
|
||||
- [ ] **Step 4: Run → PASS** + full compression suite + `npm run typecheck:core` + `npm run lint`.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add open-sse/handlers/chatCore.ts tests/unit/compression/active-combo-integration.test.ts
|
||||
git commit -m "feat(compression): chatCore loads named combos; legacy default combo can't shadow the active profile"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: UI — active-profile selector in CompressionHub (remove duplicates)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app/(dashboard)/dashboard/context/combos/CompressionHub.tsx`
|
||||
- Test: `tests/unit/ui/compressionHub-active-selector.test.tsx`
|
||||
|
||||
- [ ] **Step 1: Read the file first.** Note: it uses literal English strings (no `useTranslations`), `useState` for `settings`/`engines`/`combo`, a `saveSettings()` PUT to `/api/settings/compression`, a `MODES` array + a mode `<select>`, a Token Saver master `<Toggle>`, and a `moveStep()` reorder. The component fetches `/api/settings/compression`, `/api/compression/engines`, `/api/context/combos/default` on mount.
|
||||
|
||||
- [ ] **Step 2: Write the failing component test** at `tests/unit/ui/compressionHub-active-selector.test.tsx` (mirror `tests/unit/ui/compressionPanel.test.tsx`'s `createRoot`+`act` harness). Stub `fetch`: `/api/settings/compression` → `{ enabled:true, defaultMode:"off", activeComboId:null }`; `/api/compression/engines` → `{ engines: [] }`; `/api/context/combos` → `{ combos: [{ id:"c1", name:"RTK only", pipeline:[{engine:"rtk"}] }] }`. Assert:
|
||||
1. A `<select data-testid="active-profile-select">` renders with options "Default (from panel)" and "RTK only".
|
||||
2. Changing it to `c1` issues a `PUT /api/settings/compression` whose body has `activeComboId === "c1"`.
|
||||
3. The preview (`data-testid="active-profile-preview"`) shows the combo's engines when a combo is active.
|
||||
4. There is NO mode `<select>` for Off/Lite/Standard and NO "Token Saver" master toggle (query by their old text/testids → absent).
|
||||
|
||||
- [ ] **Step 3: Run → FAIL** (`npx vitest run tests/unit/ui/compressionHub-active-selector.test.tsx`).
|
||||
|
||||
- [ ] **Step 4: Implement** in `CompressionHub.tsx`:
|
||||
- **Remove** the Token Saver master `<Toggle>` + its label/explainer, the `MODES` mode `<select>` + its handler, and `moveStep` + the reorder buttons. Keep the component's data load + error handling.
|
||||
- **Add** `activeComboId` to the settings state/type; **fetch** the named combos list (`/api/context/combos`) on mount into a `combos` state.
|
||||
- **Add** the active-profile `<select data-testid="active-profile-select">` with a "Default (from panel)" option (value `""` → `activeComboId: null`) and one option per named combo (`value=c.id`, label `c.name`). On change: `setSettings(s => ({...s, activeComboId}))` + `PUT /api/settings/compression { activeComboId }`.
|
||||
- **Add** the read-only preview `<div data-testid="active-profile-preview">`: if `activeComboId` matches a combo, render its `pipeline` engines joined by " → "; else render the Default — import `deriveDefaultPlan` (relative path `../../../../../../open-sse/services/compression/deriveDefaultPlan.ts`, the Phase-1 panel pattern — the bare `@omniroute/...` alias does NOT resolve under vitest) and `engineCatalog` if needed; compute `deriveDefaultPlan(engines, enabled)` from the fetched settings/engines and show the order, OR (simpler, no engines map on the Hub settings type) show the text "Default — see the panel for the derived pipeline." Choose the simplest correct option that the test asserts.
|
||||
- Match the file's existing literal-English style + primitives.
|
||||
|
||||
- [ ] **Step 5: Run → PASS** + `npm run typecheck:core` + `npm run lint`.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add "src/app/(dashboard)/dashboard/context/combos/CompressionHub.tsx" tests/unit/ui/compressionHub-active-selector.test.tsx
|
||||
git commit -m "feat(dashboard): active-profile selector in CompressionHub; remove master toggle/mode/reorder"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: UI — NamedCombosManager: remove "Set as Default", add "● Active" badge
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app/(dashboard)/dashboard/context/combos/CompressionCombosPageClient.tsx`
|
||||
- Test: `tests/unit/ui/namedCombos-active-badge.test.tsx`
|
||||
|
||||
- [ ] **Step 1: Read the file first.** Note: literal English; `setDefault(id)` PUTs `/api/context/combos/{id} { isDefault: true }`; the combo cards (`combos.map`) show a `combo.isDefault` "Default" badge and, when `!combo.isDefault`, a "Set as Default" button + a Delete button.
|
||||
|
||||
- [ ] **Step 2: Write the failing component test** at `tests/unit/ui/namedCombos-active-badge.test.tsx`. Stub: `/api/context/combos` → two combos `c1`/`c2`; `/api/combos` → `[]`; `/api/compression/language-packs` → `[]`; `/api/settings/compression` → `{ activeComboId: "c2" }`. Assert:
|
||||
1. No "Set as Default" button is present anywhere.
|
||||
2. The card for `c2` shows an "● Active" badge (query `data-testid="active-badge-c2"`); the card for `c1` does NOT.
|
||||
|
||||
- [ ] **Step 3: Run → FAIL** (`npx vitest run tests/unit/ui/namedCombos-active-badge.test.tsx`).
|
||||
|
||||
- [ ] **Step 4: Implement** in `CompressionCombosPageClient.tsx`:
|
||||
- **Remove** the `setDefault` function and the "Set as Default" `<button>` from the card render.
|
||||
- **Fetch** `activeComboId` on mount (`GET /api/settings/compression` → `data.activeComboId`) into an `activeComboId` state.
|
||||
- In the card render, **replace** the `combo.isDefault` "Default" badge with: `combo.id === activeComboId && <span data-testid={\`active-badge-${combo.id}\`}>● Active</span>`. (Keep the `isDefault` field in the type; just stop surfacing it.)
|
||||
- Keep create/edit/delete/pipeline-editor/language-packs/output-mode/assignments untouched.
|
||||
|
||||
- [ ] **Step 5: Run → PASS** + `npm run typecheck:core` + `npm run lint`.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add "src/app/(dashboard)/dashboard/context/combos/CompressionCombosPageClient.tsx" tests/unit/ui/namedCombos-active-badge.test.tsx
|
||||
git commit -m "feat(dashboard): NamedCombosManager active badge; remove Set-as-Default (active selector replaces it)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Full validation + no-regression + file-size
|
||||
|
||||
**Files:**
|
||||
- Modify (if needed): `config/quality/file-size-baseline.json` (rebaseline any grown frozen file with a justification key)
|
||||
- Test: reuse existing suites.
|
||||
|
||||
- [ ] **Step 1:** Run the FULL gates:
|
||||
- `npm run typecheck:core` (clean)
|
||||
- `npm run lint` (0 errors)
|
||||
- `npm run check:cycles` (no cycles — the new `combos` threading must not create a `strategySelector → src/lib/db` cycle; it must NOT — chatCore does the DB load, not strategySelector)
|
||||
- `node --import tsx/esm --test tests/unit/compression/*.test.ts` (green, incl. the Phase-1 strategySelector/derive/resolve tests UNCHANGED — confirm no regression from the `combos` param defaults)
|
||||
- `npm run test:vitest` (blocking; green)
|
||||
- `npx vitest run tests/unit/ui/compressionHub-active-selector.test.tsx tests/unit/ui/namedCombos-active-badge.test.tsx` (green)
|
||||
- the api + db compression tests (`tests/unit/api/compression/*.test.ts`, the migration/round-trip test)
|
||||
- [ ] **Step 2:** If `check:file-size` flags `CompressionHub.tsx`/`CompressionCombosPageClient.tsx`/`strategySelector.ts`/`chatCore.ts`, update `config/quality/file-size-baseline.json` (value + a `_rebaseline_2026_06_21_phase2_active_selector` justification key, same pattern as Phase 1).
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "chore(compression): Phase 2 full validation + file-size rebaseline"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-review notes (done while writing)
|
||||
- **Spec coverage:** active-combo resolution in resolveBasePlan (T1), `combos` threading (T1), chatCore load + legacy-block guard (T2), active-profile selector + Hub cleanup (T3), Set-as-Default removal + active badge (T4), is_default-untouched assertion (T2), tests + file-size (T5). ✓
|
||||
- **Type consistency:** `NamedCombos = Record<string, CompressionPipelineStep[]>` used in T1's signatures + T2's chatCore load + `activeComboResolves`. `DerivedPlan.stackedPipeline` accepts `CompressionPipelineStep[]` (structural superset). ✓
|
||||
- **No placeholders:** concrete code/tests; UI removals point at exact elements (the executing subagent reads the file). T3 step 4 offers the simplest preview option and tells the executor to pick what the test asserts — make the test assert the combo-engine text and the "Default" fallback text so the choice is fixed at execution.
|
||||
|
||||
---
|
||||
|
||||
# 📌 RESUMO — o que ESTE plano (Fase 2) entrega
|
||||
1. **Resolução do perfil ativo** no `resolveBasePlan` (precedência routing-override > ativo > auto-trigger > Default derivado), independente de `enginesExplicit`, com `combos` threaded por `selectCompressionPlan`/`Strategy`.
|
||||
2. **chatCore** carrega combos nomeados (`listCompressionCombos`) e os passa ao resolver; o bloco legado de default-combo é guardado por `!activeComboResolves` (não sombreia o perfil ativo).
|
||||
3. **CompressionHub** ganha o seletor de perfil ativo + preview e perde master-toggle/mode-selector/reorder (duplicados/derivados).
|
||||
4. **NamedCombosManager** perde "Set as Default" e ganha o badge "● Ativo".
|
||||
5. Zero API/DB nova; `is_default`/legado intocados; validação completa + rebaseline file-size.
|
||||
|
||||
# ⏳ PENDÊNCIAS — depois desta fase
|
||||
- **Fase 3** — header `x-omniroute-compression` (resolver já header-aware; falta parsing+wiring, espelhando `x-omniroute-no-memory` #4290).
|
||||
- Deferidos da Fase 1: B-OBSERVABILITY, packs caveman de/fr/ja, bump js-tiktoken.
|
||||
- Decisão operacional: ligar o SLM (tier ultra) em produção.
|
||||
@@ -1,229 +0,0 @@
|
||||
---
|
||||
title: "Compression Config Panel — Phase 3: Per-Request Header"
|
||||
version: 3.8.33
|
||||
lastUpdated: 2026-06-21
|
||||
---
|
||||
|
||||
# Compression Config Panel — Phase 3: Per-Request Header
|
||||
|
||||
**Status:** approved direction (2026-06-21), pending spec review
|
||||
**Base branch:** `release/v3.8.33` (Phase 1 #4432, Phase 2 #4521 merged)
|
||||
**Goal:** Let a client override the resolved compression plan for a single request via the
|
||||
`x-omniroute-compression` HTTP header, taking precedence over every operator-configured
|
||||
layer (routing override, active profile, auto-trigger, Default). The compression resolver
|
||||
is already header-aware in shape; Phase 3 adds the header parsing, threads the value to the
|
||||
top of the resolver, and surfaces the resolved plan back to the client via a response header.
|
||||
|
||||
Phase 1 (#4432) built the `engines` map, `deriveDefaultPlan`, `resolveCompressionPlan`
|
||||
(header/active-combo-aware in signature), and persisted `activeComboId`. Phase 2 (#4521)
|
||||
wired the active-profile selector and lifted active-combo resolution into `resolveBasePlan`.
|
||||
Phase 3 is the final piece of the original three-phase plan.
|
||||
|
||||
---
|
||||
|
||||
## 1. Background — current state
|
||||
|
||||
The resolver is **partially** header-aware but the header never reaches the top of the
|
||||
decision. From the code map:
|
||||
|
||||
- **`open-sse/services/compression/resolveCompressionPlan.ts`** already accepts
|
||||
`ResolveCtx.header` and interprets `off` / `default` / `engine:<id>` / `<combo>` via a
|
||||
private `headerToPlan` helper. **But** no caller ever passes `header` today, so the branch
|
||||
is dormant.
|
||||
- **`open-sse/services/compression/strategySelector.ts`** is the real entry point
|
||||
(`selectCompressionStrategy`, `selectCompressionPlan`, `getEffectiveMode`, `applyCompression`).
|
||||
Its `resolveBasePlan` only calls `resolveCompressionPlan` in **two** of five precedence
|
||||
paths (routing-combo override and the `enginesExplicit` derived-default). The
|
||||
**active-profile** and **auto-trigger** paths short-circuit and `return` *before* those
|
||||
calls. So merely threading `header` into the existing `resolveCompressionPlan` calls would
|
||||
**not** give the header top precedence — it would be silently ignored whenever an active
|
||||
profile is set (the common case after Phase 2). The header must be evaluated at the **top**
|
||||
of `resolveBasePlan`.
|
||||
- **`open-sse/handlers/chatCore/headers.ts`** already has the parsing precedent:
|
||||
`isNoMemoryRequested` (`x-omniroute-no-memory`, PR #4290) + the case-insensitive
|
||||
`getHeaderValueCaseInsensitive` reader.
|
||||
- **`src/lib/db/compressionCombos.ts`**: a combo's `id` is a `uuidv4()` (except the seeded
|
||||
`default-caveman`), and its `name` is `TEXT NOT NULL` with **no UNIQUE constraint**
|
||||
(migration 042). So a header value that names a combo is far more usable as the **name**
|
||||
than the opaque UUID `id`.
|
||||
- **`open-sse/handlers/chatCore.ts`** builds the response headers (`X-OmniRoute-Model`,
|
||||
`X-OmniRoute-Cache`, …) via `buildStreamingResponseHeaders` + `attachOmniRouteMetaHeaders`
|
||||
on the main response path. There is currently **no** response header reporting the applied
|
||||
compression plan.
|
||||
|
||||
---
|
||||
|
||||
## 2. The header contract
|
||||
|
||||
`x-omniroute-compression: <value>` — mirrors the `x-omniroute-no-memory` / `no-cache`
|
||||
convention. Parsed alongside the other omniroute request headers. Keyword values and the
|
||||
`engine:` prefix are case-insensitive. Values:
|
||||
|
||||
| Value | Meaning |
|
||||
|---|---|
|
||||
| `off` | No compression for this request. |
|
||||
| `default` | The **panel-derived Default** plan, deterministically — ignores active profile, routing override, **and** auto-trigger. |
|
||||
| `engine:<id>` | A single engine, when that engine is enabled in config (e.g. `engine:rtk`). |
|
||||
| `<combo>` | A named combo. Matched **by name (case-insensitive) first, then by exact id**. |
|
||||
|
||||
**Decision A — combo matched by name:** because the stored `id` is a UUID, the ergonomic
|
||||
header value is the combo's **name** (e.g. `my-fast-combo`). Names are not unique in the DB,
|
||||
so the contract is documented as **first-match-wins**; clients wanting determinism can pass
|
||||
the exact `id`.
|
||||
|
||||
**Decision B — an explicit header value is authoritative:** any valid value
|
||||
(`off` / `default` / `engine:<id>` / `<combo>`) **bypasses auto-trigger**. For example,
|
||||
`default` on a very large prompt keeps the panel Default rather than auto-escalating. The
|
||||
mental model is "the header decides, full stop."
|
||||
|
||||
**Invalid / unknown value → ignored.** Resolution falls through to the normal operator
|
||||
precedence; the request is **never** rejected. A debug log line under the `COMPRESSION`
|
||||
channel records the unrecognized value for observability.
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecture
|
||||
|
||||
### 3.1 Resolution model (per request, most-specific wins)
|
||||
|
||||
```
|
||||
x-omniroute-compression header (per-request) <- NEW top of precedence
|
||||
-> routing-combo override (comboOverrides[comboId]) (per-route)
|
||||
-> active profile (activeComboId) (global, Phase 2)
|
||||
-> auto-trigger (large prompt -> autoTriggerMode)
|
||||
-> Default = derived from panel engines map
|
||||
-> off (master disabled, or zero engines on)
|
||||
```
|
||||
|
||||
The header is evaluated at the **top** of `resolveBasePlan`. A valid value returns its plan
|
||||
immediately; an unknown value falls through to the existing precedence unchanged.
|
||||
|
||||
### 3.2 The resolver `source`
|
||||
|
||||
`resolveBasePlan` (and the public `selectCompressionPlan`) return the existing
|
||||
`DerivedPlan` (`{ mode, stackedPipeline }`) extended with an **optional** `source` field —
|
||||
which precedence layer decided the plan:
|
||||
|
||||
`request-header` | `routing-override` | `active-profile` | `auto-trigger` | `default` | `off`
|
||||
|
||||
`mode` answers *what* compression runs; `source` answers *who* decided. The field is
|
||||
optional so Phase 1/2 callers and snapshots are unaffected. chatCore reads `plan.source`
|
||||
(+ `plan.mode`) to build the response header.
|
||||
|
||||
### 3.3 Header parsing helper
|
||||
|
||||
A new pure function in `open-sse/handlers/chatCore/headers.ts`, mirroring
|
||||
`isNoMemoryRequested`:
|
||||
|
||||
```ts
|
||||
export function resolveCompressionHeader(
|
||||
headers: Record<string, unknown> | Headers | null | undefined
|
||||
): string | null {
|
||||
const value = (getHeaderValueCaseInsensitive(headers, "x-omniroute-compression") || "").trim();
|
||||
return value || null;
|
||||
}
|
||||
```
|
||||
|
||||
It returns the raw trimmed value (or `null`); the resolver owns interpretation and casing
|
||||
rules (so the single source of truth for "what a value means" stays in the resolver, with
|
||||
the parser only reading the wire).
|
||||
|
||||
### 3.4 Threading
|
||||
|
||||
chatCore reads the header from `clientRawRequest?.headers`, then passes it as a new
|
||||
`header?: string | null` argument (default `undefined`) through
|
||||
`selectCompressionStrategy` / `selectCompressionPlan` / `getEffectiveMode` ->
|
||||
`resolveBasePlan`, exactly the pattern Phase 2 used for `combos`. Phase 1/2 call sites that
|
||||
omit the argument are byte-for-byte unchanged.
|
||||
|
||||
For the `<combo>` form, chatCore builds the named-combo map keyed by **both** combo `id` and
|
||||
lowercased `name` (`{ [c.id]: c.pipeline, [c.name.toLowerCase()]: c.pipeline }`). The
|
||||
active-profile lookup keys on `config.activeComboId` (always a UUID/slug `id`), so the added
|
||||
name keys are inert for it — one map serves both paths. The resolver matches `<combo>`
|
||||
**name-first** (per Decision A): it looks up the value lowercased (hitting a `name` key, or an
|
||||
already-lowercase `id`), then falls back to the value as-is (an exact `id`) —
|
||||
`combos[value.toLowerCase()] ?? combos[value]`. All combo `id`s are lowercase
|
||||
(`uuidv4()` hex or the `default-caveman` slug), so an exact id still resolves on the first
|
||||
lookup.
|
||||
|
||||
### 3.5 Where the header logic lives
|
||||
|
||||
The existing `headerToPlan` interpretation (`off` / `default` / `engine:<id>` / `<combo>`)
|
||||
is reused. `resolveBasePlan` evaluates the header **before** the routing-override branch.
|
||||
`default` routes to `deriveDefaultPlanFromConfig` (which already handles both
|
||||
`enginesExplicit` and legacy `defaultMode`), so `default` means "the Default profile" for
|
||||
every install type. The resolver remains **pure** — no `src/lib/db` import (enforced by the
|
||||
existing cycle/source guard).
|
||||
|
||||
---
|
||||
|
||||
## 4. Observability
|
||||
|
||||
A new `compression` key in `OMNIROUTE_RESPONSE_HEADERS`
|
||||
(`src/shared/constants/headers.ts`) -> response header:
|
||||
|
||||
```
|
||||
X-OmniRoute-Compression: <mode>; source=<source>
|
||||
```
|
||||
|
||||
Examples: `aggressive; source=request-header`, `off; source=request-header`,
|
||||
`stacked; source=active-profile`, `lite; source=auto-trigger`, `off; source=off`.
|
||||
|
||||
chatCore captures `{ mode, source }` from the compression resolution (computed early,
|
||||
~line 1530) into an outer-scope variable and injects the header when building
|
||||
`responseHeaders` (~line 4697), so it appears on both streaming and non-streaming
|
||||
responses. The header is informational only and never affects routing.
|
||||
|
||||
---
|
||||
|
||||
## 5. Error handling & safety
|
||||
|
||||
- **Header absent / blank** -> `null`; behaviour is byte-identical to Phase 2.
|
||||
- **Unknown value** -> silent fall-through to normal resolution + a `COMPRESSION` debug log;
|
||||
never a 4xx. The response header reflects the layer that actually won (e.g.
|
||||
`source=auto-trigger`), not `request-header`.
|
||||
- **`engine:<id>` naming a disabled / unknown engine** -> fall-through (same rule as the
|
||||
current `headerToPlan`: returns `null`).
|
||||
- **Gating:** the header is honored **unconditionally**, like `x-omniroute-no-memory`.
|
||||
Rationale: it only affects the compression of the **client's own request**. The worst case
|
||||
is a client opting *itself out* of compression (`off`), which increases only that client's
|
||||
own upstream token count — there is no cross-tenant, security, or cost-shifting concern.
|
||||
|
||||
---
|
||||
|
||||
## 6. Components (units & responsibilities)
|
||||
|
||||
| Unit | Responsibility | Depends on |
|
||||
|---|---|---|
|
||||
| `resolveCompressionHeader` (`chatCore/headers.ts`) | Read the raw header value off the wire. | `getHeaderValueCaseInsensitive` |
|
||||
| `resolveBasePlan` + `headerToPlan` (`strategySelector.ts` / `resolveCompressionPlan.ts`) | Interpret the value, evaluate header-first precedence, return `{ mode, stackedPipeline, source }`. | config, combos map (pure) |
|
||||
| `DerivedPlan.source` (`deriveDefaultPlan.ts` type) | Carry which layer decided. | — |
|
||||
| `OMNIROUTE_RESPONSE_HEADERS.compression` (`shared/constants/headers.ts`) | Name the response header. | — |
|
||||
| chatCore wiring (`chatCore.ts`) | Parse, thread `header`, build id+name combo map, capture `{mode,source}`, emit response header. | all of the above |
|
||||
|
||||
---
|
||||
|
||||
## 7. Testing
|
||||
|
||||
- **Parser unit** (`headers.ts`): each value form, absent, blank, mixed casing, value with
|
||||
surrounding whitespace -> correct raw/`null`.
|
||||
- **Resolver unit** (`strategySelector` / `resolveCompressionPlan`): each form resolves the
|
||||
expected plan and `source`; header beats an active profile and a routing override;
|
||||
unknown value falls through to normal resolution; a valid value bypasses auto-trigger on a
|
||||
large prompt; `default` returns the derived Default for both `enginesExplicit` and legacy
|
||||
installs; `<combo>` matches by name and by id.
|
||||
- **Integration / fetch-capture**: per-request precedence end-to-end (same config, header
|
||||
present vs absent yields different applied plans) and the `X-OmniRoute-Compression`
|
||||
response header value.
|
||||
- **Source guard**: the resolver still has no `src/lib/db` import.
|
||||
|
||||
---
|
||||
|
||||
## 8. Scope
|
||||
|
||||
**In scope:** header parsing, top-of-precedence wiring, `source` on `DerivedPlan`, the
|
||||
response header, docs (`API_REFERENCE` / `COMPRESSION_GUIDE`), tests.
|
||||
|
||||
**Out of scope (YAGNI):** bare mode names in the header (`lite`/`aggressive`/…); panel UI for
|
||||
the header; honoring the header on non-chat paths (`combo.ts` proactive-fallback, the
|
||||
preview route). The header is a chat-request feature.
|
||||
@@ -1,730 +0,0 @@
|
||||
---
|
||||
title: "Compression Phase 3: Per-Request Header — Implementation Plan"
|
||||
version: 3.8.33
|
||||
lastUpdated: 2026-06-21
|
||||
---
|
||||
|
||||
# Compression Phase 3: Per-Request Header — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add the `x-omniroute-compression` per-request header so a client can override the resolved compression plan for a single request, taking precedence over every operator-configured layer.
|
||||
|
||||
**Architecture:** A pure header interpreter (`planFromHeader`) is evaluated at the top of the compression resolver (`resolveBasePlan` in `strategySelector.ts`); chatCore parses the header off the wire, threads it through the existing selector signature (the same way Phase 2 threaded `combos`), captures the resolved `{ mode, source }`, and emits an `X-OmniRoute-Compression` response header. The resolver stays pure (no DB import).
|
||||
|
||||
**Tech Stack:** TypeScript, Node test runner (`node --import tsx/esm --test`), better-sqlite3 (combos store, read via chatCore only), Next.js handler (`open-sse/handlers/chatCore.ts`).
|
||||
|
||||
**Spec:** `docs/compression/2026-06-21-compression-phase3-request-header-design.md`
|
||||
|
||||
---
|
||||
|
||||
## Key prior-art / facts (read before starting)
|
||||
|
||||
- The resolver entry point is `resolveBasePlan` in `open-sse/services/compression/strategySelector.ts`. It already threads a `combos` map (Phase 2). Public selectors: `selectCompressionPlan` / `selectCompressionStrategy` / `getEffectiveMode`, signature ending in `..., combos = {}`.
|
||||
- `DerivedPlan` is defined in `open-sse/services/compression/deriveDefaultPlan.ts` as `{ mode: string; stackedPipeline: Array<{ engine: string; intensity?: string }> }`.
|
||||
- `open-sse/services/compression/resolveCompressionPlan.ts` carries a **dormant** header branch (`ctx.header` + private `headerToPlan`). No caller ever passes `header` (confirmed: the only `.header` refs in compression code are those lines; no test passes it). This plan **removes** that dormant branch so header interpretation has a single home.
|
||||
- Parsing precedent: `isNoMemoryRequested` in `open-sse/handlers/chatCore/headers.ts` (uses `getHeaderValueCaseInsensitive`). Tests for that module: `tests/unit/chatcore-headers.test.ts`.
|
||||
- A combo's `id` is a `uuidv4()` (or the seeded slug `default-caveman`); `name` has **no** UNIQUE constraint (migration 042). Header `<combo>` matches **name-first** (Decision A).
|
||||
- chatCore reads request headers from `clientRawRequest?.headers`. The compression block builds `namedCombos` (~line 1373) and calls `selectCompressionStrategy` (~line 1532) then `selectCompressionPlan` (~line 1601). Response headers are built at two sites: the non-streaming JSON path (`responseHeaders` at ~line 4570, after `attachOmniRouteMetaHeaders`) and the streaming path (`responseHeaders` at ~line 4697).
|
||||
- **Decision A:** `<combo>` matched name-first (lowercased), then exact id. **Decision B:** any valid header value bypasses auto-trigger.
|
||||
- **Boundary (implement as written):** the master switch is the hard gate — when `config.enabled` is false, the request is uncompressed regardless of the header (the header redirects among configured plans only when compression is enabled). A test asserts this.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Responsibility | Change |
|
||||
|---|---|---|
|
||||
| `open-sse/services/compression/deriveDefaultPlan.ts` | `DerivedPlan` type + new `CompressionSource` union + optional `source`. | Modify |
|
||||
| `open-sse/services/compression/strategySelector.ts` | `planFromHeader` interpreter, header-first precedence in `resolveBasePlan`, `source` tagging, `formatCompressionMeta`, thread `header` through public selectors. | Modify |
|
||||
| `open-sse/services/compression/resolveCompressionPlan.ts` | Remove the dormant `header` branch + `headerToPlan` + `ResolveCtx.header`. | Modify |
|
||||
| `open-sse/handlers/chatCore/headers.ts` | `resolveCompressionHeader` parser. | Modify |
|
||||
| `src/shared/constants/headers.ts` | `compression` response-header name. | Modify |
|
||||
| `open-sse/handlers/chatCore.ts` | Parse header, add name keys to `namedCombos`, thread `header`, capture `{mode,source}`, emit response header at both sites. | Modify |
|
||||
| `tests/unit/compression/compression-header-dispatch.test.ts` | Resolver + `planFromHeader` + `source` + precedence + `formatCompressionMeta`. | Create |
|
||||
| `tests/unit/chatcore-headers.test.ts` | `resolveCompressionHeader` parsing. | Modify |
|
||||
| `docs/reference/API_REFERENCE.md`, `docs/compression/COMPRESSION_GUIDE.md` | Document the header. | Modify |
|
||||
| `config/quality/file-size-baseline.json` | Rebaseline chatCore if it crosses its pin. | Modify (if needed) |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Header interpreter, precedence, and `source` in the resolver
|
||||
|
||||
**Files:**
|
||||
- Modify: `open-sse/services/compression/deriveDefaultPlan.ts`
|
||||
- Modify: `open-sse/services/compression/strategySelector.ts`
|
||||
- Modify: `open-sse/services/compression/resolveCompressionPlan.ts`
|
||||
- Test: `tests/unit/compression/compression-header-dispatch.test.ts` (create)
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `tests/unit/compression/compression-header-dispatch.test.ts`:
|
||||
|
||||
```ts
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
selectCompressionStrategy,
|
||||
selectCompressionPlan,
|
||||
planFromHeader,
|
||||
formatCompressionMeta,
|
||||
} from "../../../open-sse/services/compression/strategySelector.ts";
|
||||
import {
|
||||
DEFAULT_COMPRESSION_CONFIG,
|
||||
type CompressionConfig,
|
||||
} from "../../../open-sse/services/compression/types.ts";
|
||||
|
||||
const combos = {
|
||||
c1: [{ engine: "rtk", intensity: "standard" }, { engine: "caveman", intensity: "full" }],
|
||||
"fast combo": [{ engine: "lite" }],
|
||||
};
|
||||
|
||||
function cfg(overrides: Partial<CompressionConfig> = {}): CompressionConfig {
|
||||
return { ...DEFAULT_COMPRESSION_CONFIG, enabled: true, ...overrides };
|
||||
}
|
||||
|
||||
// header is the 7th positional arg of selectCompressionPlan/selectCompressionStrategy.
|
||||
function planWithHeader(config: CompressionConfig, header: string | null) {
|
||||
return selectCompressionPlan(config, null, 0, undefined, undefined, combos, header);
|
||||
}
|
||||
|
||||
describe("planFromHeader (Phase 3)", () => {
|
||||
it("off => mode off, source request-header", () => {
|
||||
const p = planFromHeader(cfg(), "off", combos);
|
||||
assert.deepEqual(p, { mode: "off", stackedPipeline: [], source: "request-header" });
|
||||
});
|
||||
|
||||
it("default => the panel-derived default, ignoring the active profile", () => {
|
||||
const config = cfg({
|
||||
activeComboId: "c1",
|
||||
enginesExplicit: true,
|
||||
engines: { rtk: { enabled: true } },
|
||||
});
|
||||
const p = planFromHeader(config, "default", combos);
|
||||
assert.equal(p?.mode, "rtk"); // engines map default, NOT the c1 active profile
|
||||
assert.equal(p?.source, "request-header");
|
||||
});
|
||||
|
||||
it("engine:<id> => that single engine when enabled (case-insensitive)", () => {
|
||||
const config = cfg({ enginesExplicit: true, engines: { rtk: { enabled: true } } });
|
||||
assert.equal(planFromHeader(config, "engine:RTK", combos)?.mode, "rtk");
|
||||
});
|
||||
|
||||
it("engine:<id> => null (fall-through) when the engine is disabled", () => {
|
||||
const config = cfg({ engines: { rtk: { enabled: false } } });
|
||||
assert.equal(planFromHeader(config, "engine:rtk", combos), null);
|
||||
});
|
||||
|
||||
it("<combo> matches by name (case-insensitive) and by id", () => {
|
||||
assert.deepEqual(planFromHeader(cfg(), "FAST COMBO", combos)?.stackedPipeline, combos["fast combo"]);
|
||||
assert.deepEqual(planFromHeader(cfg(), "c1", combos)?.stackedPipeline, combos.c1);
|
||||
});
|
||||
|
||||
it("unknown value => null (fall-through)", () => {
|
||||
assert.equal(planFromHeader(cfg(), "nonsense", combos), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("header precedence in resolveBasePlan (Phase 3)", () => {
|
||||
it("a valid header beats the active profile", () => {
|
||||
const config = cfg({ activeComboId: "c1" });
|
||||
assert.equal(planWithHeader(config, "off").mode, "off");
|
||||
assert.equal(planWithHeader(config, "off").source, "request-header");
|
||||
});
|
||||
|
||||
it("a valid header beats a routing-combo override", () => {
|
||||
const config = cfg({ comboOverrides: { "route-x": "stacked" } });
|
||||
const plan = selectCompressionPlan(config, "route-x", 0, undefined, undefined, combos, "off");
|
||||
assert.equal(plan.mode, "off");
|
||||
assert.equal(plan.source, "request-header");
|
||||
});
|
||||
|
||||
it("a valid header bypasses auto-trigger (Decision B)", () => {
|
||||
const config = cfg({
|
||||
autoTriggerTokens: 1000,
|
||||
autoTriggerMode: "aggressive",
|
||||
enginesExplicit: true,
|
||||
engines: { rtk: { enabled: true } },
|
||||
});
|
||||
// Large prompt would auto-escalate to aggressive; the header pins the panel default.
|
||||
assert.equal(planWithHeader(config, "default").mode, "rtk");
|
||||
});
|
||||
|
||||
it("an unknown header falls through to the normal resolution", () => {
|
||||
const config = cfg({ activeComboId: "c1" });
|
||||
const plan = planWithHeader(config, "bogus");
|
||||
assert.equal(plan.mode, "stacked");
|
||||
assert.equal(plan.source, "active-profile");
|
||||
});
|
||||
|
||||
it("master-off beats the header (hard kill switch)", () => {
|
||||
const config = cfg({ enabled: false, engines: { rtk: { enabled: true } } });
|
||||
const plan = planWithHeader(config, "engine:rtk");
|
||||
assert.equal(plan.mode, "off");
|
||||
assert.equal(plan.source, "off");
|
||||
});
|
||||
});
|
||||
|
||||
describe("source on non-header paths", () => {
|
||||
it("routing-override / active-profile / auto-trigger / default / off", () => {
|
||||
assert.equal(
|
||||
selectCompressionPlan(cfg({ comboOverrides: { r: "lite" } }), "r", 0, undefined, undefined, combos, null).source,
|
||||
"routing-override"
|
||||
);
|
||||
assert.equal(planWithHeader(cfg({ activeComboId: "c1" }), null).source, "active-profile");
|
||||
assert.equal(
|
||||
planWithHeader(cfg({ autoTriggerTokens: 10, autoTriggerMode: "lite" }), null).source,
|
||||
"auto-trigger"
|
||||
);
|
||||
assert.equal(
|
||||
planWithHeader(cfg({ enginesExplicit: true, engines: { rtk: { enabled: true } } }), null).source,
|
||||
"default"
|
||||
);
|
||||
assert.equal(planWithHeader(cfg({ enabled: false }), null).source, "off");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatCompressionMeta", () => {
|
||||
it("renders '<mode>; source=<source>'", () => {
|
||||
assert.equal(
|
||||
formatCompressionMeta({ mode: "aggressive", stackedPipeline: [], source: "request-header" }),
|
||||
"aggressive; source=request-header"
|
||||
);
|
||||
assert.equal(formatCompressionMeta({ mode: "off", stackedPipeline: [] }), "off; source=off");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
Run: `node --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit tests/unit/compression/compression-header-dispatch.test.ts`
|
||||
Expected: FAIL — `planFromHeader`/`formatCompressionMeta` are not exported yet (import error / not a function).
|
||||
|
||||
- [ ] **Step 3: Add `CompressionSource` + `source` to `DerivedPlan`**
|
||||
|
||||
In `open-sse/services/compression/deriveDefaultPlan.ts`, replace the `DerivedPlan` interface block:
|
||||
|
||||
```ts
|
||||
export type CompressionSource =
|
||||
| "request-header"
|
||||
| "routing-override"
|
||||
| "active-profile"
|
||||
| "auto-trigger"
|
||||
| "default"
|
||||
| "off";
|
||||
|
||||
export interface DerivedPlan {
|
||||
mode: string;
|
||||
stackedPipeline: Array<{ engine: string; intensity?: string }>;
|
||||
/** Which precedence layer decided this plan (Phase 3 observability). Optional so
|
||||
* Phase 1/2 callers and snapshots are unaffected. */
|
||||
source?: CompressionSource;
|
||||
}
|
||||
```
|
||||
|
||||
(Leave the `deriveDefaultPlan` function body unchanged — it does not set `source`; the resolver tags it.)
|
||||
|
||||
- [ ] **Step 4: Remove the dormant header branch from `resolveCompressionPlan.ts`**
|
||||
|
||||
In `open-sse/services/compression/resolveCompressionPlan.ts`: delete the `header` field from `ResolveCtx`, delete the `// 1. header` block, and delete the private `headerToPlan` function. The file becomes:
|
||||
|
||||
```ts
|
||||
import { deriveDefaultPlan, type DerivedPlan } from "./deriveDefaultPlan.ts";
|
||||
|
||||
export interface ResolveCtx {
|
||||
comboId?: string | null;
|
||||
combos?: Record<string, Array<{ engine: string; intensity?: string }>>; // named combo pipelines by id
|
||||
}
|
||||
|
||||
export function resolveCompressionPlan(config: any, ctx: ResolveCtx): DerivedPlan {
|
||||
if (config?.enabled === false) return { mode: "off", stackedPipeline: [] };
|
||||
|
||||
// routing-combo override
|
||||
const ov = ctx.comboId ? config?.comboOverrides?.[ctx.comboId] : undefined;
|
||||
if (ov) return modeToPlan(ov, config);
|
||||
|
||||
// active named combo
|
||||
if (config?.activeComboId && ctx.combos?.[config.activeComboId]) {
|
||||
return { mode: "stacked", stackedPipeline: ctx.combos[config.activeComboId] };
|
||||
}
|
||||
|
||||
// derived default
|
||||
return deriveDefaultPlan(config?.engines ?? {}, config?.enabled !== false);
|
||||
}
|
||||
|
||||
function modeToPlan(mode: string, config: any): DerivedPlan {
|
||||
return mode === "stacked"
|
||||
? { mode: "stacked", stackedPipeline: config?.stackedPipeline ?? [] }
|
||||
: { mode, stackedPipeline: [] };
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Implement `planFromHeader`, `withSource`, header-first precedence, and `formatCompressionMeta` in `strategySelector.ts`**
|
||||
|
||||
In `open-sse/services/compression/strategySelector.ts`:
|
||||
|
||||
(a) Update the `DerivedPlan` import to also bring `CompressionSource`:
|
||||
|
||||
```ts
|
||||
import { deriveDefaultPlan, type DerivedPlan, type CompressionSource } from "./deriveDefaultPlan.ts";
|
||||
```
|
||||
|
||||
(b) Add these helpers (place them just above `resolveBasePlan`):
|
||||
|
||||
```ts
|
||||
/** Tags a plan with the precedence layer that produced it (Phase 3 observability). */
|
||||
function withSource(plan: DerivedPlan, source: CompressionSource): DerivedPlan {
|
||||
return { ...plan, source };
|
||||
}
|
||||
|
||||
/**
|
||||
* Interprets the `x-omniroute-compression` request header into a plan, or null when the
|
||||
* value is unrecognized (caller falls through to normal resolution). Pure.
|
||||
* off -> no compression
|
||||
* default -> the panel-derived Default (ignores active profile / routing / auto-trigger)
|
||||
* engine:<id> -> that single engine, when enabled in config.engines
|
||||
* <combo> -> a named combo, matched name-first (lowercased) then exact id (Decision A)
|
||||
*/
|
||||
export function planFromHeader(
|
||||
config: CompressionConfig,
|
||||
header: string,
|
||||
combos: NamedCombos
|
||||
): DerivedPlan | null {
|
||||
const h = header.trim();
|
||||
if (!h) return null;
|
||||
const lower = h.toLowerCase();
|
||||
|
||||
if (lower === "off") return withSource({ mode: "off", stackedPipeline: [] }, "request-header");
|
||||
|
||||
if (lower === "default") {
|
||||
// Empty combos + null comboId yields the pure panel default (no active-combo leak).
|
||||
return withSource(deriveDefaultPlanFromConfig(config, null, {}), "request-header");
|
||||
}
|
||||
|
||||
if (lower.startsWith("engine:")) {
|
||||
const id = lower.slice("engine:".length);
|
||||
const engine = config.engines?.[id];
|
||||
return engine?.enabled
|
||||
? withSource(deriveDefaultPlan({ [id]: engine }, true), "request-header")
|
||||
: null;
|
||||
}
|
||||
|
||||
const combo = combos[lower] ?? combos[h];
|
||||
return combo ? withSource({ mode: "stacked", stackedPipeline: combo }, "request-header") : null;
|
||||
}
|
||||
|
||||
/** Renders the X-OmniRoute-Compression response header value. */
|
||||
export function formatCompressionMeta(plan: DerivedPlan): string {
|
||||
return `${plan.mode}; source=${plan.source ?? "off"}`;
|
||||
}
|
||||
```
|
||||
|
||||
(c) Add a `header` parameter to `resolveBasePlan` and tag every return with `withSource`:
|
||||
|
||||
```ts
|
||||
function resolveBasePlan(
|
||||
config: CompressionConfig,
|
||||
comboId: string | null,
|
||||
estimatedTokens: number,
|
||||
combos: NamedCombos = {},
|
||||
header: string | null = null
|
||||
): DerivedPlan {
|
||||
if (!config.enabled) return withSource({ mode: "off", stackedPipeline: [] }, "off");
|
||||
|
||||
// Phase 3: an explicit, recognized header wins over every operator layer (Decision B).
|
||||
// The master switch above is the hard kill: a header cannot turn compression on.
|
||||
if (header) {
|
||||
const fromHeader = planFromHeader(config, header, combos);
|
||||
if (fromHeader) return fromHeader; // already tagged "request-header"
|
||||
}
|
||||
|
||||
const comboMode = checkComboOverride(config, comboId);
|
||||
if (comboMode) {
|
||||
return withSource(resolveCompressionPlan(config, { comboId, combos }), "routing-override");
|
||||
}
|
||||
|
||||
if (config.activeComboId && combos[config.activeComboId]) {
|
||||
return withSource(
|
||||
{ mode: "stacked", stackedPipeline: combos[config.activeComboId] },
|
||||
"active-profile"
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldAutoTrigger(config, estimatedTokens)) {
|
||||
const mode = config.autoTriggerMode ?? "lite";
|
||||
return withSource(
|
||||
mode === "stacked"
|
||||
? { mode, stackedPipeline: config.stackedPipeline ?? [] }
|
||||
: { mode, stackedPipeline: [] },
|
||||
"auto-trigger"
|
||||
);
|
||||
}
|
||||
|
||||
const plan = deriveDefaultPlanFromConfig(config, comboId, combos);
|
||||
return withSource(plan, plan.mode === "off" ? "off" : "default");
|
||||
}
|
||||
```
|
||||
|
||||
(d) Thread `header` through the public selectors (append as the last positional arg, default `null`), preserving `source` through the caching-aware adjustment:
|
||||
|
||||
```ts
|
||||
export function getEffectiveMode(
|
||||
config: CompressionConfig,
|
||||
comboId: string | null,
|
||||
estimatedTokens: number,
|
||||
combos: NamedCombos = {},
|
||||
header: string | null = null
|
||||
): CompressionMode {
|
||||
return resolveBasePlan(config, comboId, estimatedTokens, combos, header).mode as CompressionMode;
|
||||
}
|
||||
|
||||
export function selectCompressionPlan(
|
||||
config: CompressionConfig,
|
||||
comboId: string | null,
|
||||
estimatedTokens: number,
|
||||
body?: Record<string, unknown>,
|
||||
context?: CachingDetectionContext,
|
||||
combos: NamedCombos = {},
|
||||
header: string | null = null
|
||||
): DerivedPlan {
|
||||
const plan = resolveBasePlan(config, comboId, estimatedTokens, combos, header);
|
||||
if (body) {
|
||||
const ctx = detectCachingContext(body, context);
|
||||
const cacheAware = getCacheAwareStrategy(plan.mode as CompressionMode, ctx);
|
||||
return { ...plan, mode: cacheAware.strategy as CompressionMode }; // ...plan preserves source
|
||||
}
|
||||
return plan;
|
||||
}
|
||||
|
||||
export function selectCompressionStrategy(
|
||||
config: CompressionConfig,
|
||||
comboId: string | null,
|
||||
estimatedTokens: number,
|
||||
body?: Record<string, unknown>,
|
||||
context?: CachingDetectionContext,
|
||||
combos: NamedCombos = {},
|
||||
header: string | null = null
|
||||
): CompressionMode {
|
||||
return selectCompressionPlan(config, comboId, estimatedTokens, body, context, combos, header)
|
||||
.mode as CompressionMode;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run the test to verify it passes**
|
||||
|
||||
Run: `node --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit tests/unit/compression/compression-header-dispatch.test.ts`
|
||||
Expected: PASS (all cases).
|
||||
|
||||
- [ ] **Step 7: Run the full compression suite to confirm no regression**
|
||||
|
||||
Run: `node --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit "tests/unit/compression/**/*.test.ts"`
|
||||
Expected: PASS — the existing 774+ compression tests stay green (resolveCompressionPlan / strategySelector / active-combo dispatch unchanged in behavior for header-less callers).
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add open-sse/services/compression/deriveDefaultPlan.ts \
|
||||
open-sse/services/compression/strategySelector.ts \
|
||||
open-sse/services/compression/resolveCompressionPlan.ts \
|
||||
tests/unit/compression/compression-header-dispatch.test.ts
|
||||
git commit -m "feat(compression): header-first resolver + plan source (Phase 3 core)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: `resolveCompressionHeader` parser
|
||||
|
||||
**Files:**
|
||||
- Modify: `open-sse/handlers/chatCore/headers.ts`
|
||||
- Test: `tests/unit/chatcore-headers.test.ts`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Append to `tests/unit/chatcore-headers.test.ts` (keep existing imports; add `resolveCompressionHeader` to the import from `../../open-sse/handlers/chatCore/headers.ts`):
|
||||
|
||||
```ts
|
||||
import { resolveCompressionHeader } from "../../open-sse/handlers/chatCore/headers.ts";
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
describe("resolveCompressionHeader", () => {
|
||||
it("reads the raw value case-insensitively and trims it", () => {
|
||||
assert.equal(resolveCompressionHeader({ "x-omniroute-compression": " engine:rtk " }), "engine:rtk");
|
||||
assert.equal(resolveCompressionHeader(new Headers({ "X-OmniRoute-Compression": "off" })), "off");
|
||||
});
|
||||
|
||||
it("returns null when absent or blank", () => {
|
||||
assert.equal(resolveCompressionHeader({}), null);
|
||||
assert.equal(resolveCompressionHeader({ "x-omniroute-compression": " " }), null);
|
||||
assert.equal(resolveCompressionHeader(null), null);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
Run: `node --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit tests/unit/chatcore-headers.test.ts`
|
||||
Expected: FAIL — `resolveCompressionHeader` is not exported.
|
||||
|
||||
- [ ] **Step 3: Implement the parser**
|
||||
|
||||
Append to `open-sse/handlers/chatCore/headers.ts`:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Per-request compression override via the `x-omniroute-compression` header. Mirrors the
|
||||
* `x-omniroute-no-memory` convention (#4290). Returns the raw trimmed value, or null when
|
||||
* absent/blank. The resolver (planFromHeader) owns interpretation and casing rules; this
|
||||
* helper only reads the wire.
|
||||
*/
|
||||
export function resolveCompressionHeader(
|
||||
headers: Record<string, unknown> | Headers | null | undefined
|
||||
): string | null {
|
||||
const value = (getHeaderValueCaseInsensitive(headers, "x-omniroute-compression") || "").trim();
|
||||
return value || null;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the test to verify it passes**
|
||||
|
||||
Run: `node --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit tests/unit/chatcore-headers.test.ts`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add open-sse/handlers/chatCore/headers.ts tests/unit/chatcore-headers.test.ts
|
||||
git commit -m "feat(compression): resolveCompressionHeader parser (Phase 3)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: chatCore wiring + response header
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/shared/constants/headers.ts`
|
||||
- Modify: `open-sse/handlers/chatCore.ts`
|
||||
|
||||
- [ ] **Step 1: Add the response-header name constant**
|
||||
|
||||
In `src/shared/constants/headers.ts`, add the `compression` entry to `OMNIROUTE_RESPONSE_HEADERS` (alphabetical, after `cacheHit`):
|
||||
|
||||
```ts
|
||||
cacheHit: "X-OmniRoute-Cache-Hit",
|
||||
compression: "X-OmniRoute-Compression",
|
||||
costSaved: "X-OmniRoute-Cost-Saved",
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Import the parser and declare the capture variable**
|
||||
|
||||
In `open-sse/handlers/chatCore.ts`, line 7, add `resolveCompressionHeader` to the existing import from `./chatCore/headers.ts`:
|
||||
|
||||
```ts
|
||||
import { getHeaderValueCaseInsensitive, isNoMemoryRequested, resolveCompressionHeader } from "./chatCore/headers.ts";
|
||||
```
|
||||
|
||||
Near the other compression-scoped `let`s (~line 1321, beside `let cavemanOutputModeApplied = false;`), add:
|
||||
|
||||
```ts
|
||||
let compressionResponseMeta: string | null = null;
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Parse the header and add name keys to the combos map**
|
||||
|
||||
In the compression block, replace the `namedCombos` build (~line 1373) so it keys by **both** id and lowercased name, and parse the header right after:
|
||||
|
||||
```ts
|
||||
let namedCombos: Record<string, CompressionPipelineStep[]> = {};
|
||||
try {
|
||||
const { listCompressionCombos } = await import("../../src/lib/db/compressionCombos.ts");
|
||||
namedCombos = Object.fromEntries(
|
||||
listCompressionCombos().flatMap((c) => [
|
||||
[c.id, c.pipeline],
|
||||
[c.name.toLowerCase(), c.pipeline],
|
||||
])
|
||||
);
|
||||
} catch (err) {
|
||||
log?.debug?.(
|
||||
"COMPRESSION",
|
||||
"Named combos load skipped: " + (err instanceof Error ? err.message : String(err))
|
||||
);
|
||||
}
|
||||
// Phase 3: per-request override. Unknown values fall through in the resolver (never error).
|
||||
const compressionHeader = resolveCompressionHeader(clientRawRequest?.headers ?? null);
|
||||
if (compressionHeader) {
|
||||
log?.debug?.("COMPRESSION", `x-omniroute-compression header: ${compressionHeader}`);
|
||||
}
|
||||
```
|
||||
|
||||
(The active-profile lookup keys on `config.activeComboId`, always a UUID/slug id, so the added name keys are inert for it — one map serves both paths. A combo named `off`/`default` cannot be selected by name because the keyword branches run first; note this in the docs.)
|
||||
|
||||
- [ ] **Step 4: Thread the header into both selector calls**
|
||||
|
||||
Append `compressionHeader` as the last argument to `selectCompressionStrategy` (~line 1532) and `selectCompressionPlan` (~line 1601):
|
||||
|
||||
```ts
|
||||
const modeBeforeOutputTransform = selectCompressionStrategy(
|
||||
config,
|
||||
compressionComboKey,
|
||||
estimatedTokens,
|
||||
body as Record<string, unknown>,
|
||||
{ provider, targetFormat, model: effectiveModel },
|
||||
namedCombos,
|
||||
compressionHeader
|
||||
);
|
||||
```
|
||||
|
||||
```ts
|
||||
const compressionPlan = selectCompressionPlan(
|
||||
config,
|
||||
compressionComboKey,
|
||||
estimatedTokens,
|
||||
compressionInputBody,
|
||||
{ provider, targetFormat, model: effectiveModel },
|
||||
namedCombos,
|
||||
compressionHeader
|
||||
);
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Capture the resolved meta**
|
||||
|
||||
Immediately after `const mode = compressionPlan.mode as CompressionConfig["defaultMode"];` (~line 1609), add (importing `formatCompressionMeta` alongside the other strategySelector imports destructured at ~line 1349):
|
||||
|
||||
```ts
|
||||
compressionResponseMeta = formatCompressionMeta(compressionPlan);
|
||||
```
|
||||
|
||||
And add `formatCompressionMeta` to the destructured import from `../services/compression/strategySelector.ts` (~line 1349):
|
||||
|
||||
```ts
|
||||
formatCompressionMeta,
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Emit the response header at both build sites**
|
||||
|
||||
In the non-streaming JSON path, right after the `attachOmniRouteMetaHeaders(responseHeaders, { ... });` call (~line 4582), add:
|
||||
|
||||
```ts
|
||||
if (compressionResponseMeta) {
|
||||
responseHeaders[OMNIROUTE_RESPONSE_HEADERS.compression] = compressionResponseMeta;
|
||||
}
|
||||
```
|
||||
|
||||
In the streaming path, right after the `responseHeaders` object literal closes (the `};` after `"x-omniroute-request-id": pendingRequestId,`, ~line 4708), add:
|
||||
|
||||
```ts
|
||||
if (compressionResponseMeta) {
|
||||
responseHeaders[OMNIROUTE_RESPONSE_HEADERS.compression] = compressionResponseMeta;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Typecheck + cycle/source guard**
|
||||
|
||||
Run: `npm run typecheck:core`
|
||||
Expected: exit 0.
|
||||
|
||||
Run: `npm run check:cycles`
|
||||
Expected: exit 0 — the resolver still has no `src/lib/db` import (header logic lives in `strategySelector.ts`, name-key map built in chatCore).
|
||||
|
||||
- [ ] **Step 8: Run the compression suite again (no regression from wiring)**
|
||||
|
||||
Run: `node --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit "tests/unit/compression/**/*.test.ts"`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 9: Commit**
|
||||
|
||||
```bash
|
||||
git add src/shared/constants/headers.ts open-sse/handlers/chatCore.ts
|
||||
git commit -m "feat(compression): wire x-omniroute-compression header + response header (Phase 3)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Docs + file-size + full validation
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/reference/API_REFERENCE.md`
|
||||
- Modify: `docs/compression/COMPRESSION_GUIDE.md`
|
||||
- Modify (if needed): `config/quality/file-size-baseline.json`
|
||||
|
||||
- [ ] **Step 1: Document the header (API reference)**
|
||||
|
||||
In `docs/reference/API_REFERENCE.md`, near the `x-omniroute-no-memory` documentation, add a section:
|
||||
|
||||
````markdown
|
||||
### `x-omniroute-compression`
|
||||
|
||||
Per-request override of the compression plan. Highest precedence — beats the routing-combo
|
||||
override, the active profile, auto-trigger, and the panel Default. Values:
|
||||
|
||||
| Value | Effect |
|
||||
|-------|--------|
|
||||
| `off` | No compression for this request. |
|
||||
| `default` | The panel-derived Default profile (ignores the active profile). |
|
||||
| `engine:<id>` | A single engine when enabled, e.g. `engine:rtk`. |
|
||||
| `<combo>` | A named combo, matched by name (case-insensitive) first, then by id. |
|
||||
|
||||
Unknown values are ignored (the request is never rejected). The applied plan is echoed in the
|
||||
`X-OmniRoute-Compression: <mode>; source=<source>` response header. The master compression
|
||||
switch is a hard gate: when compression is disabled globally, this header cannot enable it.
|
||||
````
|
||||
|
||||
- [ ] **Step 2: Document the header (compression guide)**
|
||||
|
||||
In `docs/compression/COMPRESSION_GUIDE.md`, add a short "Per-request override" subsection mirroring the table above (one paragraph + the value table). Keep all angle-bracket tokens (`engine:<id>`, `<combo>`, `<mode>`, `<source>`) inside backticks or fenced blocks (the docs are MDX-compiled).
|
||||
|
||||
- [ ] **Step 3: Validate MDX**
|
||||
|
||||
Run: `npx fumadocs-mdx`
|
||||
Expected: `[MDX] generated files in ...ms` with no error.
|
||||
|
||||
- [ ] **Step 4: Reconcile the file-size baseline if chatCore crossed its pin**
|
||||
|
||||
Run: `npm run check:file-size`
|
||||
Expected: PASS. If it reports `chatCore.ts: <actual> > <frozen>`, update `config/quality/file-size-baseline.json`: set the `open-sse/handlers/chatCore.ts` frozen value to the reported `<actual>` and add a justification key:
|
||||
|
||||
```json
|
||||
"_rebaseline_2026_06_21_phase3_request_header": "Compression Phase 3 (x-omniroute-compression per-request header) own growth: chatCore.ts <frozen>-><actual> at the existing compression-dispatch chokepoint — parse the header (resolveCompressionHeader), add lowercased-name keys to the namedCombos map, thread it as the new last arg to selectCompressionStrategy + selectCompressionPlan, capture formatCompressionMeta(compressionPlan) into compressionResponseMeta, and emit X-OmniRoute-Compression at the two response-header build sites. The resolver stays pure (planFromHeader + source live in open-sse/services/compression/strategySelector.ts, <cap). Cohesive wiring at the existing chokepoint, mirroring the Phase 2 rebaseline; not extractable without hiding the dispatch boundary. Structural shrink of chatCore.ts tracked in #3501. Covered by tests/unit/compression/compression-header-dispatch.test.ts + tests/unit/chatcore-headers.test.ts.",
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run the full validation suite**
|
||||
|
||||
Run each and confirm exit 0 / green:
|
||||
- `npm run typecheck:core`
|
||||
- `npm run lint`
|
||||
- `npm run check:cycles`
|
||||
- `npm run check:file-size`
|
||||
- `node --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit "tests/unit/compression/**/*.test.ts"` (compression subset) and `tests/unit/chatcore-headers.test.ts`
|
||||
- `npm run test:unit` (full unit suite — confirms no cross-suite regression)
|
||||
|
||||
Expected: all green. Fix any red before proceeding (do not weaken assertions to pass).
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/reference/API_REFERENCE.md docs/compression/COMPRESSION_GUIDE.md config/quality/file-size-baseline.json
|
||||
git commit -m "docs(compression): document x-omniroute-compression header + file-size reconcile (Phase 3)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review (run by the author after writing)
|
||||
|
||||
**1. Spec coverage**
|
||||
- §2 contract (off/default/engine/combo, unknown→ignore) → Task 1 `planFromHeader` + tests. ✓
|
||||
- Decision A (name-first combo) → Task 1 `planFromHeader` `combos[lower] ?? combos[h]` + Task 3 name-key map + test. ✓
|
||||
- Decision B (bypass auto-trigger) → Task 1 header-first in `resolveBasePlan` + test. ✓
|
||||
- §3.1 precedence (header at top) → Task 1 + precedence tests (beats active profile, routing override). ✓
|
||||
- §3.2 `source` on `DerivedPlan` → Task 1 `CompressionSource` + `withSource` + tests. ✓
|
||||
- §3.3 parser → Task 2. ✓
|
||||
- §3.4 threading + id+name map → Task 1 (signatures) + Task 3 (chatCore). ✓
|
||||
- §4 response header → Task 3 (constant + two sites) + `formatCompressionMeta` test. ✓
|
||||
- §5 error handling (unknown→fall-through; disabled engine→fall-through; unconditional gating; master-off boundary) → Task 1 tests. ✓
|
||||
- §7 testing (parser, resolver, precedence, source guard) → Tasks 1, 2; source guard via `check:cycles` in Task 3/4. ✓
|
||||
- §8 scope (no bare modes, no UI, chat-only) → respected; `planFromHeader` rejects bare mode names (they fall through), no UI/non-chat files touched. ✓
|
||||
|
||||
**2. Placeholder scan:** none — every code step shows full code; commands have expected output.
|
||||
|
||||
**3. Type consistency:** `DerivedPlan`/`CompressionSource` defined in Task 1 Step 3, imported in Step 5; `planFromHeader(config, header, combos)` / `formatCompressionMeta(plan)` signatures identical across Task 1 tests, implementation, and Task 3 usage; `resolveCompressionHeader(headers)` identical in Task 2 and Task 3; `header` appended as the same final positional arg in `getEffectiveMode`/`selectCompressionPlan`/`selectCompressionStrategy`. ✓
|
||||
|
||||
**Note for the implementer:** `typecheck:core`, `check:cycles`, `check:file-size`, and `lint` are existing `package.json` scripts. There is **no** `test:compression` script — run the compression subset directly with the node test runner (`node --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit "tests/unit/compression/**/*.test.ts"`), or run the whole unit suite with `npm run test:unit`. Confirm any other script name in `package.json` before running.
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Compression Engines"
|
||||
version: 3.8.2
|
||||
lastUpdated: 2026-06-17
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Compression Engines
|
||||
@@ -45,6 +45,18 @@ A separate registry compresses MCP tool description metadata at registry-level
|
||||
`open-sse/mcp-server/descriptionCompressor.ts` and [MCP-SERVER.md](../frameworks/MCP-SERVER.md). It reuses
|
||||
Caveman rules but operates on tool metadata, not request payloads.
|
||||
|
||||
### Additional built-in engines
|
||||
|
||||
Beyond Caveman, RTK, and LLMLingua-2, the registry ships several specialized lossless /
|
||||
structural engines (used by stacked pipelines, the playground, and tests):
|
||||
|
||||
| Engine | Id | What it does |
|
||||
| ------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| CCR | `ccr` | Content-Compress-Retrieve (H4): replaces large contiguous text blocks with content-addressed references, so repeated/large blocks are sent once and referenced thereafter. |
|
||||
| headroom | `headroom` | SmartCrusher (H3 + N5): lossless tabular compaction of homogeneous JSON-array payloads into a columnar `[N rows]` form. |
|
||||
| ionizer | `ionizer` | Head/middle/tail row sampling for very large homogeneous blocks, storing the elided middle as a CCR content-addressed reference. |
|
||||
| session-dedup | `session-dedup` | Content-addressed cross-turn deduplication (TokenMizer-inspired): elides text already seen in earlier turns of the same session. |
|
||||
|
||||
## Caveman
|
||||
|
||||
Caveman mode focuses on semantic condensation of normal prose:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "🗜️ Prompt Compression Guide — OmniRoute"
|
||||
version: 3.8.2
|
||||
lastUpdated: 2026-05-13
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# 🗜️ Prompt Compression Guide — OmniRoute
|
||||
@@ -181,8 +181,8 @@ Combo: "free-forever"
|
||||
Compression Combo: "coding-agent-stack"
|
||||
Pipeline: RTK -> Caveman
|
||||
Targets:
|
||||
1. gc/gemini-3-flash
|
||||
2. if/kimi-k2-thinking
|
||||
1. if/kimi-k2-thinking
|
||||
2. qw/qwen3-coder-plus
|
||||
```
|
||||
|
||||
This lets you use stacked compression on free/coding providers while keeping lite mode on paid
|
||||
@@ -196,12 +196,12 @@ auto-trigger, and the panel Default. Unknown values are ignored (the request is
|
||||
the global master switch still gates everything: when compression is off globally, the header cannot
|
||||
turn it on. Values:
|
||||
|
||||
| Value | Effect |
|
||||
|-------|--------|
|
||||
| `off` | No compression for this request. |
|
||||
| `default` | The panel-derived Default profile (ignores the active profile). |
|
||||
| `engine:<id>` | A single engine when enabled, e.g. `engine:rtk`. |
|
||||
| `<combo>` | A named combo, matched by name (case-insensitive) first, then by id. |
|
||||
| Value | Effect |
|
||||
| ------------- | -------------------------------------------------------------------- |
|
||||
| `off` | No compression for this request. |
|
||||
| `default` | The panel-derived Default profile (ignores the active profile). |
|
||||
| `engine:<id>` | A single engine when enabled, e.g. `engine:rtk`. |
|
||||
| `<combo>` | A named combo, matched by name (case-insensitive) first, then by id. |
|
||||
|
||||
The applied plan is echoed back in the `X-OmniRoute-Compression: <mode>; source=<source>` response
|
||||
header, where `<source>` is one of `request-header`, `routing-override`, `active-profile`,
|
||||
@@ -273,11 +273,11 @@ Every compressed request includes stats in the server logs:
|
||||
|
||||
## Phase Roadmap
|
||||
|
||||
| Phase | Modes | Status |
|
||||
| ------- | ------------------------------------ | ---------- |
|
||||
| Phase 1 | Off, Lite | ✅ Shipped |
|
||||
| Phase 2 | Standard, Aggressive, Ultra | ✅ Shipped |
|
||||
| Phase 3 | RTK, Stacked, Compression Combos | ✅ Shipped |
|
||||
| Phase | Modes | Status |
|
||||
| ------- | -------------------------------------------------------------------- | ---------- |
|
||||
| Phase 1 | Off, Lite | ✅ Shipped |
|
||||
| Phase 2 | Standard, Aggressive, Ultra | ✅ Shipped |
|
||||
| Phase 3 | RTK, Stacked, Compression Combos | ✅ Shipped |
|
||||
| Phase 4 | Output Styles, SLM-tier Ultra, adaptive context-budget, eval harness | ✅ Shipped |
|
||||
|
||||
---
|
||||
@@ -316,7 +316,10 @@ The `cachingAware.ts` module solves this by **detecting caching context** and
|
||||
#### Code example
|
||||
|
||||
```ts
|
||||
import { detectCachingContext, getCacheAwareStrategy } from "@omniroute/open-sse/services/compression/cachingAware";
|
||||
import {
|
||||
detectCachingContext,
|
||||
getCacheAwareStrategy,
|
||||
} from "@omniroute/open-sse/services/compression/cachingAware";
|
||||
|
||||
const body = {
|
||||
model: "anthropic/claude-sonnet-4.5",
|
||||
@@ -335,6 +338,7 @@ const strategy = getCacheAwareStrategy("aggressive", ctx);
|
||||
|
||||
Cache-aware compression is **always on** — no configuration needed. It only kicks in
|
||||
when:
|
||||
|
||||
- The request has `cache_control` markers
|
||||
- The target provider supports prompt caching (Anthropic, OpenAI, etc.)
|
||||
|
||||
@@ -361,9 +365,9 @@ const messages = [
|
||||
];
|
||||
|
||||
const { messages: aged, saved } = applyAging(messages, {
|
||||
verbatim: 3, // First 3 turns: verbatim
|
||||
light: 8, // Turns 4-8: lite compression
|
||||
moderate: 20, // Turns 9-20: caveman compression
|
||||
verbatim: 3, // First 3 turns: verbatim
|
||||
light: 8, // Turns 4-8: lite compression
|
||||
moderate: 20, // Turns 9-20: caveman compression
|
||||
// Turns 21+: heavy summarization
|
||||
});
|
||||
|
||||
@@ -374,6 +378,7 @@ const { messages: aged, saved } = applyAging(messages, {
|
||||
|
||||
Progressive aging is **always on** for `aggressive` and `ultra` modes. It's
|
||||
particularly effective for:
|
||||
|
||||
- Long-running coding sessions
|
||||
- Multi-day conversations
|
||||
- Agentic workflows with many tool calls
|
||||
@@ -386,9 +391,11 @@ model itself produce compressed, terse output (a "caveman" style).
|
||||
#### How it works
|
||||
|
||||
Instead of compressing the input, this mode adds a system prompt like:
|
||||
|
||||
> "Reply in minimal words. Skip pleasantries. Use short sentences."
|
||||
|
||||
This works particularly well for:
|
||||
|
||||
- Code generation (terser output = fewer tokens)
|
||||
- Quick Q&A (no need for elaborate explanations)
|
||||
- Batch processing (maximize throughput)
|
||||
@@ -396,6 +403,7 @@ This works particularly well for:
|
||||
#### When to use
|
||||
|
||||
Caveman output mode is **opt-in** — set it via the combo config:
|
||||
|
||||
```json
|
||||
{
|
||||
"strategy": "auto",
|
||||
@@ -441,11 +449,13 @@ Input (1000 tokens)
|
||||
#### When to use
|
||||
|
||||
Use stacked mode for:
|
||||
|
||||
- Tool-heavy workflows (agentic coding, research)
|
||||
- Cost-sensitive batch processing
|
||||
- When you need maximum token savings
|
||||
|
||||
Configure via combo:
|
||||
|
||||
```json
|
||||
{
|
||||
"strategy": "auto",
|
||||
@@ -483,6 +493,7 @@ for different use cases:
|
||||
```
|
||||
|
||||
This is useful for:
|
||||
|
||||
- **Coding combos**: Use `aggressive` mode for long sessions
|
||||
- **Quick Q&A combos**: Use `lite` mode for fast responses
|
||||
- **Tool-heavy combos**: Use `stacked` mode for max savings
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Compression Language Packs"
|
||||
version: 3.8.2
|
||||
lastUpdated: 2026-05-13
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Compression Language Packs
|
||||
@@ -25,11 +25,12 @@ Current shipped packs (verified against `rules/` directory contents):
|
||||
| English | `rules/en/` | `context`, `dedup`, `filler`, `structural`, `ultra` |
|
||||
| Spanish | `rules/es/` | `context`, `dedup`, `filler`, `structural`, `ultra` |
|
||||
| Portuguese (Brazil) | `rules/pt-BR/` | `context`, `dedup`, `filler`, `structural`, `ultra` |
|
||||
| Indonesian | `rules/id/` | `context`, `dedup`, `filler`, `structural`, `ultra` |
|
||||
| German | `rules/de/` | `context`, `filler`, `structural` |
|
||||
| French | `rules/fr/` | `context`, `filler`, `structural` |
|
||||
| Japanese | `rules/ja/` | `context`, `filler`, `structural` |
|
||||
|
||||
> **Parity note:** `en`, `es`, and `pt-BR` packs have the full 5 categories; `de`, `fr`, `ja` ship 3 categories. The missing `dedup` and `ultra` categories silently fall back to the English built-ins. Contributions welcome to add `dedup.json` and `ultra.json` for the smaller packs.
|
||||
> **Parity note:** `en`, `es`, `pt-BR`, and `id` packs have the full 5 categories; `de`, `fr`, `ja` ship 3 categories. The missing `dedup` and `ultra` categories silently fall back to the English built-ins. Contributions welcome to add `dedup.json` and `ultra.json` for the smaller packs.
|
||||
>
|
||||
> The `pt-BR` pack is based on **[Troglodita](https://github.com/leninejunior/troglodita)** by Lenine Júnior — a compression system designed from scratch for Brazilian Portuguese grammar (pleonasm reduction, PT-BR filler removal, technical abbreviations for the dev BR community).
|
||||
>
|
||||
@@ -54,12 +55,12 @@ Compression settings can include:
|
||||
"enabled": true,
|
||||
"defaultLanguage": "en",
|
||||
"autoDetect": true,
|
||||
"enabledPacks": ["en", "pt-BR", "es", "de", "fr", "ja"]
|
||||
"enabledPacks": ["en", "pt-BR", "es", "id", "de", "fr", "ja"]
|
||||
},
|
||||
"cavemanConfig": {
|
||||
"language": "en",
|
||||
"autoDetectLanguage": true,
|
||||
"enabledLanguagePacks": ["en", "pt-BR", "es", "de", "fr", "ja"]
|
||||
"enabledLanguagePacks": ["en", "pt-BR", "es", "id", "de", "fr", "ja"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Compression Rules Format"
|
||||
version: 3.8.2
|
||||
lastUpdated: 2026-05-13
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Compression Rules Format
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
---
|
||||
title: "Delegated Context Editing (Anthropic)"
|
||||
version: 3.8.31
|
||||
lastUpdated: 2026-06-20
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Delegated Context Editing (Anthropic)
|
||||
|
||||
Delegated **Context Editing** is a Claude-only context-management feature. Unlike OmniRoute's local
|
||||
compression engines (Caveman, RTK, LLMLingua, stacked pipelines) — which rewrite the request body
|
||||
*before* it leaves the proxy — Context Editing asks the **provider** to clear stale
|
||||
_before_ it leaves the proxy — Context Editing asks the **provider** to clear stale
|
||||
tool-use / tool-result blocks from its own running context window. OmniRoute only attaches a body
|
||||
parameter (`context_management.edits[]`); Claude does the actual clearing against its own tokenizer.
|
||||
|
||||
@@ -114,7 +114,10 @@ if (
|
||||
!contextEditingDisabled &&
|
||||
transformedBody?.context_management !== undefined
|
||||
) {
|
||||
const errText = await response.clone().text().catch(() => "");
|
||||
const errText = await response
|
||||
.clone()
|
||||
.text()
|
||||
.catch(() => "");
|
||||
if (/context[_-]management|context editing/i.test(errText)) {
|
||||
contextEditingDisabled = true;
|
||||
delete transformedBody.context_management;
|
||||
@@ -172,14 +175,14 @@ So delegated clearing shows up in compression analytics alongside the local engi
|
||||
|
||||
## Relationship to the local compression engines
|
||||
|
||||
| Aspect | Local engines (Caveman / RTK / LLMLingua / stacked) | Delegated Context Editing |
|
||||
| ----------------- | --------------------------------------------------- | ------------------------------------------ |
|
||||
| Where it runs | In OmniRoute, before the request leaves the proxy | In the provider (Claude), server-side |
|
||||
| What it edits | Prompt / context / tool-result text | Old tool-use / tool-result blocks |
|
||||
| Aspect | Local engines (Caveman / RTK / LLMLingua / stacked) | Delegated Context Editing |
|
||||
| ----------------- | --------------------------------------------------- | ------------------------------------------- |
|
||||
| Where it runs | In OmniRoute, before the request leaves the proxy | In the provider (Claude), server-side |
|
||||
| What it edits | Prompt / context / tool-result text | Old tool-use / tool-result blocks |
|
||||
| Provider scope | All providers | `claude` + `anthropic-compatible-cc-*` only |
|
||||
| Toggle | Compression mode settings | `contextEditing.enabled` |
|
||||
| Failure mode | Fail-open (original text) | 400-fallback: strip param, retry once |
|
||||
| Savings telemetry | `engine: <engine id>` | `engine: "context-editing"` |
|
||||
| Toggle | Compression mode settings | `contextEditing.enabled` |
|
||||
| Failure mode | Fail-open (original text) | 400-fallback: strip param, retry once |
|
||||
| Savings telemetry | `engine: <engine id>` | `engine: "context-editing"` |
|
||||
|
||||
The two are complementary: local engines compress the bytes OmniRoute sends; Context Editing lets
|
||||
Claude prune the running context across turns. They can be enabled together.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Extending the Compression Pipeline"
|
||||
version: 3.8.16
|
||||
lastUpdated: 2026-06-08
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Extending the Compression Pipeline
|
||||
@@ -9,6 +9,7 @@ lastUpdated: 2026-06-08
|
||||
> **TL;DR**: OmniRoute's compression engine is **pluggable** — you can register custom engines, ship language packs for new languages, and compose stacked pipelines. This guide shows how.
|
||||
|
||||
**Related guides:**
|
||||
|
||||
- [COMPRESSION_GUIDE.md](./COMPRESSION_GUIDE.md) — Full pipeline overview
|
||||
- [COMPRESSION_ENGINES.md](./COMPRESSION_ENGINES.md) — Engine registry and built-in engines
|
||||
- [RTK_COMPRESSION.md](./RTK_COMPRESSION.md) — RTK engine and custom filters
|
||||
@@ -20,11 +21,11 @@ lastUpdated: 2026-06-08
|
||||
|
||||
The compression system has **3 extension points**:
|
||||
|
||||
| Extension point | Use case | Difficulty |
|
||||
|-----------------|----------|------------|
|
||||
| **Custom engine** | Add a brand-new compression algorithm (e.g., domain-specific summarizer) | Advanced |
|
||||
| **Language pack** | Add support for a new natural language (e.g., Hindi, Arabic) | Medium |
|
||||
| **Stacked pipeline** | Compose existing engines in a custom order | Beginner |
|
||||
| Extension point | Use case | Difficulty |
|
||||
| -------------------- | ------------------------------------------------------------------------ | ---------- |
|
||||
| **Custom engine** | Add a brand-new compression algorithm (e.g., domain-specific summarizer) | Advanced |
|
||||
| **Language pack** | Add support for a new natural language (e.g., Hindi, Arabic) | Medium |
|
||||
| **Stacked pipeline** | Compose existing engines in a custom order | Beginner |
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
@@ -63,15 +64,15 @@ The engine interface (`open-sse/services/compression/engines/types.ts`) is the c
|
||||
|
||||
```ts
|
||||
interface CompressionEngine {
|
||||
id: string; // Unique engine ID
|
||||
name: string; // Display name
|
||||
description: string; // Short description
|
||||
icon: string; // Icon (emoji or URL)
|
||||
targets: CompressionEngineTarget[]; // ["messages", "tool_results", "code_blocks"]
|
||||
stackable: boolean; // Can be used in a stacked pipeline
|
||||
stackPriority: number; // Order in stacked pipelines (lower = earlier)
|
||||
id: string; // Unique engine ID
|
||||
name: string; // Display name
|
||||
description: string; // Short description
|
||||
icon: string; // Icon (emoji or URL)
|
||||
targets: CompressionEngineTarget[]; // ["messages", "tool_results", "code_blocks"]
|
||||
stackable: boolean; // Can be used in a stacked pipeline
|
||||
stackPriority: number; // Order in stacked pipelines (lower = earlier)
|
||||
metadata: CompressionEngineMetadata;
|
||||
|
||||
|
||||
apply(body, options?): CompressionResult;
|
||||
compress(body, config?): CompressionResult;
|
||||
getConfigSchema(): EngineConfigField[];
|
||||
@@ -83,7 +84,7 @@ interface CompressionEngine {
|
||||
|
||||
The simplest possible engine — strip extra whitespace from messages.
|
||||
|
||||
```ts
|
||||
````ts
|
||||
import type { CompressionEngine } from "omniroute/compression/engines/types";
|
||||
import { registerCompressionEngine } from "omniroute/compression/engines/registry";
|
||||
|
||||
@@ -107,8 +108,8 @@ const whitespaceEngine: CompressionEngine = {
|
||||
icon: "📝",
|
||||
targets: ["messages", "tool_results"],
|
||||
stackable: true,
|
||||
stackPriority: 100, // Run AFTER caveman/rtk
|
||||
|
||||
stackPriority: 100, // Run AFTER caveman/rtk
|
||||
|
||||
metadata: {
|
||||
id: "whitespace",
|
||||
name: "Whitespace Stripper",
|
||||
@@ -118,11 +119,11 @@ const whitespaceEngine: CompressionEngine = {
|
||||
supportsPreview: true,
|
||||
stable: true,
|
||||
},
|
||||
|
||||
|
||||
apply(body, options) {
|
||||
return this.compress(body, options?.config);
|
||||
},
|
||||
|
||||
|
||||
compress(body, config = {}) {
|
||||
let originalLength = 0;
|
||||
let compressedLength = 0;
|
||||
@@ -169,7 +170,6 @@ const whitespaceEngine: CompressionEngine = {
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
getConfigSchema() {
|
||||
return [
|
||||
{
|
||||
@@ -181,7 +181,7 @@ const whitespaceEngine: CompressionEngine = {
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
|
||||
validateConfig(config) {
|
||||
if (config.preserveCodeBlocks !== undefined && typeof config.preserveCodeBlocks !== "boolean") {
|
||||
return { valid: false, errors: ["preserveCodeBlocks must be a boolean"] };
|
||||
@@ -192,7 +192,7 @@ const whitespaceEngine: CompressionEngine = {
|
||||
|
||||
// Register globally
|
||||
registerCompressionEngine(whitespaceEngine);
|
||||
```
|
||||
````
|
||||
|
||||
### Where to Place Custom Engines
|
||||
|
||||
@@ -205,7 +205,10 @@ Or load programmatically from a plugin:
|
||||
|
||||
```ts
|
||||
// In your plugin
|
||||
import { registerCompressionEngine, unregisterCompressionEngine } from "@omniroute/open-sse/services/compression/engines/registry";
|
||||
import {
|
||||
registerCompressionEngine,
|
||||
unregisterCompressionEngine,
|
||||
} from "@omniroute/open-sse/services/compression/engines/registry";
|
||||
import { myEngine } from "./engines/my-engine";
|
||||
|
||||
export default definePlugin({
|
||||
@@ -258,15 +261,15 @@ Each rule has this shape (from `open-sse/services/compression/ruleLoader.ts`):
|
||||
|
||||
```ts
|
||||
interface FileRule {
|
||||
name: string; // Human-readable name (kebab-case)
|
||||
pattern: string; // JavaScript regex pattern
|
||||
replacement?: string; // What to replace the match with
|
||||
replacementMap?: Record<string, string>; // OR a key→replacement map
|
||||
flags?: string; // Regex flags ("gi" typically)
|
||||
name: string; // Human-readable name (kebab-case)
|
||||
pattern: string; // JavaScript regex pattern
|
||||
replacement?: string; // What to replace the match with
|
||||
replacementMap?: Record<string, string>; // OR a key→replacement map
|
||||
flags?: string; // Regex flags ("gi" typically)
|
||||
context?: "all" | "user" | "system" | "assistant";
|
||||
category?: "filler" | "context" | "structural" | "dedup" | "terse" | "ultra";
|
||||
minIntensity?: "lite" | "full" | "ultra"; // Skip below this intensity
|
||||
description?: string; // Documentation
|
||||
minIntensity?: "lite" | "full" | "ultra"; // Skip below this intensity
|
||||
description?: string; // Documentation
|
||||
}
|
||||
```
|
||||
|
||||
@@ -394,15 +397,19 @@ The output of engine N becomes the input of engine N+1.
|
||||
OmniRoute selects **ONE mode per request** based on configuration, auto-trigger thresholds, and combo overrides.
|
||||
The available modes are defined in `open-sse/services/compression/types.ts` (type `CompressionMode`):
|
||||
|
||||
| Mode | Engines | Use case |
|
||||
|------|---------|----------|
|
||||
| `off` | None | Disable all compression |
|
||||
| `rtk` | RTK only | Command-output heavy sessions (80%+ savings) |
|
||||
| `lite` | Lite only | Conservative compression (fast, safe) |
|
||||
| `standard` | Caveman | Prose compression with language packs |
|
||||
| `aggressive` | Caveman + Aggressive | Aggressive prose + aggressive final pass |
|
||||
| `ultra` | Ultra | Maximum compression (lossy, last resort) |
|
||||
| `stacked` | Custom pipeline | Compose engines in any order (see below) |
|
||||
| Mode | Engines | Use case |
|
||||
| ------------ | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `off` | None | Disable all compression |
|
||||
| `rtk` | RTK only | Command-output heavy sessions (80%+ savings) |
|
||||
| `lite` | Lite only | Conservative compression (fast, safe) |
|
||||
| `standard` | Caveman | Prose compression with language packs |
|
||||
| `aggressive` | Caveman + Aggressive | Aggressive prose + aggressive final pass |
|
||||
| `ultra` | Ultra | Maximum compression (lossy, last resort). Optionally routed through the **LLMLingua-2** SLM engine when `ultra.modelPath` is set (fail-opens to the rule-based path when the model is unavailable). |
|
||||
| `stacked` | Custom pipeline | Compose engines in any order (see below) |
|
||||
|
||||
> Beyond the mode engines above, the registry also ships specialized stackable engines —
|
||||
> **CCR**, **headroom**, **ionizer**, and **session-dedup** — documented in
|
||||
> [COMPRESSION_ENGINES.md](./COMPRESSION_ENGINES.md#additional-built-in-engines).
|
||||
|
||||
Mode selection is determined by `getEffectiveMode()` in `open-sse/services/compression/strategySelector.ts`:
|
||||
|
||||
@@ -456,16 +463,17 @@ The metadata is **read-only** — engines cannot mutate the request context, onl
|
||||
|
||||
### Execution Order Gotchas
|
||||
|
||||
| Engine order | Effect |
|
||||
|--------------|--------|
|
||||
| RTK → Caveman → Lite | **Recommended** (strips noise first, then language, then whitespace) |
|
||||
| Lite → RTK → Caveman | Bad — Lite strips whitespace from raw output, making RTK pattern matching fail |
|
||||
| Caveman → RTK | Bad — Caveman may rewrite text in ways that RTK doesn't recognize |
|
||||
| Any order with `tool_results` first | Better — tool output is the noisiest content |
|
||||
| Engine order | Effect |
|
||||
| ----------------------------------- | ------------------------------------------------------------------------------ |
|
||||
| RTK → Caveman → Lite | **Recommended** (strips noise first, then language, then whitespace) |
|
||||
| Lite → RTK → Caveman | Bad — Lite strips whitespace from raw output, making RTK pattern matching fail |
|
||||
| Caveman → RTK | Bad — Caveman may rewrite text in ways that RTK doesn't recognize |
|
||||
| Any order with `tool_results` first | Better — tool output is the noisiest content |
|
||||
|
||||
### When NOT to Stack
|
||||
|
||||
Stacking isn't always better:
|
||||
|
||||
- **Simple messages** (no tool output) — single Caveman or Lite is enough
|
||||
- **Cost-sensitive** — each engine adds ~5-50ms latency
|
||||
- **Specific tools** — RTK alone is usually sufficient for shell output
|
||||
@@ -533,13 +541,14 @@ To drive it from config, set `mode: "stacked"` and provide the step array under
|
||||
|
||||
## Reference: Built-in Engines
|
||||
|
||||
| Engine ID | Stackable | Default stackPriority | Targets |
|
||||
|-----------|-----------|-----------------------|---------|
|
||||
| `lite` | Yes | 5 | messages, tool_results |
|
||||
| `rtk` | Yes | 10 | tool_results |
|
||||
| `standard` (caveman) | Yes | 20 | messages, tool_results, code_blocks |
|
||||
| `aggressive` | Yes | 30 | messages |
|
||||
| `ultra` | Yes | 40 | messages, code_blocks |
|
||||
| Engine ID | Stackable | Default stackPriority | Targets |
|
||||
| -------------------- | --------- | --------------------- | ----------------------------------- |
|
||||
| `lite` | Yes | 5 | messages, tool_results |
|
||||
| `rtk` | Yes | 10 | tool_results |
|
||||
| `standard` (caveman) | Yes | 20 | messages, tool_results, code_blocks |
|
||||
| `aggressive` | Yes | 30 | messages |
|
||||
| `ultra` | Yes | 40 | messages, code_blocks |
|
||||
|
||||
### See Also
|
||||
|
||||
- [COMPRESSION_GUIDE.md](./COMPRESSION_GUIDE.md) — Pipeline overview
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "RTK Compression"
|
||||
version: 3.8.31
|
||||
lastUpdated: 2026-06-20
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# RTK Compression
|
||||
@@ -109,7 +109,7 @@ them too, especially when they are shared across projects.
|
||||
RTK collapses duplicate lines at two independent layers:
|
||||
|
||||
1. **Per-filter `deduplicate` (opt-in, default `false`).** A filter can set `rules.deduplicate: true`
|
||||
to collapse consecutive duplicate lines *within that filter's matched output*, before truncation.
|
||||
to collapse consecutive duplicate lines _within that filter's matched output_, before truncation.
|
||||
This runs inside `lineFilter.ts`. For legacy filters, it is auto-enabled when the filter defines
|
||||
`collapsePatterns`. Schema: `deduplicate: z.boolean().default(false)` in
|
||||
`open-sse/services/compression/engines/rtk/filterSchema.ts`.
|
||||
@@ -124,7 +124,7 @@ output), so the two compose without double-counting.
|
||||
## Line Grouping (`enableGrouping`)
|
||||
|
||||
When `rtkConfig.enableGrouping` is `true` (default `false`), RTK runs an additional `groupSimilarLines`
|
||||
pass over the post-dedup result that collapses runs of *near-equivalent* (not byte-identical)
|
||||
pass over the post-dedup result that collapses runs of _near-equivalent_ (not byte-identical)
|
||||
consecutive lines. `rtkConfig.groupingThreshold` (default `3`) is the minimum run length that triggers
|
||||
grouping. This is the structural counterpart to `deduplicateThreshold`: dedup handles exact repeats,
|
||||
grouping handles "the same shape with small differences". Both flags are part of the `rtkConfig` JSON
|
||||
@@ -201,13 +201,13 @@ the SQLite `key_value` table under `namespace = "compression"`, `key = "rtkConfi
|
||||
— including `enableGrouping`, `groupingThreshold`, `stripCodeComments`, and `preserveDocstrings` —
|
||||
round-trips through the same store and survives a restart.
|
||||
|
||||
| Key | Default | Purpose |
|
||||
| ---------------------- | ------- | ----------------------------------------------------------------------------- |
|
||||
| `deduplicateThreshold` | `3` | Engine-wide: min consecutive identical lines to collapse (bounded 2–100) |
|
||||
| `enableGrouping` | `false` | Opt-in: collapse runs of near-equivalent consecutive lines |
|
||||
| `groupingThreshold` | `3` | Min consecutive similar-line run that triggers grouping |
|
||||
| `stripCodeComments` | `false` | Opt-in: remove comments from fenced code blocks (needs `applyToCodeBlocks`) |
|
||||
| `preserveDocstrings` | `true` | When stripping comments, keep JSDoc/`/** … */` blocks |
|
||||
| Key | Default | Purpose |
|
||||
| ---------------------- | ------- | --------------------------------------------------------------------------- |
|
||||
| `deduplicateThreshold` | `3` | Engine-wide: min consecutive identical lines to collapse (bounded 2–100) |
|
||||
| `enableGrouping` | `false` | Opt-in: collapse runs of near-equivalent consecutive lines |
|
||||
| `groupingThreshold` | `3` | Min consecutive similar-line run that triggers grouping |
|
||||
| `stripCodeComments` | `false` | Opt-in: remove comments from fenced code blocks (needs `applyToCodeBlocks`) |
|
||||
| `preserveDocstrings` | `true` | When stripping comments, keep JSDoc/`/** … */` blocks |
|
||||
|
||||
## API
|
||||
|
||||
@@ -318,11 +318,11 @@ RTK supports **3 intensity levels** that trade off between **compression aggress
|
||||
|
||||
### The 3 Levels
|
||||
|
||||
| Level | Truncation threshold | Token savings | Risk | Best for |
|
||||
|-------|---------------------|---------------|------|----------|
|
||||
| `minimal` | 24 lines per section | ~20-40% | Very low | Production with critical context |
|
||||
| `standard` (default) | 24 lines per section | ~50-70% | Low | Daily coding sessions |
|
||||
| `aggressive` | 16 lines per section | ~70-90% | Medium | Long sessions, max savings |
|
||||
| Level | Truncation threshold | Token savings | Risk | Best for |
|
||||
| -------------------- | -------------------- | ------------- | -------- | -------------------------------- |
|
||||
| `minimal` | 24 lines per section | ~20-40% | Very low | Production with critical context |
|
||||
| `standard` (default) | 24 lines per section | ~50-70% | Low | Daily coding sessions |
|
||||
| `aggressive` | 16 lines per section | ~70-90% | Medium | Long sessions, max savings |
|
||||
|
||||
### Where the Truncation Happens
|
||||
|
||||
@@ -338,15 +338,15 @@ Both the **head** and **tail** of each section are preserved; middle content is
|
||||
|
||||
### What Stays vs. What Gets Cut
|
||||
|
||||
| Content | minimal | standard | aggressive |
|
||||
|---------|---------|----------|------------|
|
||||
| Errors / stack traces | ✅ preserved | ✅ preserved | ✅ preserved |
|
||||
| Test failures | ✅ preserved | ✅ preserved | ✅ preserved |
|
||||
| Build errors | ✅ preserved | ✅ preserved | ✅ preserved |
|
||||
| Test passes (verbose) | ✅ preserved | 🟡 collapsed | 🟡 collapsed |
|
||||
| Routine output (info logs) | 🟡 collapsed | 🟡 collapsed | ❌ dropped |
|
||||
| Progress bars | 🟡 collapsed | ❌ dropped | ❌ dropped |
|
||||
| Banner / ASCII art | 🟡 collapsed | ❌ dropped | ❌ dropped |
|
||||
| Content | minimal | standard | aggressive |
|
||||
| -------------------------- | ------------ | ------------ | ------------ |
|
||||
| Errors / stack traces | ✅ preserved | ✅ preserved | ✅ preserved |
|
||||
| Test failures | ✅ preserved | ✅ preserved | ✅ preserved |
|
||||
| Build errors | ✅ preserved | ✅ preserved | ✅ preserved |
|
||||
| Test passes (verbose) | ✅ preserved | 🟡 collapsed | 🟡 collapsed |
|
||||
| Routine output (info logs) | 🟡 collapsed | 🟡 collapsed | ❌ dropped |
|
||||
| Progress bars | 🟡 collapsed | ❌ dropped | ❌ dropped |
|
||||
| Banner / ASCII art | 🟡 collapsed | ❌ dropped | ❌ dropped |
|
||||
|
||||
### Choosing the Right Intensity
|
||||
|
||||
@@ -381,7 +381,9 @@ Both the **head** and **tail** of each section are preserved; middle content is
|
||||
```json
|
||||
{
|
||||
"combo": "my-coding-combo",
|
||||
"routing": { /* ... */ },
|
||||
"routing": {
|
||||
/* ... */
|
||||
},
|
||||
"compression": {
|
||||
"engine": "rtk",
|
||||
"intensity": "aggressive"
|
||||
@@ -402,7 +404,9 @@ updateEngineConfig("rtk", { intensity: "aggressive" });
|
||||
```
|
||||
|
||||
### Verifying the Effect
|
||||
|
||||
Use the **Verify Gate** (see below) to confirm your filter is safe at your chosen intensity:
|
||||
|
||||
```ts
|
||||
import { runRtkFilterTests } from "omniroute/compression/engines/rtk/verify";
|
||||
|
||||
@@ -492,24 +496,15 @@ The `engines/rtk/filters/` directory contains **49+ built-in filter JSON files**
|
||||
"^\\s*[A-Z][a-zA-Z]+Error:",
|
||||
"^\\s*[A-Z][a-zA-Z]+Exception"
|
||||
],
|
||||
"dropPatterns": [
|
||||
"site-packages/",
|
||||
"^\\s+[a-z_]+\\([^)]*\\)$"
|
||||
],
|
||||
"dropPatterns": ["site-packages/", "^\\s+[a-z_]+\\([^)]*\\)$"],
|
||||
"headLines": 5,
|
||||
"tailLines": 3,
|
||||
"maxLines": 25,
|
||||
"filterStderr": true
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": [
|
||||
"Error:",
|
||||
"Exception:",
|
||||
"Traceback"
|
||||
],
|
||||
"summaryPatterns": [
|
||||
"^[A-Z][a-zA-Z]+(?:Error|Exception):"
|
||||
]
|
||||
"errorPatterns": ["Error:", "Exception:", "Traceback"],
|
||||
"summaryPatterns": ["^[A-Z][a-zA-Z]+(?:Error|Exception):"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
@@ -598,7 +593,7 @@ RTK compress (with rawOutput.enabled=true)
|
||||
"intensity": "aggressive",
|
||||
"rawOutput": {
|
||||
"enabled": true,
|
||||
"maxBytes": 1048576 // 1MB cap
|
||||
"maxBytes": 1048576 // 1MB cap
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -608,11 +603,11 @@ RTK compress (with rawOutput.enabled=true)
|
||||
|
||||
### Storage Cost
|
||||
|
||||
| Per-request | 1MB cap | 10MB cap |
|
||||
|-------------|---------|----------|
|
||||
| Average compressed output | ~5KB | ~5KB |
|
||||
| Raw output stored | ~50-500KB | ~500KB-5MB |
|
||||
| With 1000 requests/day | 50-500MB/day | 500MB-5GB/day |
|
||||
| Per-request | 1MB cap | 10MB cap |
|
||||
| ------------------------- | ------------ | ------------- |
|
||||
| Average compressed output | ~5KB | ~5KB |
|
||||
| Raw output stored | ~50-500KB | ~500KB-5MB |
|
||||
| With 1000 requests/day | 50-500MB/day | 500MB-5GB/day |
|
||||
|
||||
> **Recommendation**: Only enable raw output for **debugging sessions** or **sampled auditing**, not always-on.
|
||||
|
||||
@@ -621,7 +616,7 @@ RTK compress (with rawOutput.enabled=true)
|
||||
```ts
|
||||
import { readRtkRawOutput } from "omniroute/compression/engines/rtk/rawOutput";
|
||||
|
||||
const raw = readRtkRawOutput(pointerId); // pointerId from compression stats
|
||||
const raw = readRtkRawOutput(pointerId); // pointerId from compression stats
|
||||
if (raw) {
|
||||
console.log("Original output:", raw);
|
||||
}
|
||||
@@ -640,13 +635,17 @@ The **RTK Filter Verification** (`open-sse/services/compression/engines/rtk/veri
|
||||
import { runRtkFilterTests } from "open-sse/services/compression/engines/rtk/verify";
|
||||
|
||||
const result = runRtkFilterTests();
|
||||
console.log(`Passed: ${result.outcomes.filter(o => o.passed).length}`);
|
||||
console.log(`Failed: ${result.outcomes.filter(o => !o.passed).length}`);
|
||||
console.log(`Passed: ${result.outcomes.filter((o) => o.passed).length}`);
|
||||
console.log(`Failed: ${result.outcomes.filter((o) => !o.passed).length}`);
|
||||
if (!result.passed) {
|
||||
console.error("Filters failed verification");
|
||||
result.outcomes.filter(o => !o.passed).forEach(o => {
|
||||
console.error(` - ${o.filterId} / ${o.testName}: expected "${o.expected}", got "${o.actual}"`);
|
||||
});
|
||||
result.outcomes
|
||||
.filter((o) => !o.passed)
|
||||
.forEach((o) => {
|
||||
console.error(
|
||||
` - ${o.filterId} / ${o.testName}: expected "${o.expected}", got "${o.actual}"`
|
||||
);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Diagrams"
|
||||
version: 3.8.2
|
||||
lastUpdated: 2026-05-13
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Diagrams
|
||||
@@ -10,16 +10,16 @@ Mermaid sources (`.mmd`) and exported SVGs for OmniRoute v3.8.0 architecture flo
|
||||
|
||||
## Canonical diagrams
|
||||
|
||||
| Source | Exported | Used in |
|
||||
| -------------------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------ |
|
||||
| [request-pipeline.mmd](./request-pipeline.mmd) | [SVG](./exported/request-pipeline.svg) | docs/architecture/ARCHITECTURE.md, docs/architecture/CODEBASE_DOCUMENTATION.md |
|
||||
| [auto-combo-9factor.mmd](./auto-combo-9factor.mmd) | [SVG](./exported/auto-combo-9factor.svg) | docs/routing/AUTO-COMBO.md |
|
||||
| [resilience-3layers.mmd](./resilience-3layers.mmd) | [SVG](./exported/resilience-3layers.svg) | docs/architecture/RESILIENCE_GUIDE.md, CLAUDE.md |
|
||||
| [i18n-flow.mmd](./i18n-flow.mmd) | [SVG](./exported/i18n-flow.svg) | docs/guides/I18N.md |
|
||||
| [mcp-tools-87.mmd](./mcp-tools-87.mmd) | [SVG](./exported/mcp-tools-87.svg) | docs/frameworks/MCP-SERVER.md |
|
||||
| [cloud-agent-flow.mmd](./cloud-agent-flow.mmd) | [SVG](./exported/cloud-agent-flow.svg) | docs/frameworks/CLOUD_AGENT.md |
|
||||
| [authz-pipeline.mmd](./authz-pipeline.mmd) | [SVG](./exported/authz-pipeline.svg) | docs/architecture/AUTHZ_GUIDE.md |
|
||||
| [db-schema-overview.mmd](./db-schema-overview.mmd) | [SVG](./exported/db-schema-overview.svg) | docs/architecture/CODEBASE_DOCUMENTATION.md |
|
||||
| Source | Exported | Used in |
|
||||
| ---------------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------ |
|
||||
| [request-pipeline.mmd](./request-pipeline.mmd) | [SVG](./exported/request-pipeline.svg) | docs/architecture/ARCHITECTURE.md, docs/architecture/CODEBASE_DOCUMENTATION.md |
|
||||
| [auto-combo-12factor.mmd](./auto-combo-12factor.mmd) | [SVG](./exported/auto-combo-12factor.svg) | docs/routing/AUTO-COMBO.md |
|
||||
| [resilience-3layers.mmd](./resilience-3layers.mmd) | [SVG](./exported/resilience-3layers.svg) | docs/architecture/RESILIENCE_GUIDE.md, CLAUDE.md |
|
||||
| [i18n-flow.mmd](./i18n-flow.mmd) | [SVG](./exported/i18n-flow.svg) | docs/guides/I18N.md |
|
||||
| [mcp-tools-94.mmd](./mcp-tools-94.mmd) | [SVG](./exported/mcp-tools-94.svg) | docs/frameworks/MCP-SERVER.md |
|
||||
| [cloud-agent-flow.mmd](./cloud-agent-flow.mmd) | [SVG](./exported/cloud-agent-flow.svg) | docs/frameworks/CLOUD_AGENT.md |
|
||||
| [authz-pipeline.mmd](./authz-pipeline.mmd) | [SVG](./exported/authz-pipeline.svg) | docs/architecture/AUTHZ_GUIDE.md |
|
||||
| [db-schema-overview.mmd](./db-schema-overview.mmd) | [SVG](./exported/db-schema-overview.svg) | docs/architecture/CODEBASE_DOCUMENTATION.md |
|
||||
|
||||
## How to update
|
||||
|
||||
@@ -49,7 +49,7 @@ From a doc in `docs/<subfolder>/`, the relative path becomes `../diagrams/...`:
|
||||
From the repo root (e.g. `CLAUDE.md`):
|
||||
|
||||
```markdown
|
||||

|
||||

|
||||
```
|
||||
|
||||
## Conventions
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
%% Auto-Combo 9-factor scoring
|
||||
%% Auto-Combo 12-factor scoring
|
||||
%% Reflects: open-sse/services/autoCombo/scoring.ts (DEFAULT_WEIGHTS, sum = 1.0)
|
||||
%% v3.8.0
|
||||
%% v3.8.40
|
||||
flowchart TB
|
||||
Request["Incoming request"] --> Candidates["Eligible candidates<br/>(provider × model × account)"]
|
||||
Candidates --> Score["Compute composite score<br/>per candidate"]
|
||||
|
||||
subgraph Factors["9-factor scoring weights (sum = 1.0)"]
|
||||
f1["health (0.22)"]
|
||||
f2["quota (0.17)"]
|
||||
f3["costInv (0.17)"]
|
||||
f4["latencyInv (0.13)"]
|
||||
subgraph Factors["12-factor scoring weights (sum = 1.0)"]
|
||||
f1["health (0.20)"]
|
||||
f2["quota (0.15)"]
|
||||
f3["costInv (0.15)"]
|
||||
f4["latencyInv (0.12)"]
|
||||
f5["taskFit (0.08)"]
|
||||
f6["specificityMatch (0.08)"]
|
||||
f7["stability (0.05)"]
|
||||
f8["tierPriority (0.05)"]
|
||||
f9["tierAffinity (0.05)"]
|
||||
f6["stability (0.05)"]
|
||||
f7["tierPriority (0.05)"]
|
||||
f8["tierAffinity (0.05)"]
|
||||
f9["specificityMatch (0.05)"]
|
||||
f10["contextAffinity (0.05)"]
|
||||
f11["connectionDensity (0.05)"]
|
||||
f12["resetWindowAffinity (0.00)"]
|
||||
end
|
||||
|
||||
Score --> Factors
|
||||
1
docs/diagrams/exported/auto-combo-12factor.svg
Normal file
1
docs/diagrams/exported/auto-combo-12factor.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 24 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 23 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 20 KiB |
1
docs/diagrams/exported/mcp-tools-94.svg
Normal file
1
docs/diagrams/exported/mcp-tools-94.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 22 KiB |
@@ -1,14 +1,15 @@
|
||||
%% MCP Server tool inventory by category
|
||||
%% Reflects: open-sse/mcp-server/ — TOTAL_MCP_TOOL_COUNT in server.ts
|
||||
%% (MCP_TOOLS 33 + memory 3 + skill 4 + agentSkill 3 + gamification 8 + plugin 8 + notion 6 + obsidian 22 = 87)
|
||||
%% (MCP_TOOLS 34 + memory 3 + skill 4 + agentSkill 3 + pool 6 + gamification 8 + plugin 8 + notion 6 + obsidian 22 = 94)
|
||||
%% Regenerate the SVG with: npm run docs:render-diagrams
|
||||
%% v3.8.26
|
||||
%% v3.8.40
|
||||
flowchart LR
|
||||
MCP["MCP Server<br/>87 tools total"]
|
||||
MCP --> Core["Core (33)<br/>routing, cache, compression,<br/>quota, proxy, tunnels, search"]
|
||||
MCP["MCP Server<br/>94 tools total"]
|
||||
MCP --> Core["Core (34)<br/>routing, cache, compression,<br/>quota, proxy, tunnels, search, web_fetch"]
|
||||
MCP --> Mem["Memory (3)<br/>memory_search, memory_save,<br/>memory_delete"]
|
||||
MCP --> Skl["Skills (4)<br/>skill_invoke, skill_list,<br/>skill_diagnose, skill_uninstall"]
|
||||
MCP --> ASkl["Agent-Skills (3)<br/>cli-registry discovery"]
|
||||
MCP --> Pool["Pool (6)<br/>pool_status, pool_sessions, pool_reset,<br/>pool_warm, pool_health, browser_pool_status"]
|
||||
MCP --> Gam["Gamification (8)"]
|
||||
MCP --> Plg["Plugins (8)"]
|
||||
MCP --> Notion["Notion (6)<br/>search, get_page, query_database,<br/>get_database, list_block_children,<br/>append_blocks"]
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "OmniRoute A2A Server Documentation"
|
||||
version: 3.8.2
|
||||
lastUpdated: 2026-05-13
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# OmniRoute A2A Server Documentation
|
||||
@@ -141,14 +141,14 @@ curl -X POST http://localhost:20128/a2a \
|
||||
|
||||
OmniRoute exposes 6 A2A skills wired in `src/lib/a2a/taskExecution.ts::A2A_SKILL_HANDLERS`. Each skill module lives in `src/lib/a2a/skills/`.
|
||||
|
||||
| Skill | ID | Description | Tags | Examples |
|
||||
| :------------------ | :-------------------- | :------------------------------------------------------------------------------------------------------------ | :---------------------- | :-------------------------------------- |
|
||||
| Smart Routing | `smart-routing` | Routes a prompt through the optimal provider/combo using OmniRoute's combo engine + scoring | routing, providers | "Route this prompt via the best model" |
|
||||
| Quota Management | `quota-management` | Reports per-provider quota state, helps callers decide when to throttle/switch | quota, providers | "Check quota for anthropic" |
|
||||
| Provider Discovery | `provider-discovery` | Lists installed providers with capabilities, free-tier flags, OAuth status | providers, discovery | "What providers are available?" |
|
||||
| Cost Analysis | `cost-analysis` | Estimates cost of a request/conversation given the catalog + recent usage | cost, usage | "Estimate cost for this conversation" |
|
||||
| Health Report | `health-report` | Aggregates circuit breaker, cooldown, lockout state per provider | health, resilience | "Show health status of all providers" |
|
||||
| List Capabilities | `list-capabilities` | Returns the full 42-entry Agent Skills catalog as a markdown table with raw SKILL.md URLs for context injection | catalog, discovery, skills | "List all OmniRoute capabilities" |
|
||||
| Skill | ID | Description | Tags | Examples |
|
||||
| :----------------- | :------------------- | :-------------------------------------------------------------------------------------------------------------- | :------------------------- | :------------------------------------- |
|
||||
| Smart Routing | `smart-routing` | Routes a prompt through the optimal provider/combo using OmniRoute's combo engine + scoring | routing, providers | "Route this prompt via the best model" |
|
||||
| Quota Management | `quota-management` | Reports per-provider quota state, helps callers decide when to throttle/switch | quota, providers | "Check quota for anthropic" |
|
||||
| Provider Discovery | `provider-discovery` | Lists installed providers with capabilities, free-tier flags, OAuth status | providers, discovery | "What providers are available?" |
|
||||
| Cost Analysis | `cost-analysis` | Estimates cost of a request/conversation given the catalog + recent usage | cost, usage | "Estimate cost for this conversation" |
|
||||
| Health Report | `health-report` | Aggregates circuit breaker, cooldown, lockout state per provider | health, resilience | "Show health status of all providers" |
|
||||
| List Capabilities | `list-capabilities` | Returns the full 42-entry Agent Skills catalog as a markdown table with raw SKILL.md URLs for context injection | catalog, discovery, skills | "List all OmniRoute capabilities" |
|
||||
|
||||
> Note: the Agent Card description currently advertises "36+ providers" (`src/app/.well-known/agent.json/route.ts:26` and `:55`). The actual catalog has grown to 180+ providers — the string should be updated in a follow-up change (tracked as a separate doc/code TODO; not modified here).
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: ACP (Agent Client Protocol)
|
||||
|
||||
# ACP (Agent Client Protocol)
|
||||
|
||||
> **TL;DR**: ACP lets OmniRoute spawn CLI agents (like Claude Code, Codex, Gemini CLI) as child processes instead of using HTTP APIs. This gives you "CLI-as-backend" transport.
|
||||
> **TL;DR**: ACP lets OmniRoute spawn CLI agents (like Claude Code, Codex) as child processes instead of using HTTP APIs. This gives you "CLI-as-backend" transport.
|
||||
|
||||
---
|
||||
|
||||
@@ -34,7 +34,6 @@ ACP supports **14 built-in CLI agents** out of the box:
|
||||
| `codex` | OpenAI Codex CLI | `codex` | stdio |
|
||||
| `claude` | Claude Code CLI | `claude` | stdio |
|
||||
| `goose` | Goose CLI | `goose` | stdio |
|
||||
| `gemini-cli` | Gemini CLI | `gemini` | stdio |
|
||||
| `openclaw` | OpenClaw | `openclaw` | stdio |
|
||||
| `aider` | Aider | `aider` | stdio |
|
||||
| `opencode` | OpenCode | `opencode` | stdio |
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
---
|
||||
title: "OmniRoute Agent Skills Catalog"
|
||||
version: 3.8.6
|
||||
lastUpdated: 2026-05-28
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# OmniRoute Agent Skills Catalog
|
||||
|
||||
> **Source of truth:** `src/lib/agentSkills/` (catalog, generator, parsers) + `skills/` directory (SKILL.md files)
|
||||
> **Last updated:** 2026-05-28 — v3.8.6
|
||||
> **Last updated:** 2026-06-28 — v3.8.40
|
||||
|
||||
Agent Skills are structured SKILL.md files that teach external agents, MCP clients, and A2A orchestrators how to use OmniRoute's REST API and CLI. Unlike [Omni Skills](./SKILLS.md) (which are LLM tool definitions executed inside OmniRoute), Agent Skills are a *documentation catalog* — static markdown that can be fed directly into agent context.
|
||||
Agent Skills are structured SKILL.md files that teach external agents, MCP clients, and A2A orchestrators how to use OmniRoute's REST API and CLI. Unlike [Omni Skills](./SKILLS.md) (which are LLM tool definitions executed inside OmniRoute), Agent Skills are a _documentation catalog_ — static markdown that can be fed directly into agent context.
|
||||
|
||||
---
|
||||
|
||||
@@ -58,20 +58,27 @@ src/lib/a2a/skills/listCapabilities.ts — A2A skill: list-capabilities
|
||||
name: omni-providers
|
||||
description: "Manage provider connections: add, test, rotate, and remove credentials."
|
||||
---
|
||||
|
||||
<!-- generated by src/lib/agentSkills/generator.ts; manual edits will be overwritten -->
|
||||
|
||||
## Overview
|
||||
|
||||
...
|
||||
|
||||
## Authentication
|
||||
|
||||
...
|
||||
|
||||
## Endpoints
|
||||
|
||||
...
|
||||
|
||||
<!-- skill:custom-start -->
|
||||
|
||||
## Custom Section (preserved across regeneration)
|
||||
|
||||
...
|
||||
|
||||
<!-- skill:custom-end -->
|
||||
```
|
||||
|
||||
@@ -83,13 +90,13 @@ The generator preserves content between `<!-- skill:custom-start -->` and `<!--
|
||||
|
||||
## REST API Discovery
|
||||
|
||||
| Endpoint | Method | Description | Auth |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `/api/agent-skills` | GET | List catalog (optional `?category=api\|cli&area=<area>`) | none |
|
||||
| `/api/agent-skills/{id}` | GET | Get single skill metadata | none |
|
||||
| `/api/agent-skills/{id}/raw` | GET | Fetch SKILL.md as `text/markdown` | none |
|
||||
| `/api/agent-skills/coverage` | GET | Coverage stats (how many SKILL.md files exist) | none |
|
||||
| `/api/agent-skills/generate` | POST | Trigger generator (dryRun/prune/onlyIds) | management |
|
||||
| Endpoint | Method | Description | Auth |
|
||||
| :--------------------------- | :----- | :------------------------------------------------------- | :--------- |
|
||||
| `/api/agent-skills` | GET | List catalog (optional `?category=api\|cli&area=<area>`) | none |
|
||||
| `/api/agent-skills/{id}` | GET | Get single skill metadata | none |
|
||||
| `/api/agent-skills/{id}/raw` | GET | Fetch SKILL.md as `text/markdown` | none |
|
||||
| `/api/agent-skills/coverage` | GET | Coverage stats (how many SKILL.md files exist) | none |
|
||||
| `/api/agent-skills/generate` | POST | Trigger generator (dryRun/prune/onlyIds) | management |
|
||||
|
||||
Example — list all API skills:
|
||||
|
||||
@@ -109,11 +116,11 @@ curl -H "Accept: text/markdown" "http://localhost:20128/api/agent-skills/omni-pr
|
||||
|
||||
Three MCP tools are registered under scope `read:catalog`:
|
||||
|
||||
| Tool | Description |
|
||||
| :--- | :--- |
|
||||
| `omniroute_agent_skills_list` | List skills (optional `category` / `area` filters) |
|
||||
| `omniroute_agent_skills_get` | Get metadata + SKILL.md for one skill by `id` |
|
||||
| `omniroute_agent_skills_coverage` | Coverage stats (API/CLI have/total) |
|
||||
| Tool | Description |
|
||||
| :-------------------------------- | :------------------------------------------------- |
|
||||
| `omniroute_agent_skills_list` | List skills (optional `category` / `area` filters) |
|
||||
| `omniroute_agent_skills_get` | Get metadata + SKILL.md for one skill by `id` |
|
||||
| `omniroute_agent_skills_coverage` | Coverage stats (API/CLI have/total) |
|
||||
|
||||
See [MCP-SERVER.md](./MCP-SERVER.md) for scope wiring and authentication.
|
||||
|
||||
@@ -125,11 +132,12 @@ The A2A skill `list-capabilities` returns the full 42-skill catalog as a markdow
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0", "id": "1",
|
||||
"jsonrpc": "2.0",
|
||||
"id": "1",
|
||||
"method": "message/send",
|
||||
"params": {
|
||||
"skill": "list-capabilities",
|
||||
"messages": [{"role": "user", "content": "List all capabilities"}]
|
||||
"messages": [{ "role": "user", "content": "List all capabilities" }]
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -142,55 +150,55 @@ See [A2A-SERVER.md](./A2A-SERVER.md) for protocol details.
|
||||
|
||||
### API Skills (22)
|
||||
|
||||
| ID | Area | Entry Point |
|
||||
| :--- | :--- | :--- |
|
||||
| `omni-auth` | auth | Auth + session management |
|
||||
| `omni-providers` | providers | Provider connection management |
|
||||
| `omni-models` | models | Model catalog and capabilities |
|
||||
| `omni-combos-routing` | combos-routing | Combo routing strategies |
|
||||
| `omni-api-keys` | api-keys | API key management |
|
||||
| `omni-usage-logs` | usage-logs | Usage and cost logs |
|
||||
| `omni-budget` | budget | Budget guards |
|
||||
| `omni-settings` | settings | Global settings |
|
||||
| `omni-proxies` | proxies | Proxy pool management |
|
||||
| `omni-cache` | cache | Semantic + prompt cache |
|
||||
| `omni-compression` | compression | Context compression engines |
|
||||
| `omni-context-rtk` | context-rtk | RTK compression |
|
||||
| `omni-resilience` | resilience | Circuit breakers + cooldowns |
|
||||
| `omni-cli-tools` | cli-tools | CLI tools REST proxy |
|
||||
| `omni-tunnels` | tunnels | Tunnel management |
|
||||
| `omni-sync-cloud` | sync-cloud | Cloud sync |
|
||||
| `omni-db-backups` | db-backups | Database backups |
|
||||
| `omni-webhooks` | webhooks | Webhook event dispatcher |
|
||||
| `omni-mcp` | mcp | MCP server (87 tools, 3 transports) |
|
||||
| `omni-agents-a2a` | agents-a2a | A2A agent protocol |
|
||||
| `omni-version-manager` | version-manager | Version and update management |
|
||||
| `omni-inference` | inference | Direct inference / completions |
|
||||
| ID | Area | Entry Point |
|
||||
| :--------------------- | :-------------- | :---------------------------------- |
|
||||
| `omni-auth` | auth | Auth + session management |
|
||||
| `omni-providers` | providers | Provider connection management |
|
||||
| `omni-models` | models | Model catalog and capabilities |
|
||||
| `omni-combos-routing` | combos-routing | Combo routing strategies |
|
||||
| `omni-api-keys` | api-keys | API key management |
|
||||
| `omni-usage-logs` | usage-logs | Usage and cost logs |
|
||||
| `omni-budget` | budget | Budget guards |
|
||||
| `omni-settings` | settings | Global settings |
|
||||
| `omni-proxies` | proxies | Proxy pool management |
|
||||
| `omni-cache` | cache | Semantic + prompt cache |
|
||||
| `omni-compression` | compression | Context compression engines |
|
||||
| `omni-context-rtk` | context-rtk | RTK compression |
|
||||
| `omni-resilience` | resilience | Circuit breakers + cooldowns |
|
||||
| `omni-cli-tools` | cli-tools | CLI tools REST proxy |
|
||||
| `omni-tunnels` | tunnels | Tunnel management |
|
||||
| `omni-sync-cloud` | sync-cloud | Cloud sync |
|
||||
| `omni-db-backups` | db-backups | Database backups |
|
||||
| `omni-webhooks` | webhooks | Webhook event dispatcher |
|
||||
| `omni-mcp` | mcp | MCP server (87 tools, 3 transports) |
|
||||
| `omni-agents-a2a` | agents-a2a | A2A agent protocol |
|
||||
| `omni-version-manager` | version-manager | Version and update management |
|
||||
| `omni-inference` | inference | Direct inference / completions |
|
||||
|
||||
### CLI Skills (20)
|
||||
|
||||
| ID | Area | CLI Command Root |
|
||||
| :--- | :--- | :--- |
|
||||
| `cli-serve` | cli-serve | `omniroute serve` |
|
||||
| `cli-health` | cli-health | `omniroute health` |
|
||||
| `cli-providers` | cli-providers | `omniroute providers` |
|
||||
| `cli-keys` | cli-keys | `omniroute keys` |
|
||||
| `cli-models` | cli-models | `omniroute models` |
|
||||
| `cli-chat` | cli-chat | `omniroute chat` |
|
||||
| `cli-routing` | cli-routing | `omniroute routing` |
|
||||
| `cli-resilience` | cli-resilience | `omniroute resilience` |
|
||||
| `cli-compression` | cli-compression | `omniroute compression` |
|
||||
| `cli-contexts` | cli-contexts | `omniroute contexts` |
|
||||
| `cli-cost-usage` | cli-cost-usage | `omniroute cost` |
|
||||
| `cli-mcp` | cli-mcp | `omniroute mcp` |
|
||||
| `cli-a2a` | cli-a2a | `omniroute a2a` |
|
||||
| `cli-tunnel` | cli-tunnel | `omniroute tunnel` |
|
||||
| `cli-backup-sync` | cli-backup-sync | `omniroute backup` |
|
||||
| `cli-policy-audit` | cli-policy-audit | `omniroute policy` |
|
||||
| `cli-batches` | cli-batches | `omniroute batch` |
|
||||
| `cli-eval` | cli-eval | `omniroute eval` |
|
||||
| `cli-plugins-skills` | cli-plugins-skills | `omniroute plugins` |
|
||||
| `cli-setup` | cli-setup | `omniroute setup` |
|
||||
| ID | Area | CLI Command Root |
|
||||
| :------------------- | :----------------- | :---------------------- |
|
||||
| `cli-serve` | cli-serve | `omniroute serve` |
|
||||
| `cli-health` | cli-health | `omniroute health` |
|
||||
| `cli-providers` | cli-providers | `omniroute providers` |
|
||||
| `cli-keys` | cli-keys | `omniroute keys` |
|
||||
| `cli-models` | cli-models | `omniroute models` |
|
||||
| `cli-chat` | cli-chat | `omniroute chat` |
|
||||
| `cli-routing` | cli-routing | `omniroute routing` |
|
||||
| `cli-resilience` | cli-resilience | `omniroute resilience` |
|
||||
| `cli-compression` | cli-compression | `omniroute compression` |
|
||||
| `cli-contexts` | cli-contexts | `omniroute contexts` |
|
||||
| `cli-cost-usage` | cli-cost-usage | `omniroute cost` |
|
||||
| `cli-mcp` | cli-mcp | `omniroute mcp` |
|
||||
| `cli-a2a` | cli-a2a | `omniroute a2a` |
|
||||
| `cli-tunnel` | cli-tunnel | `omniroute tunnel` |
|
||||
| `cli-backup-sync` | cli-backup-sync | `omniroute backup` |
|
||||
| `cli-policy-audit` | cli-policy-audit | `omniroute policy` |
|
||||
| `cli-batches` | cli-batches | `omniroute batch` |
|
||||
| `cli-eval` | cli-eval | `omniroute eval` |
|
||||
| `cli-plugins-skills` | cli-plugins-skills | `omniroute plugins` |
|
||||
| `cli-setup` | cli-setup | `omniroute setup` |
|
||||
|
||||
---
|
||||
|
||||
@@ -283,8 +291,8 @@ curl "http://localhost:20128/api/agent-skills/coverage"
|
||||
|
||||
```json
|
||||
{
|
||||
"api": {"have": 22, "total": 22},
|
||||
"cli": {"have": 20, "total": 20},
|
||||
"api": { "have": 22, "total": 22 },
|
||||
"cli": { "have": 20, "total": 20 },
|
||||
"totalSkills": 42,
|
||||
"generatedAt": "2026-05-28T00:00:00.000Z"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "AgentBridge"
|
||||
version: 3.8.31
|
||||
lastUpdated: 2026-06-20
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# AgentBridge
|
||||
@@ -29,23 +29,23 @@ This means you can:
|
||||
|
||||
### Positioning vs. the market
|
||||
|
||||
| Feature | 9router | anti-api | llm-interceptor | **OmniRoute AgentBridge** |
|
||||
|---------|:-------:|:--------:|:---------------:|:-------------------------:|
|
||||
| Antigravity | ✓ | ✓ | — | ✓ |
|
||||
| GitHub Copilot | ✓ | ✓ | — | ✓ |
|
||||
| Kiro (AWS) | ✓ | ✓ | — | ✓ |
|
||||
| OpenAI Codex | — | ✓ | — | ✓ |
|
||||
| Cursor IDE | ✓ | ✓ | — | ✓ |
|
||||
| Zed Industries | — | ✓ | — | ✓ |
|
||||
| Claude Code | — | — | ✓ | ✓ |
|
||||
| Open Code | — | — | ✓ | ✓ |
|
||||
| Trae | — | — | — | 🔍 Investigating |
|
||||
| Dashboard UI | ✓ | ✗ | ✗ | ✓ |
|
||||
| Traffic Inspector | ✗ | ✗ | ✓ | ✓ |
|
||||
| OmniRoute routing | ✗ | ✗ | ✗ | ✓ |
|
||||
| Model mapping UI | ✗ | ✗ | ✗ | ✓ |
|
||||
| Bypass list | ✗ | ✗ | ✓ | ✓ |
|
||||
| Upstream CA cert | ✗ | ✗ | ✓ | ✓ |
|
||||
| Feature | 9router | anti-api | llm-interceptor | **OmniRoute AgentBridge** |
|
||||
| ----------------- | :-----: | :------: | :-------------: | :-----------------------: |
|
||||
| Antigravity | ✓ | ✓ | — | ✓ |
|
||||
| GitHub Copilot | ✓ | ✓ | — | ✓ |
|
||||
| Kiro (AWS) | ✓ | ✓ | — | ✓ |
|
||||
| OpenAI Codex | — | ✓ | — | ✓ |
|
||||
| Cursor IDE | ✓ | ✓ | — | ✓ |
|
||||
| Zed Industries | — | ✓ | — | ✓ |
|
||||
| Claude Code | — | — | ✓ | ✓ |
|
||||
| Open Code | — | — | ✓ | ✓ |
|
||||
| Trae | — | — | — | 🔍 Investigating |
|
||||
| Dashboard UI | ✓ | ✗ | ✗ | ✓ |
|
||||
| Traffic Inspector | ✗ | ✗ | ✓ | ✓ |
|
||||
| OmniRoute routing | ✗ | ✗ | ✗ | ✓ |
|
||||
| Model mapping UI | ✗ | ✗ | ✗ | ✓ |
|
||||
| Bypass list | ✗ | ✗ | ✓ | ✓ |
|
||||
| Upstream CA cert | ✗ | ✗ | ✓ | ✓ |
|
||||
|
||||
---
|
||||
|
||||
@@ -96,7 +96,7 @@ export abstract class MitmHandlerBase {
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
body: Buffer,
|
||||
mappedModel: string,
|
||||
mappedModel: string
|
||||
): Promise<void>;
|
||||
|
||||
// Protected helpers: fetchRouter, pipeSSE, hookBufferStart, hookBufferUpdate
|
||||
@@ -117,9 +117,7 @@ export const COPILOT_TARGET: MitmTarget = {
|
||||
hosts: ["api.githubcopilot.com", "copilot-proxy.githubusercontent.com"],
|
||||
port: 443,
|
||||
endpointPatterns: ["/chat/completions", "/v1/chat/completions"],
|
||||
defaultModels: [
|
||||
{ id: "gpt-4o", name: "GPT-4o", alias: "gpt-4o" },
|
||||
],
|
||||
defaultModels: [{ id: "gpt-4o", name: "GPT-4o", alias: "gpt-4o" }],
|
||||
handler: () => import("../handlers/copilot"),
|
||||
riskNoticeKey: "providers.riskNotice.oauth",
|
||||
};
|
||||
@@ -130,15 +128,18 @@ The registry (`targets/index.ts`) exports `ALL_TARGETS` and emits `DATA_DIR/mitm
|
||||
### 2.5 Passthrough and bypass list (`src/mitm/passthrough.ts`)
|
||||
|
||||
**Bypass list** (checked first, with precedence over target match):
|
||||
|
||||
- Default patterns: banking hosts, `.gov.`, OAuth/SSO providers (Okta, Auth0), etc.
|
||||
- User patterns: stored in DB table `agent_bridge_bypass`
|
||||
- Bypassed hosts receive a transparent TCP tunnel — TLS is **never decrypted**
|
||||
|
||||
**Passthrough default** (no target match and not in bypass):
|
||||
|
||||
- Also receives a TCP tunnel — connections are never broken
|
||||
- Prevents the AgentBridge from disrupting general system HTTPS traffic
|
||||
|
||||
Routing precedence:
|
||||
|
||||
```
|
||||
bypass list → target match → passthrough
|
||||
```
|
||||
@@ -169,13 +170,13 @@ Applied to all request bodies and headers **before** they enter the Traffic Insp
|
||||
|
||||
Use the AgentBridge Server Card at `/dashboard/tools/agent-bridge`:
|
||||
|
||||
| Action | Description |
|
||||
|--------|-------------|
|
||||
| Start Server | Spawns `src/mitm/server.cjs` on port 443 |
|
||||
| Stop Server | Gracefully shuts down the child process |
|
||||
| Restart Server | Stop + start (picks up target changes) |
|
||||
| Trust Cert | Installs `DATA_DIR/mitm/ca.crt` into OS trust store |
|
||||
| Download Cert | Downloads `ca.crt` for manual installation |
|
||||
| Action | Description |
|
||||
| --------------- | ----------------------------------------------------------------------- |
|
||||
| Start Server | Spawns `src/mitm/server.cjs` on port 443 |
|
||||
| Stop Server | Gracefully shuts down the child process |
|
||||
| Restart Server | Stop + start (picks up target changes) |
|
||||
| Trust Cert | Installs `DATA_DIR/mitm/ca.crt` into OS trust store |
|
||||
| Download Cert | Downloads `ca.crt` for manual installation |
|
||||
| Regenerate Cert | Creates a new CA keypair (all existing per-agent certs are invalidated) |
|
||||
|
||||
### 3.2 Trust the certificate
|
||||
@@ -183,17 +184,20 @@ Use the AgentBridge Server Card at `/dashboard/tools/agent-bridge`:
|
||||
The AgentBridge CA certificate must be trusted by the OS before IDEs will accept the MITM connection.
|
||||
|
||||
**Linux (NSS — Chrome/Firefox):**
|
||||
|
||||
```bash
|
||||
certutil -A -d sql:$HOME/.pki/nssdb -n "OmniRoute AgentBridge" -t CT,, -i ~/.omniroute/mitm/ca.crt
|
||||
```
|
||||
|
||||
**macOS (Keychain):**
|
||||
|
||||
```bash
|
||||
sudo security add-trusted-cert -d -r trustRoot \
|
||||
-k /Library/Keychains/System.keychain ~/.omniroute/mitm/ca.crt
|
||||
```
|
||||
|
||||
**Windows (certmgr):**
|
||||
|
||||
```powershell
|
||||
certutil -addstore -f Root $env:USERPROFILE\.omniroute\mitm\ca.crt
|
||||
```
|
||||
@@ -206,7 +210,7 @@ Some IDEs — notably **Antigravity IDE**, and other Electron / VS Code-derived
|
||||
their own Node.js runtime that **does not consult the OS trust store** for outbound
|
||||
`fetch`/HTTPS. Trusting the CA at the OS/NSS level is enough for the IDE's native **backend**
|
||||
(e.g. a Go language server, which uses the OS CA bundle), but the **Electron frontend** will
|
||||
still fail TLS — it surfaces as the app being *logged out* or showing a *"connection error"*
|
||||
still fail TLS — it surfaces as the app being _logged out_ or showing a _"connection error"_
|
||||
even though the MITM log shows the backend's bootstrap calls returning `200`. Two steps are
|
||||
required, and both matter:
|
||||
|
||||
@@ -228,6 +232,7 @@ locally-trusted CA overrides). `NODE_EXTRA_CA_CERTS` covers the Node `fetch` pat
|
||||
For each agent you want to intercept, its API host(s) must resolve to `127.0.0.1`. AgentBridge manages `/etc/hosts` entries automatically when you toggle DNS for an agent in the Setup Wizard.
|
||||
|
||||
Example `/etc/hosts` entries for GitHub Copilot:
|
||||
|
||||
```
|
||||
127.0.0.1 api.githubcopilot.com
|
||||
127.0.0.1 copilot-proxy.githubusercontent.com
|
||||
@@ -238,9 +243,9 @@ Example `/etc/hosts` entries for GitHub Copilot:
|
||||
Use the Model Mapping Table in each agent card to define source → target mappings:
|
||||
|
||||
| Source model (agent native) | Target model (OmniRoute) |
|
||||
|-----------------------------|--------------------------|
|
||||
| `gpt-4o` | `claude-sonnet-4.7` |
|
||||
| `*` (wildcard) | `claude-haiku-4.7` |
|
||||
| --------------------------- | ------------------------ |
|
||||
| `gpt-4o` | `claude-sonnet-4.7` |
|
||||
| `*` (wildcard) | `claude-haiku-4.7` |
|
||||
|
||||
Wildcard `*` maps any unrecognized model to the specified target. Persisted in `agent_bridge_mappings` table.
|
||||
|
||||
@@ -257,27 +262,27 @@ AgentBridge intercepts credentials (OAuth tokens, API keys) that the IDE uses to
|
||||
|
||||
### 3.6 Maintenance & Diagnostics
|
||||
|
||||
The dashboard exposes a **Maintenance & Diagnostics** card (`AgentBridgeMaintenanceCard`, in `src/app/(dashboard)/dashboard/tools/agent-bridge/components/`) that surfaces operational MITM routes which previously had no UI. Its subtitle: *"Self-test the capture pipeline, undo leftover system state, and move your setup between machines."* The card client helpers live in `src/lib/inspector/agentBridgeMaintenanceApi.ts`.
|
||||
The dashboard exposes a **Maintenance & Diagnostics** card (`AgentBridgeMaintenanceCard`, in `src/app/(dashboard)/dashboard/tools/agent-bridge/components/`) that surfaces operational MITM routes which previously had no UI. Its subtitle: _"Self-test the capture pipeline, undo leftover system state, and move your setup between machines."_ The card client helpers live in `src/lib/inspector/agentBridgeMaintenanceApi.ts`.
|
||||
|
||||
| Button | Route | What it does |
|
||||
|--------|-------|--------------|
|
||||
| **Diagnose** | `GET /api/tools/agent-bridge/diagnose` | Runs the capture-pipeline self-test and shows a per-check report (✓/✗ + remediation hint). |
|
||||
| **Repair** | `POST /api/tools/agent-bridge/repair` | Undoes orphaned MITM system state (DNS spoof entries, root CA, system proxy) left behind by a crash or SIGKILL. Idempotent — reports "Nothing to repair" when state is clean. |
|
||||
| **Remove CA** | `DELETE /api/tools/agent-bridge/cert` | Untrusts and removes the MITM root CA from the OS trust store (explicit, idempotent). Shown only when the CA is currently trusted; requires an inline "Remove CA?" confirmation. |
|
||||
| **Export config** | `GET /api/tools/agent-bridge/config` | Downloads the portable config JSON (see §3.7). |
|
||||
| **Import config** | `POST /api/tools/agent-bridge/config` | Uploads a previously-exported config JSON (see §3.7). |
|
||||
| Button | Route | What it does |
|
||||
| ----------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Diagnose** | `GET /api/tools/agent-bridge/diagnose` | Runs the capture-pipeline self-test and shows a per-check report (✓/✗ + remediation hint). |
|
||||
| **Repair** | `POST /api/tools/agent-bridge/repair` | Undoes orphaned MITM system state (DNS spoof entries, root CA, system proxy) left behind by a crash or SIGKILL. Idempotent — reports "Nothing to repair" when state is clean. |
|
||||
| **Remove CA** | `DELETE /api/tools/agent-bridge/cert` | Untrusts and removes the MITM root CA from the OS trust store (explicit, idempotent). Shown only when the CA is currently trusted; requires an inline "Remove CA?" confirmation. |
|
||||
| **Export config** | `GET /api/tools/agent-bridge/config` | Downloads the portable config JSON (see §3.7). |
|
||||
| **Import config** | `POST /api/tools/agent-bridge/config` | Uploads a previously-exported config JSON (see §3.7). |
|
||||
|
||||
**Diagnostics checks** (`summarizeDiagnostics()` in `src/mitm/inspector/diagnostics.ts`). The route runs the effectful probe for each and feeds the booleans into the pure summarizer; a single `healthy` verdict plus a per-failure hint is returned:
|
||||
|
||||
| Check name | What it verifies | Hint on failure |
|
||||
|------------|------------------|-----------------|
|
||||
| `server-running` | The MITM server process is active | "The MITM server is not running. Start it from the AgentBridge tab." |
|
||||
| `server-reachable` | The MITM server accepts connections on its port (TCP probe) | "The MITM server is not accepting connections on its port. Check that the port is free and that you have privileges to bind it." |
|
||||
| `cert-exists` | The MITM certificate has been generated on disk | "No MITM certificate has been generated yet. Generate one from the AgentBridge tab." |
|
||||
| `cert-trusted` | The MITM root CA is in the OS trust store | "The MITM root CA is not trusted by the OS store, so TLS interception will fail. Trust the certificate from the AgentBridge tab." |
|
||||
| `dns-configured` | Target hostnames are spoofed in `/etc/hosts` | "Target hostnames are not spoofed in /etc/hosts, so traffic never reaches the proxy. Enable DNS for the agent(s) you want to capture." |
|
||||
| Check name | What it verifies | Hint on failure |
|
||||
| ------------------ | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `server-running` | The MITM server process is active | "The MITM server is not running. Start it from the AgentBridge tab." |
|
||||
| `server-reachable` | The MITM server accepts connections on its port (TCP probe) | "The MITM server is not accepting connections on its port. Check that the port is free and that you have privileges to bind it." |
|
||||
| `cert-exists` | The MITM certificate has been generated on disk | "No MITM certificate has been generated yet. Generate one from the AgentBridge tab." |
|
||||
| `cert-trusted` | The MITM root CA is in the OS trust store | "The MITM root CA is not trusted by the OS store, so TLS interception will fail. Trust the certificate from the AgentBridge tab." |
|
||||
| `dns-configured` | Target hostnames are spoofed in `/etc/hosts` | "Target hostnames are not spoofed in /etc/hosts, so traffic never reaches the proxy. Enable DNS for the agent(s) you want to capture." |
|
||||
|
||||
**Orphaned-state banner:** when the page detects state left behind by a crash (DNS spoof / CA / system proxy), the card shows an amber banner — *"A previous session left system state behind (DNS spoof, CA, or system proxy). Run Repair to clean it up."* — and highlights the **Repair** button. `Repair` is the application-layer analogue of ProxyBridge's `--cleanup` flag (it delegates to `repairMitm()` in `src/mitm/manager.ts`).
|
||||
**Orphaned-state banner:** when the page detects state left behind by a crash (DNS spoof / CA / system proxy), the card shows an amber banner — _"A previous session left system state behind (DNS spoof, CA, or system proxy). Run Repair to clean it up."_ — and highlights the **Repair** button. `Repair` is the application-layer analogue of ProxyBridge's `--cleanup` flag (it delegates to `repairMitm()` in `src/mitm/manager.ts`).
|
||||
|
||||
> The MITM root CA is kept installed across stop/start to avoid repeated sudo
|
||||
> prompts (the same behavior as mitmproxy/Charles), so removing it is an explicit
|
||||
@@ -289,11 +294,11 @@ AgentBridge can serialize the **operator-tunable** state into a versioned JSON b
|
||||
|
||||
The export includes exactly three pieces (built-in defaults are intentionally **NOT** exported, so importing never duplicates or fights them):
|
||||
|
||||
| Field | Source | Notes |
|
||||
|-------|--------|-------|
|
||||
| `bypassPatterns` | user-defined bypass patterns (`agent_bridge_bypass`) | default bank/gov/okta patterns are excluded |
|
||||
| `customHosts` | Traffic Inspector custom hosts (`inspector_custom_hosts`) | each: `{ host, kind: "llm"\|"app"\|"custom", label? }` |
|
||||
| `agentMappings` | per-agent model mappings (`agent_bridge_mappings`) | `{ [agentId]: [{ source, target }] }` for every agent that has mappings |
|
||||
| Field | Source | Notes |
|
||||
| ---------------- | --------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| `bypassPatterns` | user-defined bypass patterns (`agent_bridge_bypass`) | default bank/gov/okta patterns are excluded |
|
||||
| `customHosts` | Traffic Inspector custom hosts (`inspector_custom_hosts`) | each: `{ host, kind: "llm"\|"app"\|"custom", label? }` |
|
||||
| `agentMappings` | per-agent model mappings (`agent_bridge_mappings`) | `{ [agentId]: [{ source, target }] }` for every agent that has mappings |
|
||||
|
||||
```jsonc
|
||||
// GET /api/tools/agent-bridge/config
|
||||
@@ -301,7 +306,7 @@ The export includes exactly three pieces (built-in defaults are intentionally **
|
||||
"version": 1,
|
||||
"bypassPatterns": ["*.internal.example.com"],
|
||||
"customHosts": [{ "host": "api.example.com", "kind": "llm", "label": null }],
|
||||
"agentMappings": { "copilot": [{ "source": "gpt-4o", "target": "claude-sonnet-4.7" }] }
|
||||
"agentMappings": { "copilot": [{ "source": "gpt-4o", "target": "claude-sonnet-4.7" }] },
|
||||
}
|
||||
```
|
||||
|
||||
@@ -317,17 +322,17 @@ What is **NOT** in the config: server running state, cert paths, per-agent DNS s
|
||||
|
||||
## §4 Per-agent reference
|
||||
|
||||
| # | Agent | Status | Hosts intercepted | Auth type |
|
||||
|---|-------|--------|-------------------|-----------|
|
||||
| 1 | **Antigravity** | ✅ Supported | `daily-cloudcode-pa.googleapis.com`, `cloudcode-pa.googleapis.com` | Firebase OAuth |
|
||||
| 2 | **Kiro (AWS)** | ✅ Supported | `prod.kiro.aws`, `dev.kiro.aws` | AWS SigV4 |
|
||||
| 3 | **GitHub Copilot** | ✅ Supported | `api.githubcopilot.com`, `copilot-proxy.githubusercontent.com` | GitHub OAuth |
|
||||
| 4 | **OpenAI Codex** | ✅ Supported | `api.openai.com` (Codex paths), `chatgpt.com` | OpenAI key |
|
||||
| 5 | **Cursor IDE** | ✅ Supported | `api2.cursor.sh`, `api.cursor.sh` | Cursor OAuth |
|
||||
| 6 | **Zed Industries** | ✅ Supported | `api.zed.dev`, `llm.zed.dev` | Zed OAuth |
|
||||
| 7 | **Claude Code** | ✅ Supported | `api.anthropic.com` (opt-in) | Anthropic key |
|
||||
| 8 | **Open Code** | ✅ Supported | `openrouter.ai`, `api.openai.com` (zen paths) | API key |
|
||||
| 9 | **Trae** | 🔍 Investigating | TBD — see §8 | TBD |
|
||||
| # | Agent | Status | Hosts intercepted | Auth type |
|
||||
| --- | ------------------ | ---------------- | ------------------------------------------------------------------ | -------------- |
|
||||
| 1 | **Antigravity** | ✅ Supported | `daily-cloudcode-pa.googleapis.com`, `cloudcode-pa.googleapis.com` | Firebase OAuth |
|
||||
| 2 | **Kiro (AWS)** | ✅ Supported | `prod.kiro.aws`, `dev.kiro.aws` | AWS SigV4 |
|
||||
| 3 | **GitHub Copilot** | ✅ Supported | `api.githubcopilot.com`, `copilot-proxy.githubusercontent.com` | GitHub OAuth |
|
||||
| 4 | **OpenAI Codex** | ✅ Supported | `api.openai.com` (Codex paths), `chatgpt.com` | OpenAI key |
|
||||
| 5 | **Cursor IDE** | ✅ Supported | `api2.cursor.sh`, `api.cursor.sh` | Cursor OAuth |
|
||||
| 6 | **Zed Industries** | ✅ Supported | `api.zed.dev`, `llm.zed.dev` | Zed OAuth |
|
||||
| 7 | **Claude Code** | ✅ Supported | `api.anthropic.com` (opt-in) | Anthropic key |
|
||||
| 8 | **Open Code** | ✅ Supported | `openrouter.ai`, `api.openai.com` (zen paths) | API key |
|
||||
| 9 | **Trae** | 🔍 Investigating | TBD — see §8 | TBD |
|
||||
|
||||
### Setup wizard steps (per agent)
|
||||
|
||||
@@ -342,7 +347,7 @@ Each agent card has a 3-step setup wizard:
|
||||
For agents 1–8, AgentBridge attempts to auto-detect IDE installation:
|
||||
|
||||
```ts
|
||||
export async function detectAgent(agentId: AgentId): Promise<DetectionResult>
|
||||
export async function detectAgent(agentId: AgentId): Promise<DetectionResult>;
|
||||
// Returns: { installed: boolean, version?: string, path?: string }
|
||||
```
|
||||
|
||||
@@ -354,10 +359,10 @@ Detection uses OS-specific paths and binary checks (e.g., `code --list-extension
|
||||
|
||||
### Hard Rules applied
|
||||
|
||||
| Rule | Application |
|
||||
|------|-------------|
|
||||
| **#12** `sanitizeErrorMessage` | All handler errors are sanitized before response or buffer entry |
|
||||
| **#13** Shell env-passing | `/etc/hosts` edits use `env` option — no string interpolation of paths |
|
||||
| Rule | Application |
|
||||
| --------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| **#12** `sanitizeErrorMessage` | All handler errors are sanitized before response or buffer entry |
|
||||
| **#13** Shell env-passing | `/etc/hosts` edits use `env` option — no string interpolation of paths |
|
||||
| **#15 + #17** `isLocalOnlyPath()` | `/api/tools/agent-bridge/` is LOCAL_ONLY + SPAWN_CAPABLE — loopback enforced before auth |
|
||||
|
||||
### Bypass list for sensitive hosts
|
||||
@@ -365,6 +370,7 @@ Detection uses OS-specific paths and binary checks (e.g., `code --list-extension
|
||||
The bypass list ensures that financial institutions, OAuth/SSO providers, and other sensitive hosts are **never decrypted**. Their TLS traffic passes through as a transparent TCP tunnel — OmniRoute never sees the plaintext.
|
||||
|
||||
Default bypass patterns include:
|
||||
|
||||
- `*.bank.*`, `*.gov.*` (financial/government)
|
||||
- `*.okta.com`, `*.auth0.com`, `*.microsoft.com` (SSO/identity)
|
||||
- `*.apple.com`, `*.icloud.com` (Apple system services)
|
||||
@@ -374,6 +380,7 @@ User-added bypass patterns are stored in `agent_bridge_bypass` table and take pr
|
||||
### Secret masking
|
||||
|
||||
`maskSecrets()` from `src/mitm/maskSecrets.ts` is applied:
|
||||
|
||||
- On every request body before `TrafficBuffer.push()`
|
||||
- On every header before logging or broadcasting
|
||||
|
||||
@@ -434,11 +441,13 @@ variant fails under the same setup.
|
||||
### DNS not propagated
|
||||
|
||||
Check that `/etc/hosts` was updated:
|
||||
|
||||
```bash
|
||||
grep "omniroute\|127.0.0.1.*github\|127.0.0.1.*cursor" /etc/hosts
|
||||
```
|
||||
|
||||
Flush DNS cache:
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder
|
||||
@@ -451,12 +460,14 @@ ipconfig /flushdns
|
||||
### IDE not detected
|
||||
|
||||
Auto-detection uses common installation paths. If detection fails but the IDE is installed:
|
||||
|
||||
- Check if the IDE binary is in a non-standard location
|
||||
- The Setup Wizard still works — detection failure just means the badge won't show the install path
|
||||
|
||||
### Handler errors (upstream fetch fails)
|
||||
|
||||
If AgentBridge intercepts but all requests fail:
|
||||
|
||||
1. Verify at least one provider is connected at `/dashboard/providers`
|
||||
2. Check OmniRoute server logs: `APP_LOG_LEVEL=debug` in `.env`
|
||||
3. Verify `OMNIROUTE_BASE_URL` points to the correct router endpoint (default: `http://127.0.0.1:20128`)
|
||||
@@ -469,33 +480,33 @@ All routes are `LOCAL_ONLY` (loopback-only, enforced before auth) and `SPAWN_CAP
|
||||
|
||||
Base path: `/api/tools/agent-bridge/`
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/tools/agent-bridge/state` | Global server state + per-agent detection/status |
|
||||
| GET | `/api/tools/agent-bridge/agents` | List registered agents (id, name, hosts, viability, state) |
|
||||
| GET | `/api/tools/agent-bridge/agents/{id}` | State of one agent (target config + detection + stored state) |
|
||||
| PATCH | `/api/tools/agent-bridge/agents/{id}` | Update `setup_completed` for agent |
|
||||
| GET | `/api/tools/agent-bridge/agents/{id}/detect` | Run detection probe for agent (`installed`, `version?`, `path?`) |
|
||||
| POST | `/api/tools/agent-bridge/agents/{id}/dns` | Enable/disable DNS for agent (`{enabled: boolean}`) |
|
||||
| GET | `/api/tools/agent-bridge/agents/{id}/mappings` | Model mappings for agent |
|
||||
| PUT | `/api/tools/agent-bridge/agents/{id}/mappings` | Replace model mappings |
|
||||
| POST | `/api/tools/agent-bridge/server` | Start/stop/restart server (`action: "start"\|"stop"\|"restart"\|"trust-cert"\|"regenerate-cert"`) |
|
||||
| GET | `/api/tools/agent-bridge/cert` | Cert status (`exists`, `trusted`, `path`) |
|
||||
| POST | `/api/tools/agent-bridge/cert` | Trust (install) the MITM root CA |
|
||||
| DELETE | `/api/tools/agent-bridge/cert` | Untrust (remove) the MITM root CA — idempotent (see §3.6) |
|
||||
| POST | `/api/tools/agent-bridge/cert/regenerate` | Regenerate the self-signed MITM cert |
|
||||
| GET | `/api/tools/agent-bridge/cert/download` | Stream the PEM cert for download |
|
||||
| GET | `/api/tools/agent-bridge/bypass` | List bypass patterns (`default` + `user`) |
|
||||
| POST | `/api/tools/agent-bridge/bypass` | Replace user-defined bypass patterns wholesale |
|
||||
| DELETE | `/api/tools/agent-bridge/bypass?pattern=...` | Remove a single user-defined bypass pattern |
|
||||
| GET | `/api/tools/agent-bridge/diagnose` | Capture-pipeline self-test (see §3.6) |
|
||||
| POST | `/api/tools/agent-bridge/repair` | Undo orphaned MITM system state (see §3.6) |
|
||||
| GET | `/api/tools/agent-bridge/config` | Export portable config JSON (see §3.7) |
|
||||
| POST | `/api/tools/agent-bridge/config` | Import portable config JSON (see §3.7) |
|
||||
| GET | `/api/tools/agent-bridge/upstream-ca` | Get configured upstream CA path |
|
||||
| POST | `/api/tools/agent-bridge/upstream-ca` | Validate + persist upstream CA path |
|
||||
| POST | `/api/tools/agent-bridge/upstream-ca/test` | Validate-only (dry-run) an upstream CA path — does not persist |
|
||||
| GET / POST / DELETE | `/api/tools/agent-bridge/tproxy` | TPROXY transparent-decrypt capture mode — see [`docs/security/MITM-TPROXY-DECRYPT.md`](../security/MITM-TPROXY-DECRYPT.md) |
|
||||
| Method | Path | Description |
|
||||
| ------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
||||
| GET | `/api/tools/agent-bridge/state` | Global server state + per-agent detection/status |
|
||||
| GET | `/api/tools/agent-bridge/agents` | List registered agents (id, name, hosts, viability, state) |
|
||||
| GET | `/api/tools/agent-bridge/agents/{id}` | State of one agent (target config + detection + stored state) |
|
||||
| PATCH | `/api/tools/agent-bridge/agents/{id}` | Update `setup_completed` for agent |
|
||||
| GET | `/api/tools/agent-bridge/agents/{id}/detect` | Run detection probe for agent (`installed`, `version?`, `path?`) |
|
||||
| POST | `/api/tools/agent-bridge/agents/{id}/dns` | Enable/disable DNS for agent (`{enabled: boolean}`) |
|
||||
| GET | `/api/tools/agent-bridge/agents/{id}/mappings` | Model mappings for agent |
|
||||
| PUT | `/api/tools/agent-bridge/agents/{id}/mappings` | Replace model mappings |
|
||||
| POST | `/api/tools/agent-bridge/server` | Start/stop/restart server (`action: "start"\|"stop"\|"restart"\|"trust-cert"\|"regenerate-cert"`) |
|
||||
| GET | `/api/tools/agent-bridge/cert` | Cert status (`exists`, `trusted`, `path`) |
|
||||
| POST | `/api/tools/agent-bridge/cert` | Trust (install) the MITM root CA |
|
||||
| DELETE | `/api/tools/agent-bridge/cert` | Untrust (remove) the MITM root CA — idempotent (see §3.6) |
|
||||
| POST | `/api/tools/agent-bridge/cert/regenerate` | Regenerate the self-signed MITM cert |
|
||||
| GET | `/api/tools/agent-bridge/cert/download` | Stream the PEM cert for download |
|
||||
| GET | `/api/tools/agent-bridge/bypass` | List bypass patterns (`default` + `user`) |
|
||||
| POST | `/api/tools/agent-bridge/bypass` | Replace user-defined bypass patterns wholesale |
|
||||
| DELETE | `/api/tools/agent-bridge/bypass?pattern=...` | Remove a single user-defined bypass pattern |
|
||||
| GET | `/api/tools/agent-bridge/diagnose` | Capture-pipeline self-test (see §3.6) |
|
||||
| POST | `/api/tools/agent-bridge/repair` | Undo orphaned MITM system state (see §3.6) |
|
||||
| GET | `/api/tools/agent-bridge/config` | Export portable config JSON (see §3.7) |
|
||||
| POST | `/api/tools/agent-bridge/config` | Import portable config JSON (see §3.7) |
|
||||
| GET | `/api/tools/agent-bridge/upstream-ca` | Get configured upstream CA path |
|
||||
| POST | `/api/tools/agent-bridge/upstream-ca` | Validate + persist upstream CA path |
|
||||
| POST | `/api/tools/agent-bridge/upstream-ca/test` | Validate-only (dry-run) an upstream CA path — does not persist |
|
||||
| GET / POST / DELETE | `/api/tools/agent-bridge/tproxy` | TPROXY transparent-decrypt capture mode — see [`docs/security/MITM-TPROXY-DECRYPT.md`](../security/MITM-TPROXY-DECRYPT.md) |
|
||||
|
||||
Full OpenAPI schemas: `docs/openapi.yaml` → tag `AgentBridge`.
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
title: "Agent Protocols Guide"
|
||||
version: 3.8.2
|
||||
lastUpdated: 2026-05-13
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Agent Protocols Guide
|
||||
|
||||
> **Source:** `src/lib/{a2a,acp,cloudAgent}/`, `src/app/api/{a2a,acp,cloud}/`, `src/app/api/v1/agents/`
|
||||
> **Last updated:** 2026-05-13 — v3.8.0
|
||||
> **Last updated:** 2026-06-28 — v3.8.40
|
||||
|
||||
OmniRoute exposes three different agent-related surfaces. They look similar at first glance but solve different problems. Use this page to pick the right one.
|
||||
|
||||
@@ -17,7 +17,7 @@ OmniRoute exposes three different agent-related surfaces. They look similar at f
|
||||
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------- | -------------------- |
|
||||
| **A2A — Agent-to-Agent** | Cross-agent collaboration with peer agents that speak the A2A protocol | JSON-RPC 2.0 over HTTP | A2A v0.3 (open spec) |
|
||||
| **ACP — CLI Agents Registry** | Detecting / registering / launching CLI coding agents installed on the user's machine (Cursor, Cline, Codex CLI, Claude Code, Aider, etc.) | HTTP REST | OmniRoute-specific |
|
||||
| **Cloud Agents** | Submitting long-running coding tasks to external cloud services (Codex Cloud, Devin, Jules) | HTTP REST + DB-backed tasks | OmniRoute-specific |
|
||||
| **Cloud Agents** | Submitting long-running coding tasks to external cloud services (Codex Cloud, Devin, Jules, Cursor Cloud) | HTTP REST + DB-backed tasks | OmniRoute-specific |
|
||||
|
||||
The three are independent — pick any subset.
|
||||
|
||||
@@ -56,13 +56,14 @@ Do you need a cloud service to do work outside this machine (Codex Cloud / Devin
|
||||
- `tasks/get` — read task by ID
|
||||
- `tasks/cancel` — cancel a running task
|
||||
|
||||
### Built-in skills (5)
|
||||
### Built-in skills (6)
|
||||
|
||||
- `smart-routing` — route a prompt through the optimal combo
|
||||
- `quota-management` — report per-provider quota state
|
||||
- `provider-discovery` — list installed providers with capabilities
|
||||
- `cost-analysis` — estimate cost of a request/conversation
|
||||
- `health-report` — aggregate breaker/cooldown/lockout state per provider
|
||||
- `list-capabilities` — enumerate the agent's available skills and metadata
|
||||
|
||||
### Deep dive
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
title: "Cloud Agents"
|
||||
version: 3.8.31
|
||||
lastUpdated: 2026-06-20
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Cloud Agents
|
||||
|
||||
> **Source of truth:** `src/lib/cloudAgent/` and `src/app/api/v1/agents/tasks/`
|
||||
> **Last updated:** 2026-06-20 — v3.8.31 (frontmatter refresh; 4 agents incl. cursor-cloud)
|
||||
> **Last updated:** 2026-06-28 — v3.8.40 (frontmatter refresh; 4 agents incl. cursor-cloud)
|
||||
|
||||
OmniRoute orchestrates third-party cloud-hosted coding agents (Codex Cloud, Cursor,
|
||||
Devin, Jules) as long-running tasks. Each agent is wrapped behind a uniform interface so
|
||||
@@ -24,11 +24,11 @@ artifact, and supports follow-up messages and (in some providers) plan approval
|
||||
|
||||
## Supported Agents
|
||||
|
||||
| Provider ID | Class | Source | Upstream Base URL | Plan Approval |
|
||||
| ------------- | ----------------- | ------------------------------------ | --------------------------------------- | ------------- |
|
||||
| `jules` | `JulesAgent` | `src/lib/cloudAgent/agents/jules.ts` | `https://jules.googleapis.com/v1alpha` | Yes |
|
||||
| `devin` | `DevinAgent` | `src/lib/cloudAgent/agents/devin.ts` | `https://api.devin.ai/v1` | Yes |
|
||||
| `codex-cloud` | `CodexCloudAgent` | `src/lib/cloudAgent/agents/codex.ts` | `https://api.openai.com/v1/codex/cloud` | No (auto) |
|
||||
| Provider ID | Class | Source | Upstream Base URL | Plan Approval |
|
||||
| -------------- | ------------------ | ------------------------------------- | --------------------------------------- | ------------- |
|
||||
| `jules` | `JulesAgent` | `src/lib/cloudAgent/agents/jules.ts` | `https://jules.googleapis.com/v1alpha` | Yes |
|
||||
| `devin` | `DevinAgent` | `src/lib/cloudAgent/agents/devin.ts` | `https://api.devin.ai/v1` | Yes |
|
||||
| `codex-cloud` | `CodexCloudAgent` | `src/lib/cloudAgent/agents/codex.ts` | `https://api.openai.com/v1/codex/cloud` | No (auto) |
|
||||
| `cursor-cloud` | `CursorCloudAgent` | `src/lib/cloudAgent/agents/cursor.ts` | `https://api.cursor.com/v0` | No (auto) |
|
||||
|
||||
Registry: `src/lib/cloudAgent/registry.ts` — exports `getAgent(providerId)`,
|
||||
|
||||
@@ -6,7 +6,7 @@ description: "Reference for 9Router and CLIProxyAPI"
|
||||
# Embedded Services
|
||||
|
||||
> **Version:** v3.8.4
|
||||
> **Last updated:** 2026-05-25
|
||||
> **Last updated:** 2026-06-28
|
||||
> **Audience:** Engineers adding, maintaining, or debugging embedded services (9Router, CLIProxyAPI).
|
||||
|
||||
Embedded services are locally-installed process sidecar tools that OmniRoute installs, supervises, and
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
title: "Evaluations (Evals)"
|
||||
version: 3.8.2
|
||||
lastUpdated: 2026-05-13
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Evaluations (Evals)
|
||||
|
||||
> **Source of truth:** `src/lib/evals/`, `src/lib/db/evals.ts`, `src/app/api/evals/`
|
||||
> **Last updated:** 2026-05-13 — v3.8.0
|
||||
> **Last updated:** 2026-06-28 — v3.8.40
|
||||
|
||||
OmniRoute ships a generic evaluation framework you can use to benchmark routing
|
||||
configurations, single providers/models, or the bundled "golden set" suites.
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
title: "Gamification & Leaderboard System"
|
||||
version: 3.8.2
|
||||
lastUpdated: 2026-05-19
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Gamification & Leaderboard System
|
||||
|
||||
> **Source of truth:** `src/lib/gamification/`, `src/lib/db/gamification.ts`, `src/app/api/gamification/`
|
||||
> **Last updated:** 2026-05-19 — v3.8.0
|
||||
> **Last updated:** 2026-06-28 — v3.8.40
|
||||
|
||||
OmniRoute includes a local-first gamification layer that rewards users for
|
||||
engaging with the platform — making requests, switching providers, creating
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
---
|
||||
title: "OmniRoute MCP Server Documentation"
|
||||
version: 3.8.31
|
||||
lastUpdated: 2026-06-20
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# OmniRoute MCP Server Documentation
|
||||
|
||||
> Model Context Protocol server with 87 tools across routing, cache, compression, memory, skills, proxy, and context source operations.
|
||||
> Model Context Protocol server with 94 tools across routing, cache, compression, memory, skills, proxy, pool, and context source operations.
|
||||
>
|
||||
> Source of truth: `open-sse/mcp-server/schemas/tools.ts` (33 base) + `memoryTools.ts` (3) + `skillTools.ts` (4) + `agentSkillTools.ts` (3) + `gamificationTools.ts` (8) + `pluginTools.ts` (8) + `notionTools.ts` (6) + `obsidianTools.ts` (22) = **87** (`TOTAL_MCP_TOOL_COUNT`). Tool registration and scope wiring lives in `open-sse/mcp-server/server.ts`.
|
||||
> Source of truth: `open-sse/mcp-server/schemas/tools.ts` (34 base) + `memoryTools.ts` (3) + `skillTools.ts` (4) + `agentSkillTools.ts` (3) + `poolTools.ts` (6) + `gamificationTools.ts` (8) + `pluginTools.ts` (8) + `notionTools.ts` (6) + `obsidianTools.ts` (22) = **94** (`TOTAL_MCP_TOOL_COUNT`). Tool registration and scope wiring lives in `open-sse/mcp-server/server.ts`.
|
||||
|
||||

|
||||

|
||||
|
||||
> Source: [diagrams/mcp-tools-87.mmd](../diagrams/mcp-tools-87.mmd) (regenerate via `npm run docs:render-diagrams`).
|
||||
> Source: [diagrams/mcp-tools-94.mmd](../diagrams/mcp-tools-94.mmd) (regenerate via `npm run docs:render-diagrams`).
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -194,14 +194,14 @@ curl http://localhost:20128/api/settings/notion
|
||||
curl -X DELETE http://localhost:20128/api/settings/notion
|
||||
```
|
||||
|
||||
| Tool | Scopes | Description |
|
||||
| :-------------------------------- | :------------- | :------------------------------------------------------------- |
|
||||
| `omniroute_notion_search` | `read:notion` | Full-text search across all pages and databases |
|
||||
| `omniroute_notion_list_databases` | `read:notion` | List all accessible databases with schema metadata |
|
||||
| `omniroute_notion_get_database` | `read:notion` | Get database schema by ID |
|
||||
| `omniroute_notion_query_database` | `read:notion` | Query a database with filters, sorts, and pagination |
|
||||
| `omniroute_notion_read` | `read:notion` | Read a page or block by ID with its content |
|
||||
| `omniroute_notion_append_blocks` | `write:notion` | Append children blocks to a parent block (max 100 per request) |
|
||||
| Tool | Scopes | Description |
|
||||
| :--------------------------- | :------------- | :------------------------------------------------------------- |
|
||||
| `notion_search` | `read:notion` | Full-text search across all pages and databases |
|
||||
| `notion_get_page` | `read:notion` | Get a page by ID with its properties |
|
||||
| `notion_list_block_children` | `read:notion` | List the child blocks of a page or block |
|
||||
| `notion_query_database` | `read:notion` | Query a database with filters, sorts, and pagination |
|
||||
| `notion_get_database` | `read:notion` | Get database schema by ID |
|
||||
| `notion_append_blocks` | `write:notion` | Append children blocks to a parent block (max 100 per request) |
|
||||
|
||||
## Agent Skill Catalog Tools (3)
|
||||
|
||||
@@ -217,7 +217,7 @@ See [AGENT-SKILLS.md](./AGENT-SKILLS.md) for the full catalog and how external a
|
||||
|
||||
## Related Frameworks (v3.8.0)
|
||||
|
||||
The MCP tool inventory above (87 tools = 33 core + 3 memory + 4 skills + 3 agent-skills + 8 gamification + 8 plugins + 6 notion + 22 obsidian) is intentionally
|
||||
The MCP tool inventory above (94 tools = 34 core + 3 memory + 4 skills + 3 agent-skills + 6 pool + 8 gamification + 8 plugins + 6 notion + 22 obsidian) is intentionally
|
||||
scoped to runtime routing/cache/compression/memory/skills/proxy/context-source operations. Two adjacent
|
||||
frameworks ship alongside the MCP server in v3.8.0 and are documented separately:
|
||||
|
||||
@@ -329,9 +329,9 @@ MCP tool, prompt, and resource registries can compress descriptions at registrat
|
||||
|
||||
## Tool Cardinality Reduction (F4.3)
|
||||
|
||||
Description compression shrinks each tool's metadata; **tool-cardinality reduction** goes one step further by reducing *how many* tools are announced at all. Advertising fewer tools in the `tools/list` manifest cuts the per-request token cost the client's model pays for the tool catalog ("layer 5" compression). The implementation is a pure, stateless filter in `open-sse/mcp-server/toolCardinality.ts` (`reduceToolManifest`), wired into the registration loop in `createMcpServer()` (`open-sse/mcp-server/server.ts`).
|
||||
Description compression shrinks each tool's metadata; **tool-cardinality reduction** goes one step further by reducing _how many_ tools are announced at all. Advertising fewer tools in the `tools/list` manifest cuts the per-request token cost the client's model pays for the tool catalog ("layer 5" compression). The implementation is a pure, stateless filter in `open-sse/mcp-server/toolCardinality.ts` (`reduceToolManifest`), wired into the registration loop in `createMcpServer()` (`open-sse/mcp-server/server.ts`).
|
||||
|
||||
**Opt-in, off by default.** The filter only runs when at least one of two environment variables is set; with neither set, all 87 tools are announced unchanged.
|
||||
**Opt-in, off by default.** The filter only runs when at least one of two environment variables is set; with neither set, all 94 tools are announced unchanged.
|
||||
|
||||
| Variable | Mode |
|
||||
| :--------------- | :-------------------------------------------------------------------------------------- |
|
||||
@@ -398,7 +398,7 @@ Use the dashboard or the `/api/mcp/audit` and `/api/mcp/audit/stats` REST endpoi
|
||||
| `open-sse/mcp-server/audit.ts` | Tool call audit logging (`mcp_tool_audit`) |
|
||||
| `open-sse/mcp-server/runtimeHeartbeat.ts` | stdio heartbeat writer (`mcp-heartbeat.json`) |
|
||||
| `open-sse/mcp-server/descriptionCompressor.ts` | Description compression for tool / prompt / resource registries |
|
||||
| `open-sse/mcp-server/schemas/tools.ts` | Zod schemas + tool registry (`MCP_TOOLS`, 30 entries) |
|
||||
| `open-sse/mcp-server/schemas/tools.ts` | Zod schemas + tool registry (`MCP_TOOLS`, 34 entries) |
|
||||
| `open-sse/mcp-server/tools/advancedTools.ts` | Phase 2 + cache + 1proxy tool handlers |
|
||||
| `open-sse/mcp-server/tools/compressionTools.ts` | Compression tool handlers |
|
||||
| `open-sse/mcp-server/tools/memoryTools.ts` | Memory tool definitions (3 tools) |
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
title: "Memory System"
|
||||
version: 3.8.31
|
||||
lastUpdated: 2026-06-20
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Memory System
|
||||
|
||||
> **Source of truth:** `src/lib/memory/` and `src/app/api/memory/`
|
||||
> **Last updated:** 2026-06-20 — v3.8.31 (off-by-default + int8 quantization catch-up)
|
||||
> **Last updated:** 2026-06-28 — v3.8.40 (off-by-default + int8 quantization catch-up)
|
||||
|
||||
OmniRoute provides persistent conversational memory keyed by API key (and
|
||||
optionally session id). Memories are extracted automatically from LLM responses
|
||||
@@ -83,6 +83,7 @@ infrastructure and settings. Three tiers exist, applied in priority order:
|
||||
```
|
||||
|
||||
Degradation is automatic and transparent:
|
||||
|
||||
- If sqlite-vec fails to load, tier 1 is unavailable → falls back to tier 0.
|
||||
- If embedding source returns an error, tier 1 falls back to tier 0.
|
||||
- If Qdrant is unhealthy, tier 2 falls back to tier 1 (or tier 0 if tier 1
|
||||
@@ -93,14 +94,15 @@ Degradation is automatic and transparent:
|
||||
The embedding layer (`src/lib/memory/embedding/`) resolves which source to use
|
||||
based on `MemorySettingsExtended.embeddingSource`:
|
||||
|
||||
| Source | Description | Key required | Cold start |
|
||||
| -------------- | ------------------------------------------------------------------------------- | ------------ | ---------- |
|
||||
| `remote` | Uses a configured provider's embedding API (OpenAI, Cohere, etc.) | Yes | None |
|
||||
| `static` | Local lookup-table embedding via `potion-base-8M` (WordPiece + mean pooling) | No | ~200ms |
|
||||
| `transformers` | Local ONNX inference via `@huggingface/transformers` v4, `all-MiniLM-L6-v2` | No | ~3s + ~400MB RAM |
|
||||
| `auto` | Runtime resolution: remote (if key exists) → static → transformers → null | Depends | Depends |
|
||||
| Source | Description | Key required | Cold start |
|
||||
| -------------- | ---------------------------------------------------------------------------- | ------------ | ---------------- |
|
||||
| `remote` | Uses a configured provider's embedding API (OpenAI, Cohere, etc.) | Yes | None |
|
||||
| `static` | Local lookup-table embedding via `potion-base-8M` (WordPiece + mean pooling) | No | ~200ms |
|
||||
| `transformers` | Local ONNX inference via `@huggingface/transformers` v4, `all-MiniLM-L6-v2` | No | ~3s + ~400MB RAM |
|
||||
| `auto` | Runtime resolution: remote (if key exists) → static → transformers → null | Depends | Depends |
|
||||
|
||||
**Resolution order for `auto`:**
|
||||
|
||||
1. Find first provider in `listEmbeddingProviders()` with `hasKey === true` → `remote`.
|
||||
2. If `settings.staticEnabled === true` → `static`.
|
||||
3. If `settings.transformersEnabled === true` → `transformers`.
|
||||
@@ -123,6 +125,7 @@ RRF(d) = Σ 1 / (k + rank_i(d)) where k = 60 (configurable via MEMORY_RRF_
|
||||
```
|
||||
|
||||
Concretely:
|
||||
|
||||
1. Run FTS5 search → ranked list `R_fts` (position 1..N).
|
||||
2. Run KNN vector search → ranked list `R_vec` (position 1..M).
|
||||
3. For each unique `memoryId`:
|
||||
@@ -150,6 +153,7 @@ amortizes the backfill cost across real requests without blocking startup.
|
||||
`GET /api/memory/engine-status` (`vectorStore.needsReindex`).
|
||||
|
||||
The `memory_vec_meta` table (migration `073_memory_vec.sql`) stores:
|
||||
|
||||
- `active_dim` — current vector dimension (null = not yet calibrated).
|
||||
- `embedding_signature` — `${source}:${model}:${dim}` used to detect changes.
|
||||
- `last_reset_at` — timestamp of last full reset.
|
||||
@@ -160,15 +164,15 @@ The `memory_vec_meta` table (migration `073_memory_vec.sql`) stores:
|
||||
Seven new fields were added to `MemorySettingsExtended` (plan 21, D9) in
|
||||
`src/shared/schemas/memory.ts`, persisted via `src/lib/db/settings.ts`:
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
| ---------------------- | --------------------------------------------- | ------------ | --------------------------------------------- |
|
||||
| `embeddingSource` | `"remote" \| "static" \| "transformers" \| "auto"` | `"auto"` | Which embedding source to use |
|
||||
| `embeddingProviderModel` | `string \| null` | `null` | Provider/model in `provider/model` format |
|
||||
| `transformersEnabled` | `boolean` | `false` | Opt-in for Transformers.js (MiniLM, ~400MB) |
|
||||
| `staticEnabled` | `boolean` | `false` | Opt-in for static potion-base-8M local model |
|
||||
| `rerankEnabled` | `boolean` | `false` | Enable reranking step (adds +200-500ms/req) |
|
||||
| `rerankProviderModel` | `string \| null` | `null` | Rerank provider/model in `provider/model` format |
|
||||
| `vectorStore` | `"sqlite-vec" \| "qdrant" \| "auto"` | `"auto"` | Which vector backend to use |
|
||||
| Field | Type | Default | Description |
|
||||
| ------------------------ | -------------------------------------------------- | -------- | ------------------------------------------------ |
|
||||
| `embeddingSource` | `"remote" \| "static" \| "transformers" \| "auto"` | `"auto"` | Which embedding source to use |
|
||||
| `embeddingProviderModel` | `string \| null` | `null` | Provider/model in `provider/model` format |
|
||||
| `transformersEnabled` | `boolean` | `false` | Opt-in for Transformers.js (MiniLM, ~400MB) |
|
||||
| `staticEnabled` | `boolean` | `false` | Opt-in for static potion-base-8M local model |
|
||||
| `rerankEnabled` | `boolean` | `false` | Enable reranking step (adds +200-500ms/req) |
|
||||
| `rerankProviderModel` | `string \| null` | `null` | Rerank provider/model in `provider/model` format |
|
||||
| `vectorStore` | `"sqlite-vec" \| "qdrant" \| "auto"` | `"auto"` | Which vector backend to use |
|
||||
|
||||
These are exposed via `GET /PUT /api/settings/memory` (schema `MemorySettingsExtendedSchema`).
|
||||
|
||||
@@ -239,13 +243,13 @@ The settings UI exposes Qdrant config, health check, semantic search test,
|
||||
and cleanup in the **Engine tab** of `/dashboard/memory`. The corresponding
|
||||
routes under `src/app/api/settings/qdrant/` are all wired as of v3.8.6:
|
||||
|
||||
| Route | Method | Description |
|
||||
| ----- | ------ | ----------- |
|
||||
| `/api/settings/qdrant` | `GET` / `PUT` | Read / update Qdrant settings |
|
||||
| `/api/settings/qdrant/health` | `GET` | Liveness probe + latency |
|
||||
| `/api/settings/qdrant/search` | `POST` | Semantic search test |
|
||||
| `/api/settings/qdrant/cleanup` | `POST` | Remove expired / old points |
|
||||
| `/api/settings/qdrant/embedding-models` | `GET` | List available embedding models |
|
||||
| Route | Method | Description |
|
||||
| --------------------------------------- | ------------- | ------------------------------- |
|
||||
| `/api/settings/qdrant` | `GET` / `PUT` | Read / update Qdrant settings |
|
||||
| `/api/settings/qdrant/health` | `GET` | Liveness probe + latency |
|
||||
| `/api/settings/qdrant/search` | `POST` | Semantic search test |
|
||||
| `/api/settings/qdrant/cleanup` | `POST` | Remove expired / old points |
|
||||
| `/api/settings/qdrant/embedding-models` | `GET` | List available embedding models |
|
||||
|
||||
### Vector quantization (int8 — opt-in, both backends)
|
||||
|
||||
@@ -254,10 +258,10 @@ footprint of stored vectors (~4× smaller than Float32) at a small recall cost.
|
||||
Default is **off** on both — vectors stay full-precision unless explicitly
|
||||
enabled.
|
||||
|
||||
| Backend | Setting | Type | Default | Where read |
|
||||
| ------------ | -------------------------------- | ----------------------------- | -------- | --------------------------------------------------- |
|
||||
| Qdrant | `qdrantQuantization` (DB key) | `"none" \| "int8" \| "binary"` | `"none"` | `src/lib/memory/qdrant.ts::normalizeQdrantConfig()` |
|
||||
| sqlite-vec | `MEMORY_VEC_QUANTIZATION` (env) | `"none" \| "int8"` | `"none"` | `src/lib/memory/vectorStore.ts::requestedVecQuantization()` |
|
||||
| Backend | Setting | Type | Default | Where read |
|
||||
| ---------- | ------------------------------- | ------------------------------ | -------- | ----------------------------------------------------------- |
|
||||
| Qdrant | `qdrantQuantization` (DB key) | `"none" \| "int8" \| "binary"` | `"none"` | `src/lib/memory/qdrant.ts::normalizeQdrantConfig()` |
|
||||
| sqlite-vec | `MEMORY_VEC_QUANTIZATION` (env) | `"none" \| "int8"` | `"none"` | `src/lib/memory/vectorStore.ts::requestedVecQuantization()` |
|
||||
|
||||
- **Qdrant** is configured per-instance via the `qdrantQuantization` setting
|
||||
key (exposed as the `quantization` field on `PUT /api/settings/qdrant`). When
|
||||
@@ -381,15 +385,15 @@ strategy via `toMemoryRetrievalConfig()` (chronological order).
|
||||
|
||||
See also the "Settings extension" section above for field descriptions.
|
||||
|
||||
| DB key | API field | Default |
|
||||
| ------------------------- | ---------------------- | ------------- |
|
||||
| `memoryEmbeddingSource` | `embeddingSource` | `"auto"` |
|
||||
| `memoryEmbeddingModel` | `embeddingProviderModel` | `null` |
|
||||
| `memoryTransformersEnabled` | `transformersEnabled` | `false` |
|
||||
| `memoryStaticEnabled` | `staticEnabled` | `false` |
|
||||
| `memoryRerankEnabled` | `rerankEnabled` | `false` |
|
||||
| `memoryRerankModel` | `rerankProviderModel` | `null` |
|
||||
| `memoryVectorStore` | `vectorStore` | `"auto"` |
|
||||
| DB key | API field | Default |
|
||||
| --------------------------- | ------------------------ | -------- |
|
||||
| `memoryEmbeddingSource` | `embeddingSource` | `"auto"` |
|
||||
| `memoryEmbeddingModel` | `embeddingProviderModel` | `null` |
|
||||
| `memoryTransformersEnabled` | `transformersEnabled` | `false` |
|
||||
| `memoryStaticEnabled` | `staticEnabled` | `false` |
|
||||
| `memoryRerankEnabled` | `rerankEnabled` | `false` |
|
||||
| `memoryRerankModel` | `rerankProviderModel` | `null` |
|
||||
| `memoryVectorStore` | `vectorStore` | `"auto"` |
|
||||
|
||||
Qdrant-related DB keys (`qdrantEnabled`, `qdrantHost`, `qdrantPort`,
|
||||
`qdrantApiKey`, `qdrantCollection` default `"omniroute_memory"`,
|
||||
@@ -400,16 +404,16 @@ Qdrant-related DB keys (`qdrantEnabled`, `qdrantHost`, `qdrantPort`,
|
||||
|
||||
Six optional env vars tune the engine's runtime behaviour (documented in `.env.example`):
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------------------- | ------- | -------------------------------------------------- |
|
||||
| `MEMORY_EMBEDDING_CACHE_TTL_MS` | `300000` | Embedding cache TTL (5 min) |
|
||||
| `MEMORY_EMBEDDING_CACHE_MAX` | `1000` | Max entries in embedding LRU cache |
|
||||
| `MEMORY_TRANSFORMERS_MODEL` | `Xenova/all-MiniLM-L6-v2` | HF repo for Transformers.js model |
|
||||
| `MEMORY_STATIC_MODEL` | `minishlab/potion-base-8M` | HF repo for static potion model |
|
||||
| `MEMORY_STATIC_CACHE_DIR` | `<DATA_DIR>/embeddings` | Where to store downloaded models |
|
||||
| `MEMORY_VEC_TOP_K` | `20` | Default top-K for vector search |
|
||||
| `MEMORY_RRF_K` | `60` | RRF k constant for hybrid search |
|
||||
| `MEMORY_VEC_QUANTIZATION` | `none` | Set to `int8` to store local sqlite-vec vectors quantized (~4× smaller; opt-in). Mode change forces a reindex. |
|
||||
| Variable | Default | Description |
|
||||
| ------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| `MEMORY_EMBEDDING_CACHE_TTL_MS` | `300000` | Embedding cache TTL (5 min) |
|
||||
| `MEMORY_EMBEDDING_CACHE_MAX` | `1000` | Max entries in embedding LRU cache |
|
||||
| `MEMORY_TRANSFORMERS_MODEL` | `Xenova/all-MiniLM-L6-v2` | HF repo for Transformers.js model |
|
||||
| `MEMORY_STATIC_MODEL` | `minishlab/potion-base-8M` | HF repo for static potion model |
|
||||
| `MEMORY_STATIC_CACHE_DIR` | `<DATA_DIR>/embeddings` | Where to store downloaded models |
|
||||
| `MEMORY_VEC_TOP_K` | `20` | Default top-K for vector search |
|
||||
| `MEMORY_RRF_K` | `60` | RRF k constant for hybrid search |
|
||||
| `MEMORY_VEC_QUANTIZATION` | `none` | Set to `int8` to store local sqlite-vec vectors quantized (~4× smaller; opt-in). Mode change forces a reindex. |
|
||||
|
||||
## Summarisation (`summarization.ts`)
|
||||
|
||||
@@ -431,37 +435,37 @@ All endpoints require management auth (`requireManagementAuth`).
|
||||
|
||||
### Core memory endpoints (existing + updated)
|
||||
|
||||
| Method | Path | Description |
|
||||
| ---------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `GET` | `/api/memory` | Paginated list with filters: `apiKeyId`, `type`, `sessionId`, `q`, `limit`, `page`, `offset`. Response includes `stats.total`, `stats.tokensUsed`, `stats.hitRate`, `cacheStats` |
|
||||
| `POST` | `/api/memory` | Create entry (Zod-validated: `content`, `key`, optional `type`, `sessionId`, `apiKeyId`, `metadata`, `expiresAt`). Calls `createMemory()` which upserts on `(apiKeyId, key)` |
|
||||
| `GET` | `/api/memory/[id]` | Fetch a single entry by UUID |
|
||||
| `PUT` | `/api/memory/[id]` | Update entry fields (`type`, `key`, `content`, `metadata`). Body: `MemoryUpdatePutSchema`. Also syncs vector if embedding source available. |
|
||||
| `DELETE` | `/api/memory/[id]` | Delete an entry; also deletes from `vec_memories` (D15) and Qdrant best-effort. Returns 404 when missing. |
|
||||
| `GET` | `/api/memory/health` | Runs `verifyExtractionPipeline("health-check")` — round-trip create→list→delete. Returns `{working, latencyMs, error?}` |
|
||||
| Method | Path | Description |
|
||||
| -------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `GET` | `/api/memory` | Paginated list with filters: `apiKeyId`, `type`, `sessionId`, `q`, `limit`, `page`, `offset`. Response includes `stats.total`, `stats.tokensUsed`, `stats.hitRate`, `cacheStats` |
|
||||
| `POST` | `/api/memory` | Create entry (Zod-validated: `content`, `key`, optional `type`, `sessionId`, `apiKeyId`, `metadata`, `expiresAt`). Calls `createMemory()` which upserts on `(apiKeyId, key)` |
|
||||
| `GET` | `/api/memory/[id]` | Fetch a single entry by UUID |
|
||||
| `PUT` | `/api/memory/[id]` | Update entry fields (`type`, `key`, `content`, `metadata`). Body: `MemoryUpdatePutSchema`. Also syncs vector if embedding source available. |
|
||||
| `DELETE` | `/api/memory/[id]` | Delete an entry; also deletes from `vec_memories` (D15) and Qdrant best-effort. Returns 404 when missing. |
|
||||
| `GET` | `/api/memory/health` | Runs `verifyExtractionPipeline("health-check")` — round-trip create→list→delete. Returns `{working, latencyMs, error?}` |
|
||||
|
||||
### New memory engine endpoints (plan 21)
|
||||
|
||||
| Method | Path | Description |
|
||||
| -------- | ---------------------------------- | --------------------------------------------------------------------------------------- |
|
||||
| `POST` | `/api/memory/retrieve-preview` | Dry-run of `retrieveMemories` — returns ranked results with score, tier, tokens. Body: `RetrievePreviewSchema`. Does NOT inject or modify memories. |
|
||||
| `GET` | `/api/memory/embedding-providers` | Lists providers with embedding models, indicating which have a configured API key. |
|
||||
| `GET` | `/api/memory/engine-status` | Returns full engine status: keyword tier, embedding resolution, vector store stats, Qdrant health, rerank config. Shape: `MemoryEngineStatusSchema`. |
|
||||
| `POST` | `/api/memory/summarize` | Manually trigger memory compaction. Body: `MemorySummarizeSchema` (`olderThanDays`, `apiKeyId?`, `dryRun`). Returns `{candidates, tokensSaved}`. |
|
||||
| `POST` | `/api/memory/reindex` | Trigger vector reindex for memories with `needs_reindex=1`. Body: `MemoryReindexSchema` (`force`). Returns `{started, pending}`. |
|
||||
| Method | Path | Description |
|
||||
| ------ | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `POST` | `/api/memory/retrieve-preview` | Dry-run of `retrieveMemories` — returns ranked results with score, tier, tokens. Body: `RetrievePreviewSchema`. Does NOT inject or modify memories. |
|
||||
| `GET` | `/api/memory/embedding-providers` | Lists providers with embedding models, indicating which have a configured API key. |
|
||||
| `GET` | `/api/memory/engine-status` | Returns full engine status: keyword tier, embedding resolution, vector store stats, Qdrant health, rerank config. Shape: `MemoryEngineStatusSchema`. |
|
||||
| `POST` | `/api/memory/summarize` | Manually trigger memory compaction. Body: `MemorySummarizeSchema` (`olderThanDays`, `apiKeyId?`, `dryRun`). Returns `{candidates, tokensSaved}`. |
|
||||
| `POST` | `/api/memory/reindex` | Trigger vector reindex for memories with `needs_reindex=1`. Body: `MemoryReindexSchema` (`force`). Returns `{started, pending}`. |
|
||||
|
||||
### Settings endpoints
|
||||
|
||||
| Method | Path | Description |
|
||||
| -------- | ---------------------------------- | ------------------------------------------------------------------------ |
|
||||
| `GET` | `/api/settings/memory` | Current normalised `MemorySettingsExtended` (7 new fields + legacy) |
|
||||
| `PUT` | `/api/settings/memory` | Update any field from `MemorySettingsExtendedSchema` (12 total fields) |
|
||||
| `GET` | `/api/settings/qdrant` | Current Qdrant settings (`QdrantSettingsSchema`) |
|
||||
| `PUT` | `/api/settings/qdrant` | Update Qdrant settings. Body: `QdrantSettingsUpdateSchema`. `apiKey` = empty string removes key. |
|
||||
| `GET` | `/api/settings/qdrant/health` | Liveness probe against configured Qdrant instance. Returns `QdrantHealthResultSchema`. |
|
||||
| `POST` | `/api/settings/qdrant/search` | Semantic search test against Qdrant. Body: `QdrantSearchSchema` (`query`, `topK`). |
|
||||
| `POST` | `/api/settings/qdrant/cleanup` | Remove Qdrant points for expired / old memories. |
|
||||
| `GET` | `/api/settings/qdrant/embedding-models` | List embedding models available for Qdrant. |
|
||||
| Method | Path | Description |
|
||||
| ------ | --------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| `GET` | `/api/settings/memory` | Current normalised `MemorySettingsExtended` (7 new fields + legacy) |
|
||||
| `PUT` | `/api/settings/memory` | Update any field from `MemorySettingsExtendedSchema` (12 total fields) |
|
||||
| `GET` | `/api/settings/qdrant` | Current Qdrant settings (`QdrantSettingsSchema`) |
|
||||
| `PUT` | `/api/settings/qdrant` | Update Qdrant settings. Body: `QdrantSettingsUpdateSchema`. `apiKey` = empty string removes key. |
|
||||
| `GET` | `/api/settings/qdrant/health` | Liveness probe against configured Qdrant instance. Returns `QdrantHealthResultSchema`. |
|
||||
| `POST` | `/api/settings/qdrant/search` | Semantic search test against Qdrant. Body: `QdrantSearchSchema` (`query`, `topK`). |
|
||||
| `POST` | `/api/settings/qdrant/cleanup` | Remove Qdrant points for expired / old memories. |
|
||||
| `GET` | `/api/settings/qdrant/embedding-models` | List embedding models available for Qdrant. |
|
||||
|
||||
The `/api/memory` list query supports either `page`-based pagination
|
||||
(`parsePaginationParams`) **or** raw `offset` — when `offset` is present it
|
||||
@@ -490,6 +494,7 @@ See [MCP-SERVER.md](./MCP-SERVER.md) for transport and scope details.
|
||||
`src/app/(dashboard)/dashboard/memory/page.tsx` is now a **3-tab Studio**:
|
||||
|
||||
### Tab: Memórias / Memories
|
||||
|
||||
- Concept card (collapsible "How it works" explainer).
|
||||
- Real-time list, search, and pagination (debounced 300 ms).
|
||||
- Type filter (`factual` / `episodic` / `procedural` / `semantic` / all).
|
||||
@@ -503,6 +508,7 @@ See [MCP-SERVER.md](./MCP-SERVER.md) for transport and scope details.
|
||||
- A green/red health dot driven by `GET /api/memory/health`.
|
||||
|
||||
### Tab: Playground
|
||||
|
||||
- Query input + strategy selector (Exact / Semantic / Hybrid) + token budget.
|
||||
- "Simulate" → `POST /api/memory/retrieve-preview` — shows ranked results with
|
||||
`score`, `tier`, `tokens`, `vecScore`, `ftsScore`.
|
||||
@@ -510,6 +516,7 @@ See [MCP-SERVER.md](./MCP-SERVER.md) for transport and scope details.
|
||||
whether a fallback occurred.
|
||||
|
||||
### Tab: Engine
|
||||
|
||||
- Engine status panel (keyword FTS5 chip, embedding chip, vector store chip,
|
||||
Qdrant health chip, rerank chip).
|
||||
- "Reindex Now" button → `POST /api/memory/reindex`.
|
||||
@@ -574,10 +581,10 @@ default TTL 5 min).
|
||||
- `src/app/api/memory/reindex/route.ts`
|
||||
- `src/app/api/settings/memory/route.ts`
|
||||
- `src/app/api/settings/qdrant/route.ts` + sub-routes
|
||||
- `src/app/(dashboard)/dashboard/memory/` — Studio UI (page + components +
|
||||
tabs + hooks)
|
||||
- `open-sse/handlers/chatCore.ts` (injection / extraction wiring)
|
||||
- `open-sse/mcp-server/tools/memoryTools.ts`
|
||||
- `src/app/(dashboard)/dashboard/memory/` — Studio UI (page + components +
|
||||
tabs + hooks)
|
||||
- `open-sse/handlers/chatCore.ts` (injection / extraction wiring)
|
||||
- `open-sse/mcp-server/tools/memoryTools.ts`
|
||||
|
||||
---
|
||||
|
||||
@@ -587,12 +594,12 @@ OmniRoute's memory engine supports **four embedding sources** (`src/lib/memory/e
|
||||
|
||||
### The Four Providers
|
||||
|
||||
| Provider | Source | Latency | Cost | Quality | Setup |
|
||||
|----------|--------|---------|------|---------|-------|
|
||||
| `transformers` | Local ONNX model (Xenova/all-MiniLM-L6-v2) | ~50-150ms (CPU) | Free | Good | `npm install` only |
|
||||
| `static` | Pre-computed vectors (cached) | <1ms | Free | N/A (depends on cache hit) | None |
|
||||
| `remote` | OpenAI / Cohere / Voyage API | ~100-300ms | $0.02-0.10/1M tokens | Excellent | API key |
|
||||
| `cache` | In-memory LRU layer over any source | <1ms (hit), full latency (miss) | Free | Same as underlying | None |
|
||||
| Provider | Source | Latency | Cost | Quality | Setup |
|
||||
| -------------- | ------------------------------------------ | ------------------------------- | -------------------- | -------------------------- | ------------------ |
|
||||
| `transformers` | Local ONNX model (Xenova/all-MiniLM-L6-v2) | ~50-150ms (CPU) | Free | Good | `npm install` only |
|
||||
| `static` | Pre-computed vectors (cached) | <1ms | Free | N/A (depends on cache hit) | None |
|
||||
| `remote` | OpenAI / Cohere / Voyage API | ~100-300ms | $0.02-0.10/1M tokens | Excellent | API key |
|
||||
| `cache` | In-memory LRU layer over any source | <1ms (hit), full latency (miss) | Free | Same as underlying | None |
|
||||
|
||||
### Decision Tree
|
||||
|
||||
@@ -643,16 +650,17 @@ The cache is always on by default and configured via env vars:
|
||||
MEMORY_EMBEDDING_CACHE_MAX=1000 # Max cached items
|
||||
MEMORY_EMBEDDING_CACHE_TTL_MS=300000 # TTL (5 min)
|
||||
```
|
||||
|
||||
### Performance Numbers
|
||||
|
||||
Benchmark on a typical 4-core x86 server (texts ~100 tokens each):
|
||||
|
||||
| Provider | p50 | p95 | p99 | Cost / 1M embeddings |
|
||||
|----------|-----|-----|-----|-----------------------|
|
||||
| `transformers` (CPU) | 80ms | 180ms | 350ms | Free |
|
||||
| `remote` (OpenAI) | 120ms | 220ms | 400ms | ~$0.02 (ada-002) / $0.13 (3-large) |
|
||||
| `static` (Qdrant) | 15ms | 30ms | 60ms | Depends on Qdrant hosting |
|
||||
| `cache` (hit) | <1ms | <1ms | 2ms | Free |
|
||||
| Provider | p50 | p95 | p99 | Cost / 1M embeddings |
|
||||
| -------------------- | ----- | ----- | ----- | ---------------------------------- |
|
||||
| `transformers` (CPU) | 80ms | 180ms | 350ms | Free |
|
||||
| `remote` (OpenAI) | 120ms | 220ms | 400ms | ~$0.02 (ada-002) / $0.13 (3-large) |
|
||||
| `static` (Qdrant) | 15ms | 30ms | 60ms | Depends on Qdrant hosting |
|
||||
| `cache` (hit) | <1ms | <1ms | 2ms | Free |
|
||||
|
||||
---
|
||||
|
||||
@@ -662,43 +670,43 @@ The `extraction.ts` module (`src/lib/memory/extraction.ts`) uses **regex pattern
|
||||
|
||||
### Default Pattern Categories
|
||||
|
||||
| Category | Example pattern | Captures |
|
||||
|----------|-----------------|----------|
|
||||
| PREFERENCE_PATTERNS | `"I prefer <X>"`, `"I like <X>"`, `"I hate <X>"` | User preferences |
|
||||
| DECISION_PATTERNS | `"I'll use <X>"`, `"I decided to <X>"`, `"I went with <X>"` | User decisions (episodic) |
|
||||
| PATTERN_PATTERNS | `"I usually <X>"`, `"I always <X>"`, `"I never <X>"` | Persistent behavioral patterns |
|
||||
| Category | Example pattern | Captures |
|
||||
| ------------------- | ----------------------------------------------------------- | ------------------------------ |
|
||||
| PREFERENCE_PATTERNS | `"I prefer <X>"`, `"I like <X>"`, `"I hate <X>"` | User preferences |
|
||||
| DECISION_PATTERNS | `"I'll use <X>"`, `"I decided to <X>"`, `"I went with <X>"` | User decisions (episodic) |
|
||||
| PATTERN_PATTERNS | `"I usually <X>"`, `"I always <X>"`, `"I never <X>"` | Persistent behavioral patterns |
|
||||
|
||||
### Example Patterns (Simplified)
|
||||
|
||||
```ts
|
||||
// From src/lib/memory/extraction.ts
|
||||
const PREFERENCE_PATTERNS = [
|
||||
/\bI\s+(?:really\s+)?prefer\s+([^.,\n]+)/gi,
|
||||
/\bI\s+(?:really\s+)?like\s+([^.,\n]+)/gi,
|
||||
/\bI\s+(?:hate|dislike|avoid)\s+([^.,\n]+)/gi
|
||||
/\bI\s+(?:hate|dislike|avoid)\s+([^.,\n]+)/gi,
|
||||
];
|
||||
const DECISION_PATTERNS = [
|
||||
/\bI'?(?:ll|will)\s+use\s+([^.,\n]+)/gi,
|
||||
/\bI\s+(?:have\s+)?decided\s+(?:to\s+)?([^.,\n]+)/gi
|
||||
];
|
||||
const PATTERN_PATTERNS = [
|
||||
/\bI\s+usually\s+([^.,\n]+)/gi,
|
||||
/\bI\s+always\s+([^.,\n]+)/gi
|
||||
/\bI\s+(?:have\s+)?decided\s+(?:to\s+)?([^.,\n]+)/gi,
|
||||
];
|
||||
const PATTERN_PATTERNS = [/\bI\s+usually\s+([^.,\n]+)/gi, /\bI\s+always\s+([^.,\n]+)/gi];
|
||||
```
|
||||
|
||||
### What Gets Extracted
|
||||
|
||||
When a user says:
|
||||
|
||||
> "I prefer TypeScript. I'll use Postgres for this project. I always commit before pushing. I don't like Python."
|
||||
Extraction produces 4 memories:
|
||||
| Key | Category | Type | Content |
|
||||
|-----|----------|------|---------|
|
||||
| `preference:typescript` | preference | factual | "TypeScript" |
|
||||
| `decision:postgres_for_this_project` | decision | episodic | "Postgres for this project" |
|
||||
| `pattern:commit_before_pushing` | pattern | factual | "commit before pushing" |
|
||||
| `preference:python` | preference | factual | "Python" |
|
||||
> Extraction produces 4 memories:
|
||||
> | Key | Category | Type | Content |
|
||||
> |-----|----------|------|---------|
|
||||
> | `preference:typescript` | preference | factual | "TypeScript" |
|
||||
> | `decision:postgres_for_this_project` | decision | episodic | "Postgres for this project" |
|
||||
> | `pattern:commit_before_pushing` | pattern | factual | "commit before pushing" |
|
||||
> | `preference:python` | preference | factual | "Python" |
|
||||
|
||||
### Extraction Limits
|
||||
|
||||
To prevent runaway extraction, the following limits apply:
|
||||
|
||||
| Min content length | 3 chars |
|
||||
@@ -709,6 +717,7 @@ To prevent runaway extraction, the following limits apply:
|
||||
Extraction runs automatically whenever memory is enabled; there is no separate
|
||||
extraction-only toggle. To turn it off, disable memory entirely (`enabled: false`
|
||||
via `PUT /api/settings/memory`). Consider doing so when:
|
||||
|
||||
- You have high message volume and the extraction cost is non-trivial
|
||||
- Your conversations are mostly transient (chat, debugging) with no long-term value
|
||||
- You're already capturing context via custom plugins
|
||||
@@ -728,18 +737,19 @@ RRF(d) = Σ 1 / (k + rank_i(d))
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
- `k` is the constant (default 60)
|
||||
- `rank_i(d)` is the rank of document `d` in the i-th retrieval system (FTS, vector)
|
||||
- The sum runs over all retrieval systems
|
||||
|
||||
### How `k` Affects Results
|
||||
|
||||
| `k` value | Effect | Best for |
|
||||
|-----------|--------|----------|
|
||||
| `k=0` | Pure rank fusion (no smoothing) | Theoretical baseline |
|
||||
| `k=10-30` | Heavily weights top results, low-rank barely contributes | When top-3 results are usually correct |
|
||||
| **`k=60`** (default) | Balanced — top-10 results all contribute meaningfully | General-purpose retrieval |
|
||||
| `k=100+` | Flatter — even low-rank results can dominate if they appear in multiple systems | When recall > precision is critical |
|
||||
| `k` value | Effect | Best for |
|
||||
| -------------------- | ------------------------------------------------------------------------------- | -------------------------------------- |
|
||||
| `k=0` | Pure rank fusion (no smoothing) | Theoretical baseline |
|
||||
| `k=10-30` | Heavily weights top results, low-rank barely contributes | When top-3 results are usually correct |
|
||||
| **`k=60`** (default) | Balanced — top-10 results all contribute meaningfully | General-purpose retrieval |
|
||||
| `k=100+` | Flatter — even low-rank results can dominate if they appear in multiple systems | When recall > precision is critical |
|
||||
|
||||
### Tuning `k` in Practice
|
||||
|
||||
@@ -755,12 +765,14 @@ MEMORY_RRF_K=120
|
||||
```
|
||||
|
||||
**Example with `k=20`:**
|
||||
|
||||
- FTS rank 1 → contribution `1/21 = 0.048`
|
||||
- FTS rank 10 → contribution `1/30 = 0.033`
|
||||
- Vector rank 1 → contribution `0.048`
|
||||
- Combined max: `0.096`
|
||||
|
||||
**Example with `k=60`:**
|
||||
|
||||
- FTS rank 1 → contribution `1/61 = 0.016`
|
||||
- FTS rank 10 → contribution `1/70 = 0.014`
|
||||
- Vector rank 1 → contribution `0.016`
|
||||
@@ -770,12 +782,12 @@ With higher `k`, the **relative difference** between top-1 and rank-10 is smalle
|
||||
|
||||
### When to Change `k`
|
||||
|
||||
| Symptom | Try |
|
||||
|---------|-----|
|
||||
| Top result always wins, but it's wrong | **Lower** k (e.g., 20) — top-rank confidence matters more |
|
||||
| Symptom | Try |
|
||||
| -------------------------------------- | ------------------------------------------------------------ |
|
||||
| Top result always wins, but it's wrong | **Lower** k (e.g., 20) — top-rank confidence matters more |
|
||||
| Right answer is in top-5 but not top-1 | **Higher** k (e.g., 100) — flatter scoring rewards consensus |
|
||||
| Recall is high but precision is low | **Lower** k — sharpen the ranking |
|
||||
| Recall is low (missing relevant docs) | **Higher** k — give lower-ranked docs a chance |
|
||||
| Recall is high but precision is low | **Lower** k — sharpen the ranking |
|
||||
| Recall is low (missing relevant docs) | **Higher** k — give lower-ranked docs a chance |
|
||||
|
||||
### RRF Weighting
|
||||
|
||||
@@ -795,9 +807,9 @@ The `summarization.ts` module (`src/lib/memory/summarization.ts`) compresses old
|
||||
|
||||
### When Summarization Triggers
|
||||
|
||||
| Trigger | Threshold (default) |
|
||||
|---------|---------------------|
|
||||
| Manual trigger via API | n/a |
|
||||
| Trigger | Threshold (default) |
|
||||
| ---------------------- | ------------------- |
|
||||
| Manual trigger via API | n/a |
|
||||
|
||||
### What Gets Summarized
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Notion Context Source"
|
||||
version: 3.8.24
|
||||
lastUpdated: 2026-06-13
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Notion Context Source
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Obsidian Context Source"
|
||||
version: 3.8.24
|
||||
lastUpdated: 2026-06-13
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Obsidian Context Source
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "OpenCode Integration"
|
||||
version: 3.8.2
|
||||
lastUpdated: 2026-05-14
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# OpenCode Integration
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
---
|
||||
title: "open-sse Architecture"
|
||||
version: 3.8.16
|
||||
lastUpdated: 2026-06-08
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# open-sse Architecture
|
||||
|
||||
> **TL;DR**: `open-sse/` is the core streaming engine that powers every LLM request in OmniRoute. It contains ~406 files implementing the request pipeline, executors, services, MCP server, and translation layer. This guide explains how the pieces fit together.
|
||||
> **TL;DR**: `open-sse/` is the core streaming engine that powers every LLM request in OmniRoute. It contains ~900 files implementing the request pipeline, executors, services, MCP server, and translation layer. This guide explains how the pieces fit together.
|
||||
|
||||
**Source:** `open-sse/` (workspace package, ~143K LOC across 406 files)
|
||||
**Source:** `open-sse/` (workspace package, ~900 files; 811 `.ts`)
|
||||
|
||||
---
|
||||
|
||||
@@ -36,11 +36,11 @@ open-sse/
|
||||
├── types.d.ts # Public type exports
|
||||
├── package.json # @omniroute/open-sse
|
||||
├── config/ # Provider configs, constants, registries
|
||||
├── executors/ # Per-provider HTTP executors (59 files)
|
||||
├── executors/ # Per-provider HTTP executors (67 + base.ts/index.ts)
|
||||
├── handlers/ # Request handlers (chatCore, responses, etc.)
|
||||
├── lib/ # Internal utilities
|
||||
├── mcp-server/ # Model Context Protocol server
|
||||
├── services/ # ~114 service modules
|
||||
├── services/ # ~298 service modules
|
||||
├── transformer/ # Responses API format transformer
|
||||
├── translator/ # Format translation (OpenAI ↔ Claude ↔ Gemini)
|
||||
└── utils/ # Shared utilities (logging, error, stream, etc.)
|
||||
@@ -49,12 +49,12 @@ open-sse/
|
||||
### Module Counts
|
||||
|
||||
| Directory | Files | Purpose |
|
||||
| `executors/` | 62 | Per-provider HTTP executors (unified via DefaultExecutor factory) |
|
||||
| `handlers/` | ~15 | Request entry points (chatCore, responses, embeddings) |
|
||||
| `services/` | ~114 | Routing, caching, rate limiting, refresh, etc. |
|
||||
| `translator/` | ~10 | Format conversion (OpenAI ↔ Claude ↔ Gemini) |
|
||||
| `mcp-server/` | 30 | MCP tools and transports |
|
||||
| `utils/` | ~30 | Cross-cutting utilities (logging, error, stream) |
|
||||
| `executors/` | 68 | Per-provider HTTP executors (unified via DefaultExecutor factory) |
|
||||
| `handlers/` | 16 | Request entry points (chatCore, responses, embeddings) |
|
||||
| `services/` | ~298 | Routing, caching, rate limiting, refresh, etc. |
|
||||
| `translator/` | ~27 | Format conversion (OpenAI ↔ Claude ↔ Gemini) |
|
||||
| `mcp-server/` | 32 | MCP tools and transports |
|
||||
| `utils/` | ~65 | Cross-cutting utilities (logging, error, stream) |
|
||||
| `config/` | ~10 | Provider configs, constants, registries |
|
||||
|
||||
---
|
||||
@@ -104,6 +104,7 @@ Resolves the request to a concrete `(provider, model, account, credentials)` tup
|
||||
- Pick the next viable target
|
||||
|
||||
For `auto/*` models, this stage also:
|
||||
|
||||
- Runs the **9-factor scoring** algorithm (`services/autoCombo/`)
|
||||
- Selects a `provider+model` pair based on health, cost, latency, etc.
|
||||
|
||||
@@ -136,6 +137,7 @@ All providers use `DefaultExecutor` (`executors/default.ts`) via the `getExecuto
|
||||
- Handles auth refresh if needed (OAuth providers)
|
||||
|
||||
All executors extend `BaseExecutor` (`executors/base.ts`, 1170 LOC) which provides:
|
||||
|
||||
- Common retry logic
|
||||
- Proxy integration
|
||||
- Circuit breaker integration
|
||||
@@ -177,17 +179,17 @@ export async function handleChat(request: NextRequest) {
|
||||
// 1. Auth + CORS
|
||||
await authenticateRequest(request);
|
||||
applyCorsHeaders(response);
|
||||
|
||||
|
||||
// 2. Body validation
|
||||
const body = await parseRequestBody(request);
|
||||
|
||||
|
||||
// 3. Format detection + translation
|
||||
const sourceFormat = detectFormat(request);
|
||||
const targetFormat = getTargetFormat(providerId);
|
||||
if (needsTranslation(sourceFormat, targetFormat)) {
|
||||
body = translateRequest(body, sourceFormat, targetFormat);
|
||||
}
|
||||
|
||||
|
||||
// 4. Combo routing
|
||||
const targets = await resolveComboTargets(comboId, body);
|
||||
for (const target of targets) {
|
||||
@@ -199,7 +201,7 @@ export async function handleChat(request: NextRequest) {
|
||||
// Continue to next target
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 5. Emergency fallback
|
||||
return await emergencyFallback(body);
|
||||
}
|
||||
@@ -228,29 +230,29 @@ export async function handleComboChat(body, comboId): Promise<ChatResult> {
|
||||
|
||||
Supports **17 routing strategies** (see `src/shared/constants/routingStrategies.ts`):
|
||||
|
||||
| Strategy | Behavior |
|
||||
|----------|----------|
|
||||
| `priority` | First-target ordered list |
|
||||
| `weighted` | Probabilistic by per-target weight |
|
||||
| `round-robin` | Cycle through targets in order |
|
||||
| `context-relay` | Hand off context across targets |
|
||||
| `fill-first` | Fill quota before moving to next |
|
||||
| `p2c` | Power of two choices |
|
||||
| `random` | Uniform random |
|
||||
| `least-used` | Pick the one with fewest recent uses |
|
||||
| `cost-optimized` | Cheapest healthy target first |
|
||||
| `reset-aware` | Aware of provider reset windows |
|
||||
| `reset-window` | Reset window-based routing |
|
||||
| `headroom` | Most remaining quota headroom first |
|
||||
| `strict-random` | Truly uniform (no quality weighting) |
|
||||
| `auto` | Use 9-factor scoring (`autoCombo/`) |
|
||||
| `lkgp` | Last known good provider first |
|
||||
| `context-optimized` | Best for long-context requests |
|
||||
| `fusion` | Fan out to a panel in parallel, then synthesize via a judge (`fusion.ts`) |
|
||||
| Strategy | Behavior |
|
||||
| ------------------- | ------------------------------------------------------------------------- |
|
||||
| `priority` | First-target ordered list |
|
||||
| `weighted` | Probabilistic by per-target weight |
|
||||
| `round-robin` | Cycle through targets in order |
|
||||
| `context-relay` | Hand off context across targets |
|
||||
| `fill-first` | Fill quota before moving to next |
|
||||
| `p2c` | Power of two choices |
|
||||
| `random` | Uniform random |
|
||||
| `least-used` | Pick the one with fewest recent uses |
|
||||
| `cost-optimized` | Cheapest healthy target first |
|
||||
| `reset-aware` | Aware of provider reset windows |
|
||||
| `reset-window` | Reset window-based routing |
|
||||
| `headroom` | Most remaining quota headroom first |
|
||||
| `strict-random` | Truly uniform (no quality weighting) |
|
||||
| `auto` | Use 9-factor scoring (`autoCombo/`) |
|
||||
| `lkgp` | Last known good provider first |
|
||||
| `context-optimized` | Best for long-context requests |
|
||||
| `fusion` | Fan out to a panel in parallel, then synthesize via a judge (`fusion.ts`) |
|
||||
|
||||
### base.ts (1170 LOC)
|
||||
|
||||
The **abstract executor** that all 59 executors extend. It contains:
|
||||
The **abstract executor** that all 67 executors extend. It contains:
|
||||
|
||||
- `buildUrl()` — default URL construction (subclasses override for custom)
|
||||
- `buildHeaders()` — default headers (auth, content-type)
|
||||
@@ -266,7 +268,8 @@ export class DefaultExecutor extends BaseExecutor {
|
||||
```
|
||||
|
||||
Provider-specific behavior (auth headers, base URL, version headers) is configured via the provider registry, not separate executor classes.
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
---
|
||||
|
||||
@@ -343,12 +346,13 @@ export default {
|
||||
id: "together",
|
||||
baseURL: "https://api.together.xyz/v1/chat/completions",
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
**Custom auth** is handled through the provider registry's auth configuration (API key, OAuth, header profiles).
|
||||
|
||||
**Custom request body** transformations (e.g., Anthropic separating `system` from `messages`) are registered per-provider in `open-sse/translator/`.
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
### The Executor Factory
|
||||
|
||||
@@ -362,7 +366,7 @@ const result = await executor.execute({
|
||||
model: "claude-sonnet-4-5",
|
||||
messages: [...],
|
||||
});
|
||||
```
|
||||
````
|
||||
|
||||
The factory is generated from `config/providerRegistry.ts` which lists all 212+ providers and their executor class.
|
||||
|
||||
@@ -383,6 +387,7 @@ if (needsTranslation(sourceFormat, targetFormat)) {
|
||||
```
|
||||
|
||||
Common translations:
|
||||
|
||||
- `OpenAI → Anthropic`: separate `system` field, `x-api-key` header
|
||||
- `OpenAI → Gemini`: `contents` instead of `messages`, `systemInstruction`
|
||||
- `OpenAI → Responses API`: `input` array, `previous_response_id` state
|
||||
@@ -404,6 +409,7 @@ Common translations:
|
||||
- **30+ tools** (provider management, combos, memory, cache, compression, 1proxy, skills)
|
||||
- **3 transports**: stdio, SSE, Streamable HTTP
|
||||
- **13 scopes** for fine-grained authorization
|
||||
|
||||
### Tool Registration
|
||||
|
||||
Tools are registered as standalone files in `open-sse/mcp-server/tools/`, each exporting a name, schema, handler, and scope:
|
||||
@@ -467,6 +473,7 @@ createResponsesApiTransformStream(): TransformStream
|
||||
```
|
||||
|
||||
This handles:
|
||||
|
||||
- `response.output_item.added` events
|
||||
- `response.output_text.delta` events
|
||||
- `response.completed` event
|
||||
@@ -478,13 +485,13 @@ This handles:
|
||||
|
||||
`open-sse/config/` holds the configuration layer:
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `providerRegistry.ts` | 212+ provider definitions |
|
||||
| `providerModels.ts` | Model aliases, format mapping |
|
||||
| `constants.ts` | Timeouts, limits, status codes |
|
||||
| File | Purpose |
|
||||
| ----------------------------- | --------------------------------- |
|
||||
| `providerRegistry.ts` | 212+ provider definitions |
|
||||
| `providerModels.ts` | Model aliases, format mapping |
|
||||
| `constants.ts` | Timeouts, limits, status codes |
|
||||
| `defaultThinkingSignature.ts` | Default Claude thinking signature |
|
||||
| `modelStrip.ts` (in services) | Per-provider field stripping |
|
||||
| `modelStrip.ts` (in services) | Per-provider field stripping |
|
||||
|
||||
### Provider Registry Schema
|
||||
|
||||
@@ -509,13 +516,13 @@ Zod validation at module load ensures all provider configs are valid.
|
||||
|
||||
The routing engine has strict performance budgets:
|
||||
|
||||
| Operation | Target | Measurement |
|
||||
|-----------|--------|-------------|
|
||||
| Combo resolution | <10ms | For 50 targets |
|
||||
| Rate limit check | <1ms | In-memory token bucket |
|
||||
| Model family fallback | <5ms | Cached family definitions |
|
||||
| Request routing dispatch | <2ms | Hot path |
|
||||
| **No blocking I/O in routing hot path** | — | All async |
|
||||
| Operation | Target | Measurement |
|
||||
| --------------------------------------- | ------ | ------------------------- |
|
||||
| Combo resolution | <10ms | For 50 targets |
|
||||
| Rate limit check | <1ms | In-memory token bucket |
|
||||
| Model family fallback | <5ms | Cached family definitions |
|
||||
| Request routing dispatch | <2ms | Hot path |
|
||||
| **No blocking I/O in routing hot path** | — | All async |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Playground Studio"
|
||||
version: 3.8.7
|
||||
lastUpdated: 2026-05-30
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Playground Studio
|
||||
@@ -82,14 +82,14 @@ Tools/function calling and structured output UI:
|
||||
|
||||
`StudioConfigPane.tsx` — always visible, collapsible.
|
||||
|
||||
| Field | Component | Notes |
|
||||
|-------|-----------|-------|
|
||||
| Endpoint | `<select>` | 10 options matching `PlaygroundEndpoint` |
|
||||
| Model | `<input>` | free text, e.g. `openai/gpt-4o` |
|
||||
| System prompt | `<textarea>` | fed into all tabs |
|
||||
| Parameters | `ParamSliders` | temperature, max_tokens, top_p, presence/frequency penalty, seed, stop |
|
||||
| Presets | `PresetPicker` | load/save named config snapshots (persisted in DB) |
|
||||
| Improve prompt | `ImprovePromptButton` | opens quota-warning modal, calls `/api/playground/improve-prompt` |
|
||||
| Field | Component | Notes |
|
||||
| -------------- | --------------------- | ---------------------------------------------------------------------- |
|
||||
| Endpoint | `<select>` | 10 options matching `PlaygroundEndpoint` |
|
||||
| Model | `<input>` | free text, e.g. `openai/gpt-4o` |
|
||||
| System prompt | `<textarea>` | fed into all tabs |
|
||||
| Parameters | `ParamSliders` | temperature, max_tokens, top_p, presence/frequency penalty, seed, stop |
|
||||
| Presets | `PresetPicker` | load/save named config snapshots (persisted in DB) |
|
||||
| Improve prompt | `ImprovePromptButton` | opens quota-warning modal, calls `/api/playground/improve-prompt` |
|
||||
|
||||
State is lifted to `PlaygroundStudio.tsx` and passed down to all tabs. Switching tabs
|
||||
preserves config state.
|
||||
@@ -149,14 +149,14 @@ from the current `PlaygroundState`. API key placeholder is always `$OMNIROUTE_AP
|
||||
|
||||
## Backend Routes
|
||||
|
||||
| Method | Path | Handler |
|
||||
|--------|------|---------|
|
||||
| `POST` | `/api/playground/improve-prompt` | Zod-validates `ImprovePromptRequestSchema`; calls `/v1/chat/completions` with meta-prompt |
|
||||
| `GET` | `/api/playground/presets` | Returns `{ presets: PlaygroundPresetListItem[] }` |
|
||||
| `POST` | `/api/playground/presets` | Creates preset; validates `PlaygroundPresetCreateSchema` |
|
||||
| `GET` | `/api/playground/presets/:id` | Returns one preset or 404 |
|
||||
| `PUT` | `/api/playground/presets/:id` | Partial update |
|
||||
| `DELETE` | `/api/playground/presets/:id` | 204 |
|
||||
| Method | Path | Handler |
|
||||
| -------- | -------------------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| `POST` | `/api/playground/improve-prompt` | Zod-validates `ImprovePromptRequestSchema`; calls `/v1/chat/completions` with meta-prompt |
|
||||
| `GET` | `/api/playground/presets` | Returns `{ presets: PlaygroundPresetListItem[] }` |
|
||||
| `POST` | `/api/playground/presets` | Creates preset; validates `PlaygroundPresetCreateSchema` |
|
||||
| `GET` | `/api/playground/presets/:id` | Returns one preset or 404 |
|
||||
| `PUT` | `/api/playground/presets/:id` | Partial update |
|
||||
| `DELETE` | `/api/playground/presets/:id` | 204 |
|
||||
|
||||
Auth: optional (`REQUIRE_API_KEY`). Errors via `buildErrorBody()` (Hard Rule #12).
|
||||
|
||||
@@ -164,42 +164,42 @@ Auth: optional (`REQUIRE_API_KEY`). Errors via `buildErrorBody()` (Hard Rule #12
|
||||
|
||||
## Key Files
|
||||
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `src/app/(dashboard)/dashboard/playground/PlaygroundStudio.tsx` | Shell component, tab orchestrator |
|
||||
| `src/app/(dashboard)/dashboard/playground/components/StudioTopBar.tsx` | Tabs + counter + export button |
|
||||
| `src/app/(dashboard)/dashboard/playground/components/StudioConfigPane.tsx` | Shared config panel |
|
||||
| `src/app/(dashboard)/dashboard/playground/components/tabs/ChatTab.tsx` | Chat workbench |
|
||||
| `src/app/(dashboard)/dashboard/playground/components/tabs/CompareTab.tsx` | Multi-model compare |
|
||||
| `src/app/(dashboard)/dashboard/playground/components/tabs/ApiTab.tsx` | Monaco editor (preserved) |
|
||||
| `src/app/(dashboard)/dashboard/playground/components/tabs/BuildTab.tsx` | Tools + structured output |
|
||||
| `src/app/(dashboard)/dashboard/playground/components/ExportCodeModal.tsx` | Code export modal |
|
||||
| `src/app/(dashboard)/dashboard/playground/components/CompareColumn.tsx` | Single compare column |
|
||||
| `src/app/(dashboard)/dashboard/playground/components/ProviderMetrics.tsx` | TTFT/TPS display |
|
||||
| `src/app/(dashboard)/dashboard/playground/hooks/useStreamMetrics.ts` | Client-side metric hook |
|
||||
| `src/app/(dashboard)/dashboard/playground/hooks/usePresets.ts` | Presets CRUD hook |
|
||||
| `src/app/(dashboard)/dashboard/playground/hooks/useImprovePrompt.ts` | Improve-prompt hook |
|
||||
| `src/lib/playground/codeExport.ts` | curl/Python/TS generator (shared with Search Tools) |
|
||||
| `src/lib/playground/promptImprover.ts` | Meta-prompt builder |
|
||||
| `src/lib/playground/streamMetrics.ts` | Pure metrics computation |
|
||||
| `src/lib/db/playgroundPresets.ts` | DB module (CRUD) |
|
||||
| `src/app/api/playground/improve-prompt/route.ts` | Improve-prompt REST route |
|
||||
| `src/app/api/playground/presets/route.ts` | Presets list + create |
|
||||
| `src/app/api/playground/presets/[id]/route.ts` | Presets get/update/delete |
|
||||
| `src/lib/db/migrations/084_playground_presets.sql` | DB migration |
|
||||
| Path | Purpose |
|
||||
| -------------------------------------------------------------------------- | --------------------------------------------------- |
|
||||
| `src/app/(dashboard)/dashboard/playground/PlaygroundStudio.tsx` | Shell component, tab orchestrator |
|
||||
| `src/app/(dashboard)/dashboard/playground/components/StudioTopBar.tsx` | Tabs + counter + export button |
|
||||
| `src/app/(dashboard)/dashboard/playground/components/StudioConfigPane.tsx` | Shared config panel |
|
||||
| `src/app/(dashboard)/dashboard/playground/components/tabs/ChatTab.tsx` | Chat workbench |
|
||||
| `src/app/(dashboard)/dashboard/playground/components/tabs/CompareTab.tsx` | Multi-model compare |
|
||||
| `src/app/(dashboard)/dashboard/playground/components/tabs/ApiTab.tsx` | Monaco editor (preserved) |
|
||||
| `src/app/(dashboard)/dashboard/playground/components/tabs/BuildTab.tsx` | Tools + structured output |
|
||||
| `src/app/(dashboard)/dashboard/playground/components/ExportCodeModal.tsx` | Code export modal |
|
||||
| `src/app/(dashboard)/dashboard/playground/components/CompareColumn.tsx` | Single compare column |
|
||||
| `src/app/(dashboard)/dashboard/playground/components/ProviderMetrics.tsx` | TTFT/TPS display |
|
||||
| `src/app/(dashboard)/dashboard/playground/hooks/useStreamMetrics.ts` | Client-side metric hook |
|
||||
| `src/app/(dashboard)/dashboard/playground/hooks/usePresets.ts` | Presets CRUD hook |
|
||||
| `src/app/(dashboard)/dashboard/playground/hooks/useImprovePrompt.ts` | Improve-prompt hook |
|
||||
| `src/lib/playground/codeExport.ts` | curl/Python/TS generator (shared with Search Tools) |
|
||||
| `src/lib/playground/promptImprover.ts` | Meta-prompt builder |
|
||||
| `src/lib/playground/streamMetrics.ts` | Pure metrics computation |
|
||||
| `src/lib/db/playgroundPresets.ts` | DB module (CRUD) |
|
||||
| `src/app/api/playground/improve-prompt/route.ts` | Improve-prompt REST route |
|
||||
| `src/app/api/playground/presets/route.ts` | Presets list + create |
|
||||
| `src/app/api/playground/presets/[id]/route.ts` | Presets get/update/delete |
|
||||
| `src/lib/db/migrations/084_playground_presets.sql` | DB migration |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| Monaco editor not rendering in API tab | SSR loaded Monaco | Verify `ApiTab` uses `dynamic(..., { ssr: false })` |
|
||||
| Compare streams fire sequentially | Wrong `Promise.all` usage | All stream starts must be dispatched in one `Promise.all` call |
|
||||
| Metrics show `null` TTFT | First chunk handler not wired | Check `useStreamMetrics.onFirstChunk()` is called in the SSE reader loop |
|
||||
| Preset not persisting | DB migration not run | Run `npm run db:migrate` or restart the server (migration auto-runs on startup) |
|
||||
| Improve prompt returns 502 | Model not set in Config | User must enter a model name in the Config pane before improving |
|
||||
| Export code shows `MISSING_API_KEY` | Placeholder not inserted | `codeExport.ts` always uses `API_KEY_PLACEHOLDER = "$OMNIROUTE_API_KEY"` |
|
||||
| Symptom | Cause | Fix |
|
||||
| -------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------- |
|
||||
| Monaco editor not rendering in API tab | SSR loaded Monaco | Verify `ApiTab` uses `dynamic(..., { ssr: false })` |
|
||||
| Compare streams fire sequentially | Wrong `Promise.all` usage | All stream starts must be dispatched in one `Promise.all` call |
|
||||
| Metrics show `null` TTFT | First chunk handler not wired | Check `useStreamMetrics.onFirstChunk()` is called in the SSE reader loop |
|
||||
| Preset not persisting | DB migration not run | Run `npm run db:migrate` or restart the server (migration auto-runs on startup) |
|
||||
| Improve prompt returns 502 | Model not set in Config | User must enter a model name in the Config pane before improving |
|
||||
| Export code shows `MISSING_API_KEY` | Placeholder not inserted | `codeExport.ts` always uses `API_KEY_PLACEHOLDER = "$OMNIROUTE_API_KEY"` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
---
|
||||
title: "OmniRoute CLI Plugin System"
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# OmniRoute CLI Plugin System
|
||||
|
||||
Extend the `omniroute` CLI without modifying its core. Plugins follow the `omniroute-cmd-*` naming convention, similar to `gh extension` or `kubectl plugin`.
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Plugin Marketplace"
|
||||
version: 3.8.24
|
||||
lastUpdated: 2026-06-13
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Plugin Marketplace
|
||||
@@ -9,7 +9,7 @@ lastUpdated: 2026-06-13
|
||||
> **Source of truth:** `src/lib/plugins/` (`marketplace.ts`, `manager.ts`, `manifest.ts`,
|
||||
> `scanner.ts`, `loader.ts`), `src/app/api/plugins/`, and
|
||||
> `src/app/(dashboard)/dashboard/plugins/`
|
||||
> **Last updated:** 2026-06-13 — v3.8.24
|
||||
> **Last updated:** 2026-06-28 — v3.8.40
|
||||
|
||||
OmniRoute ships a WordPress-style plugin system. Plugins are self-contained
|
||||
directories — each with a `plugin.json` manifest and an entry file — that hook
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
---
|
||||
title: "OmniRoute Plugin SDK"
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# OmniRoute Plugin SDK
|
||||
|
||||
## Quick Start
|
||||
@@ -28,6 +34,7 @@ export default definePlugin({
|
||||
Factory function that creates a Plugin object with defaults.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `name` (string, required) — Plugin name in kebab-case
|
||||
- `priority` (number, optional, default: 100) — Lower runs first
|
||||
- `enabled` (boolean, optional, default: true) — Start enabled?
|
||||
@@ -69,15 +76,15 @@ onRequest: (ctx) => {
|
||||
|
||||
## Plugin Context (`PluginContext`)
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `requestId` | `string` | Unique request identifier |
|
||||
| `model` | `string` | Requested model name |
|
||||
| `provider` | `string` | Target provider ID |
|
||||
| `body` | `Record<string, unknown>` | Request body |
|
||||
| `headers` | `Record<string, string>` | Request headers |
|
||||
| `metadata` | `Record<string, unknown>` | Mutable metadata |
|
||||
| `timestamp` | `number` | Request timestamp |
|
||||
| Field | Type | Description |
|
||||
| ----------- | ------------------------- | ------------------------- |
|
||||
| `requestId` | `string` | Unique request identifier |
|
||||
| `model` | `string` | Requested model name |
|
||||
| `provider` | `string` | Target provider ID |
|
||||
| `body` | `Record<string, unknown>` | Request body |
|
||||
| `headers` | `Record<string, string>` | Request headers |
|
||||
| `metadata` | `Record<string, unknown>` | Mutable metadata |
|
||||
| `timestamp` | `number` | Request timestamp |
|
||||
|
||||
## Manifest (`plugin.json`)
|
||||
|
||||
@@ -134,13 +141,13 @@ Or as simple booleans (default priority 100):
|
||||
|
||||
Plugins run in a sandboxed VM context. Access to external resources requires explicit permissions:
|
||||
|
||||
| Permission | Grants |
|
||||
|---|---|
|
||||
| `network` | `fetch`, `AbortController`, `Headers`, `Request`, `Response` |
|
||||
| `file-read` | `fs.readFile`, `fs.readdir`, `fs.stat` |
|
||||
| `file-write` | `fs.writeFile`, `fs.mkdir`, `fs.rm` |
|
||||
| `env` | Read-only `process.env` proxy |
|
||||
| `exec` | `child_process.exec`, `child_process.execSync` |
|
||||
| Permission | Grants |
|
||||
| ------------ | ------------------------------------------------------------ |
|
||||
| `network` | `fetch`, `AbortController`, `Headers`, `Request`, `Response` |
|
||||
| `file-read` | `fs.readFile`, `fs.readdir`, `fs.stat` |
|
||||
| `file-write` | `fs.writeFile`, `fs.mkdir`, `fs.rm` |
|
||||
| `env` | Read-only `process.env` proxy |
|
||||
| `exec` | `child_process.exec`, `child_process.execSync` |
|
||||
|
||||
Without a permission, the corresponding globals are simply not available in the sandbox.
|
||||
|
||||
@@ -167,22 +174,22 @@ Config values are persisted in the database and accessible via the dashboard con
|
||||
|
||||
## Built-in Events
|
||||
|
||||
| Event | When | Payload |
|
||||
|---|---|---|
|
||||
| `onRequest` | Before chat handler | Request context |
|
||||
| `onResponse` | After chat handler | Response data |
|
||||
| `onError` | On handler error | Error object |
|
||||
| `onModelSelect` | Model selected for routing | Model info |
|
||||
| `onComboResolve` | Combo routing resolved | Combo targets |
|
||||
| `onRateLimit` | Rate limit hit | Limit info |
|
||||
| `onQuotaExhaust` | Quota exhausted | Quota info |
|
||||
| `onProviderError` | Provider returned error | Error details |
|
||||
| `onStreamStart` | SSE stream started | Stream info |
|
||||
| `onStreamEnd` | SSE stream ended | Stream stats |
|
||||
| `onInstall` | Plugin installed | `{ name, version, manifest }` |
|
||||
| `onActivate` | Plugin activated | `{ name, version, manifest }` |
|
||||
| `onDeactivate` | Plugin deactivated | `{ name, version, manifest }` |
|
||||
| `onUninstall` | Plugin uninstalled (before files deleted) | `{ name, version, manifest }` |
|
||||
| Event | When | Payload |
|
||||
| ----------------- | ----------------------------------------- | ----------------------------- |
|
||||
| `onRequest` | Before chat handler | Request context |
|
||||
| `onResponse` | After chat handler | Response data |
|
||||
| `onError` | On handler error | Error object |
|
||||
| `onModelSelect` | Model selected for routing | Model info |
|
||||
| `onComboResolve` | Combo routing resolved | Combo targets |
|
||||
| `onRateLimit` | Rate limit hit | Limit info |
|
||||
| `onQuotaExhaust` | Quota exhausted | Quota info |
|
||||
| `onProviderError` | Provider returned error | Error details |
|
||||
| `onStreamStart` | SSE stream started | Stream info |
|
||||
| `onStreamEnd` | SSE stream ended | Stream stats |
|
||||
| `onInstall` | Plugin installed | `{ name, version, manifest }` |
|
||||
| `onActivate` | Plugin activated | `{ name, version, manifest }` |
|
||||
| `onDeactivate` | Plugin deactivated | `{ name, version, manifest }` |
|
||||
| `onUninstall` | Plugin uninstalled (before files deleted) | `{ name, version, manifest }` |
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -215,7 +222,7 @@ export default definePlugin({
|
||||
const window = 60000; // 1 minute
|
||||
const maxRequests = 100;
|
||||
|
||||
const timestamps = (requests.get(key) || []).filter(t => t > now - window);
|
||||
const timestamps = (requests.get(key) || []).filter((t) => t > now - window);
|
||||
timestamps.push(now);
|
||||
requests.set(key, timestamps);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Search Tools Studio"
|
||||
version: 3.8.7
|
||||
lastUpdated: 2026-05-30
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Search Tools Studio
|
||||
@@ -75,14 +75,14 @@ Runs the same query/URL across up to **4 providers in parallel** (D22):
|
||||
|
||||
`SearchToolsConfigPane.tsx` — always visible, collapsible.
|
||||
|
||||
| Field | Notes |
|
||||
|-------|-------|
|
||||
| Provider | Dropdown with status badge (configured / missing / rate limited) |
|
||||
| Type | `web` or `news` (search only) |
|
||||
| Full page | Toggle for scrape — fetches entire page vs first visible content |
|
||||
| Format | `markdown`, `text`, or `html` (scrape only) |
|
||||
| Rerank model | Optional model for post-search reranking |
|
||||
| History | Collapsible search history section |
|
||||
| Field | Notes |
|
||||
| ------------ | ---------------------------------------------------------------- |
|
||||
| Provider | Dropdown with status badge (configured / missing / rate limited) |
|
||||
| Type | `web` or `news` (search only) |
|
||||
| Full page | Toggle for scrape — fetches entire page vs first visible content |
|
||||
| Format | `markdown`, `text`, or `html` (scrape only) |
|
||||
| Rerank model | Optional model for post-search reranking |
|
||||
| History | Collapsible search history section |
|
||||
|
||||
---
|
||||
|
||||
@@ -90,13 +90,13 @@ Runs the same query/URL across up to **4 providers in parallel** (D22):
|
||||
|
||||
`SearchConceptCard.tsx` — always visible, collapsible accordion. Explains:
|
||||
|
||||
| Concept | One-liner |
|
||||
|---------|-----------|
|
||||
| **Search** | Fetches a list of web results (title, URL, snippet, relevance score) |
|
||||
| **Scrape** | Extracts the full content of a URL (markdown, text or HTML) |
|
||||
| **Compare** | Runs the same query in N providers side-by-side |
|
||||
| **Rerank** | Reorders results via LLM to improve query relevance |
|
||||
| **Auto (cheapest)** | Picks the cheapest available provider automatically |
|
||||
| Concept | One-liner |
|
||||
| ------------------- | -------------------------------------------------------------------- |
|
||||
| **Search** | Fetches a list of web results (title, URL, snippet, relevance score) |
|
||||
| **Scrape** | Extracts the full content of a URL (markdown, text or HTML) |
|
||||
| **Compare** | Runs the same query in N providers side-by-side |
|
||||
| **Rerank** | Reorders results via LLM to improve query relevance |
|
||||
| **Auto (cheapest)** | Picks the cheapest available provider automatically |
|
||||
|
||||
---
|
||||
|
||||
@@ -105,15 +105,15 @@ Runs the same query/URL across up to **4 providers in parallel** (D22):
|
||||
`ProviderCatalog.tsx` exposes the full provider list from `GET /api/search/providers`
|
||||
(extended in F4 to include fetch providers):
|
||||
|
||||
| Field | Source |
|
||||
|-------|--------|
|
||||
| `id`, `name` | `searchRegistry.ts` |
|
||||
| `kind` | `"search"` (12 providers) or `"fetch"` (firecrawl, jina-reader, tavily-search) |
|
||||
| `costPerQuery` | Registry data |
|
||||
| `freeMonthlyQuota` | Registry data |
|
||||
| `searchTypes` / `fetchFormats` | Registry data |
|
||||
| `status` | `"configured"` / `"missing"` / `"rate_limited"` — derived at runtime from credential store |
|
||||
| `configureHref` | `/dashboard/providers` |
|
||||
| Field | Source |
|
||||
| ------------------------------ | ------------------------------------------------------------------------------------------ |
|
||||
| `id`, `name` | `searchRegistry.ts` |
|
||||
| `kind` | `"search"` (12 providers) or `"fetch"` (firecrawl, jina-reader, tavily-search) |
|
||||
| `costPerQuery` | Registry data |
|
||||
| `freeMonthlyQuota` | Registry data |
|
||||
| `searchTypes` / `fetchFormats` | Registry data |
|
||||
| `status` | `"configured"` / `"missing"` / `"rate_limited"` — derived at runtime from credential store |
|
||||
| `configureHref` | `/dashboard/providers` |
|
||||
|
||||
The status is **derived at request time** by checking whether credentials exist and whether
|
||||
all keys are currently in cooldown.
|
||||
@@ -145,32 +145,32 @@ Only one backend change was needed for this feature:
|
||||
|
||||
## Key Files
|
||||
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `src/app/(dashboard)/dashboard/search-tools/SearchToolsClient.tsx` | Studio shell, tab orchestrator |
|
||||
| `src/app/(dashboard)/dashboard/search-tools/components/SearchToolsTopBar.tsx` | Tabs + metrics + export button |
|
||||
| `src/app/(dashboard)/dashboard/search-tools/components/SearchToolsConfigPane.tsx` | Shared config panel |
|
||||
| `src/app/(dashboard)/dashboard/search-tools/components/SearchConceptCard.tsx` | Explainer cards (always visible) |
|
||||
| `src/app/(dashboard)/dashboard/search-tools/components/ProviderCatalog.tsx` | Provider list with metadata |
|
||||
| `src/app/(dashboard)/dashboard/search-tools/components/ScrapeResult.tsx` | Markdown preview + raw toggle |
|
||||
| `src/app/(dashboard)/dashboard/search-tools/components/tabs/SearchTab.tsx` | Search + rerank tab |
|
||||
| `src/app/(dashboard)/dashboard/search-tools/components/tabs/ScrapeTab.tsx` | Scrape tab |
|
||||
| `src/app/(dashboard)/dashboard/search-tools/components/tabs/CompareTab.tsx` | Multi-provider compare tab |
|
||||
| `src/app/(dashboard)/dashboard/search-tools/hooks/useScrapeFetch.ts` | Scrape fetch hook |
|
||||
| `src/app/api/search/providers/route.ts` | Extended with `kind` + `status` + fetch providers |
|
||||
| `open-sse/config/searchRegistry.ts` | Source of truth for search provider metadata |
|
||||
| Path | Purpose |
|
||||
| --------------------------------------------------------------------------------- | ------------------------------------------------- |
|
||||
| `src/app/(dashboard)/dashboard/search-tools/SearchToolsClient.tsx` | Studio shell, tab orchestrator |
|
||||
| `src/app/(dashboard)/dashboard/search-tools/components/SearchToolsTopBar.tsx` | Tabs + metrics + export button |
|
||||
| `src/app/(dashboard)/dashboard/search-tools/components/SearchToolsConfigPane.tsx` | Shared config panel |
|
||||
| `src/app/(dashboard)/dashboard/search-tools/components/SearchConceptCard.tsx` | Explainer cards (always visible) |
|
||||
| `src/app/(dashboard)/dashboard/search-tools/components/ProviderCatalog.tsx` | Provider list with metadata |
|
||||
| `src/app/(dashboard)/dashboard/search-tools/components/ScrapeResult.tsx` | Markdown preview + raw toggle |
|
||||
| `src/app/(dashboard)/dashboard/search-tools/components/tabs/SearchTab.tsx` | Search + rerank tab |
|
||||
| `src/app/(dashboard)/dashboard/search-tools/components/tabs/ScrapeTab.tsx` | Scrape tab |
|
||||
| `src/app/(dashboard)/dashboard/search-tools/components/tabs/CompareTab.tsx` | Multi-provider compare tab |
|
||||
| `src/app/(dashboard)/dashboard/search-tools/hooks/useScrapeFetch.ts` | Scrape fetch hook |
|
||||
| `src/app/api/search/providers/route.ts` | Extended with `kind` + `status` + fetch providers |
|
||||
| `open-sse/config/searchRegistry.ts` | Source of truth for search provider metadata |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| Scrape tab shows "endpoint not available" | `/v1/web/fetch` not wired | Verify plan 05 is merged; check `src/app/api/v1/web/fetch/route.ts` exists |
|
||||
| Provider catalog shows all as "missing" | Credentials not configured | Add credentials in `/dashboard/providers` |
|
||||
| Scrape content is truncated | Response > 256 KB cap | Expected behavior (D21). Use "view raw" button for full content |
|
||||
| Compare tab only shows 2 providers | Rate limit active | Two or more providers may be in cooldown — check provider status in Config pane |
|
||||
| "Size" shown as raw key in table | Missing i18n key | Verify `search.size` exists in the locale file; rebuild i18n |
|
||||
| Symptom | Cause | Fix |
|
||||
| ----------------------------------------- | -------------------------- | ------------------------------------------------------------------------------- |
|
||||
| Scrape tab shows "endpoint not available" | `/v1/web/fetch` not wired | Verify plan 05 is merged; check `src/app/api/v1/web/fetch/route.ts` exists |
|
||||
| Provider catalog shows all as "missing" | Credentials not configured | Add credentials in `/dashboard/providers` |
|
||||
| Scrape content is truncated | Response > 256 KB cap | Expected behavior (D21). Use "view raw" button for full content |
|
||||
| Compare tab only shows 2 providers | Rate limit active | Two or more providers may be in cooldown — check provider status in Config pane |
|
||||
| "Size" shown as raw key in table | Missing i18n key | Verify `search.size` exists in the locale file; rebuild i18n |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
title: "Skills Framework"
|
||||
version: 3.8.6
|
||||
lastUpdated: 2026-05-28
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Skills Framework
|
||||
|
||||
> **Source of truth:** `src/lib/skills/` and `src/app/api/skills/`
|
||||
> **Last updated:** 2026-05-28 — v3.8.6
|
||||
> **Last updated:** 2026-06-28 — v3.8.40
|
||||
|
||||
OmniRoute exposes an extensible Skills framework that lets language models (and operators) compose reusable capabilities — from filesystem reads and HTTP requests to sandboxed code execution and curated marketplace skills.
|
||||
|
||||
@@ -19,19 +19,19 @@ A skill is a versioned, schema-defined unit of work. OmniRoute can inject skills
|
||||
|
||||
OmniRoute has two distinct but complementary skill systems:
|
||||
|
||||
| Dimension | **Omni Skills** (this doc) | **Agent Skills** |
|
||||
| :--- | :--- | :--- |
|
||||
| Purpose | LLM tool injection + sandboxed execution | SKILL.md catalog for external agents to discover and consume |
|
||||
| Source of truth | `src/lib/skills/` + marketplace | `src/lib/agentSkills/` + `skills/` directory |
|
||||
| Runtime mode | Injected into outbound requests, executed on tool-call events | Static markdown catalog + REST/MCP/A2A discovery endpoints |
|
||||
| Who uses it | OmniRoute itself (combo routing, inbound LLM calls) | External agents, MCP clients, A2A orchestrators |
|
||||
| Count | Variable (marketplace-driven) | 42 canonical entries (22 API + 20 CLI) |
|
||||
| Format | `SkillDefinition` with tool schema + handler | `SKILL.md` frontmatter + markdown body |
|
||||
| Discovery | `/api/skills/*` REST + `omniroute_skills_*` MCP tools | `/api/agent-skills/*` REST + `omniroute_agent_skills_*` MCP tools + A2A `list-capabilities` |
|
||||
| Dimension | **Omni Skills** (this doc) | **Agent Skills** |
|
||||
| :-------------- | :------------------------------------------------------------ | :------------------------------------------------------------------------------------------ |
|
||||
| Purpose | LLM tool injection + sandboxed execution | SKILL.md catalog for external agents to discover and consume |
|
||||
| Source of truth | `src/lib/skills/` + marketplace | `src/lib/agentSkills/` + `skills/` directory |
|
||||
| Runtime mode | Injected into outbound requests, executed on tool-call events | Static markdown catalog + REST/MCP/A2A discovery endpoints |
|
||||
| Who uses it | OmniRoute itself (combo routing, inbound LLM calls) | External agents, MCP clients, A2A orchestrators |
|
||||
| Count | Variable (marketplace-driven) | 42 canonical entries (22 API + 20 CLI) |
|
||||
| Format | `SkillDefinition` with tool schema + handler | `SKILL.md` frontmatter + markdown body |
|
||||
| Discovery | `/api/skills/*` REST + `omniroute_skills_*` MCP tools | `/api/agent-skills/*` REST + `omniroute_agent_skills_*` MCP tools + A2A `list-capabilities` |
|
||||
|
||||
**Omni Skills** are the execution engine — they define what OmniRoute *can do* when an LLM invokes a tool.
|
||||
**Omni Skills** are the execution engine — they define what OmniRoute _can do_ when an LLM invokes a tool.
|
||||
|
||||
**Agent Skills** are the documentation catalog — they explain to external agents *how to use* OmniRoute's REST API and CLI, with structured SKILL.md files that can be fed directly into agent prompts.
|
||||
**Agent Skills** are the documentation catalog — they explain to external agents _how to use_ OmniRoute's REST API and CLI, with structured SKILL.md files that can be fed directly into agent prompts.
|
||||
|
||||
For the Agent Skills catalog, generator, MCP tools, and A2A skill, see [docs/frameworks/AGENT-SKILLS.md](./AGENT-SKILLS.md).
|
||||
|
||||
@@ -332,10 +332,10 @@ The `SkillExecutor` (`src/lib/skills/executor.ts`) is a **singleton** that manag
|
||||
|
||||
### Default Configuration
|
||||
|
||||
| Setting | Default | Configurable via |
|
||||
|---------|---------|------------------|
|
||||
| `timeout` | `30000` (30s) | `skillExecutor.setTimeout(ms)` |
|
||||
| `maxRetries` | `3` | `skillExecutor.setMaxRetries(count)` |
|
||||
| Setting | Default | Configurable via |
|
||||
| ------------ | ------------- | ------------------------------------ |
|
||||
| `timeout` | `30000` (30s) | `skillExecutor.setTimeout(ms)` |
|
||||
| `maxRetries` | `3` | `skillExecutor.setMaxRetries(count)` |
|
||||
|
||||
> **Important**: The executor is a singleton — calling `setTimeout()` affects all subsequent invocations globally. Per-skill timeouts are not currently supported; if you need different timeouts per skill, submit separate processes or fork the executor.
|
||||
|
||||
@@ -345,11 +345,11 @@ From `src/lib/skills/types.ts`:
|
||||
|
||||
```ts
|
||||
enum SkillStatus {
|
||||
PENDING = "pending", // Queued, not yet started
|
||||
RUNNING = "running", // Handler invoked
|
||||
SUCCESS = "success", // Handler returned valid output
|
||||
ERROR = "error", // Handler threw an exception
|
||||
TIMEOUT = "timeout", // Exceeded the executor's timeout
|
||||
PENDING = "pending", // Queued, not yet started
|
||||
RUNNING = "running", // Handler invoked
|
||||
SUCCESS = "success", // Handler returned valid output
|
||||
ERROR = "error", // Handler threw an exception
|
||||
TIMEOUT = "timeout", // Exceeded the executor's timeout
|
||||
}
|
||||
```
|
||||
|
||||
@@ -413,9 +413,9 @@ The `SkillMode` enum (`src/lib/skills/types.ts`) controls **when and how** skill
|
||||
|
||||
```ts
|
||||
enum SkillMode {
|
||||
AUTO = "auto", // LLM decides when to call the skill
|
||||
MANUAL = "manual", // Only invoked by explicit user request
|
||||
HYBRID = "hybrid", // AUTO scoring + manual override
|
||||
AUTO = "auto", // LLM decides when to call the skill
|
||||
MANUAL = "manual", // Only invoked by explicit user request
|
||||
HYBRID = "hybrid", // AUTO scoring + manual override
|
||||
}
|
||||
```
|
||||
|
||||
@@ -423,11 +423,11 @@ enum SkillMode {
|
||||
|
||||
### When to Use Each Mode
|
||||
|
||||
| Mode | LLM behavior | Use case |
|
||||
|------|--------------|----------|
|
||||
| `AUTO` | LLM can call the skill when it deems necessary | General-purpose skills (file reads, HTTP requests) |
|
||||
| `MANUAL` | LLM cannot call the skill; only an explicit `executeSkill` API call invokes it | Sensitive operations (database writes, payments) |
|
||||
| `HYBRID` | LLM can suggest the skill; user must confirm | Skills that have side effects but aren't dangerous |
|
||||
| Mode | LLM behavior | Use case |
|
||||
| -------- | ------------------------------------------------------------------------------ | -------------------------------------------------- |
|
||||
| `AUTO` | LLM can call the skill when it deems necessary | General-purpose skills (file reads, HTTP requests) |
|
||||
| `MANUAL` | LLM cannot call the skill; only an explicit `executeSkill` API call invokes it | Sensitive operations (database writes, payments) |
|
||||
| `HYBRID` | LLM can suggest the skill; user must confirm | Skills that have side effects but aren't dangerous |
|
||||
|
||||
### AUTO Scoring
|
||||
|
||||
@@ -454,25 +454,26 @@ The browser skill (`src/lib/skills/builtin/browser.ts`) provides headless browse
|
||||
// Enable in your config
|
||||
const config: SkillConfig = {
|
||||
enabled: true,
|
||||
mode: SkillMode.MANUAL, // Always require explicit invocation
|
||||
mode: SkillMode.MANUAL, // Always require explicit invocation
|
||||
allowedSkills: ["browser"],
|
||||
timeout: 60000, // 60s for page loads
|
||||
timeout: 60000, // 60s for page loads
|
||||
maxRetries: 1,
|
||||
};
|
||||
```
|
||||
|
||||
### Other Built-in Categories
|
||||
|
||||
| Category | Skills | Mode |
|
||||
|----------|--------|------|
|
||||
| File I/O | `file_read`, `file_write` | AUTO |
|
||||
| HTTP | `http_request` | AUTO |
|
||||
| Search | `web_search` | AUTO |
|
||||
| Code Exec | `eval_code` (sandboxed JavaScript/Python) | HYBRID |
|
||||
| System | `execute_command` (sandboxed CLI execution) | MANUAL |
|
||||
| Category | Skills | Mode |
|
||||
| --------- | ------------------------------------------- | ------ |
|
||||
| File I/O | `file_read`, `file_write` | AUTO |
|
||||
| HTTP | `http_request` | AUTO |
|
||||
| Search | `web_search` | AUTO |
|
||||
| Code Exec | `eval_code` (sandboxed JavaScript/Python) | HYBRID |
|
||||
| System | `execute_command` (sandboxed CLI execution) | MANUAL |
|
||||
|
||||
### Adding a Custom Skill
|
||||
|
||||
See the [Plugin SDK & Skills Integration](../plugins/PLUGIN_SDK.md) for how to add a custom skill via the plugin system.
|
||||
See the [Plugin SDK & Skills Integration](./PLUGIN_SDK.md) for how to add a custom skill via the plugin system.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Traffic Inspector"
|
||||
version: 3.8.31
|
||||
lastUpdated: 2026-06-20
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Traffic Inspector
|
||||
@@ -18,21 +18,21 @@ Traffic Inspector is OmniRoute's built-in HTTPS traffic debugger — a Charles P
|
||||
|
||||
### What makes Traffic Inspector unique
|
||||
|
||||
| Feature | mitmweb | Charles | Fiddler | **OmniRoute Traffic Inspector** |
|
||||
|---------|:-------:|:-------:|:-------:|:-------------------------------:|
|
||||
| Web-based | ✓ | ✗ | ✗ | ✓ |
|
||||
| Open-source | ✓ | ✗ | partial | ✓ |
|
||||
| **Agent-aware** (knows if request is from Antigravity/Copilot/etc.) | ✗ | ✗ | ✗ | ✓ |
|
||||
| **LLM-aware** (parses OpenAI/Anthropic/Gemini shape, tokens, model) | ✗ | ✗ | ✗ | ✓ |
|
||||
| **Model mapping visible** (gemini-3-flash → claude-sonnet-4.7) | ✗ | ✗ | ✗ | ✓ |
|
||||
| **Proxy/upstream latency split** | partial | ✗ | ✗ | ✓ |
|
||||
| **Integrated with OmniRoute** routing, fallback, cost | ✗ | ✗ | ✗ | ✓ |
|
||||
| **System-wide proxy debug** (any app on the machine) | ✓ | ✓ | ✓ | ✓ |
|
||||
| **Custom host capture** (per-host DNS redirect) | ✓ | ✓ | ✓ | ✓ |
|
||||
| **HTTP_PROXY env mode** | ✓ | ✓ | ✓ | ✓ |
|
||||
| **Conversation view** (multi-turn bubbles, tool_use/tool_result) | ✗ | ✗ | ✗ | ✓ |
|
||||
| **SSE stream merger** (reconstruct from delta events) | ✗ | ✗ | ✗ | ✓ |
|
||||
| **Session recording** (named, exportable .har/.jsonl) | ✗ | ✓ | ✓ | ✓ |
|
||||
| Feature | mitmweb | Charles | Fiddler | **OmniRoute Traffic Inspector** |
|
||||
| ------------------------------------------------------------------- | :-----: | :-----: | :-----: | :-----------------------------: |
|
||||
| Web-based | ✓ | ✗ | ✗ | ✓ |
|
||||
| Open-source | ✓ | ✗ | partial | ✓ |
|
||||
| **Agent-aware** (knows if request is from Antigravity/Copilot/etc.) | ✗ | ✗ | ✗ | ✓ |
|
||||
| **LLM-aware** (parses OpenAI/Anthropic/Gemini shape, tokens, model) | ✗ | ✗ | ✗ | ✓ |
|
||||
| **Model mapping visible** (gemini-3-flash → claude-sonnet-4.7) | ✗ | ✗ | ✗ | ✓ |
|
||||
| **Proxy/upstream latency split** | partial | ✗ | ✗ | ✓ |
|
||||
| **Integrated with OmniRoute** routing, fallback, cost | ✗ | ✗ | ✗ | ✓ |
|
||||
| **System-wide proxy debug** (any app on the machine) | ✓ | ✓ | ✓ | ✓ |
|
||||
| **Custom host capture** (per-host DNS redirect) | ✓ | ✓ | ✓ | ✓ |
|
||||
| **HTTP_PROXY env mode** | ✓ | ✓ | ✓ | ✓ |
|
||||
| **Conversation view** (multi-turn bubbles, tool_use/tool_result) | ✗ | ✗ | ✗ | ✓ |
|
||||
| **SSE stream merger** (reconstruct from delta events) | ✗ | ✗ | ✗ | ✓ |
|
||||
| **Session recording** (named, exportable .har/.jsonl) | ✗ | ✓ | ✓ | ✓ |
|
||||
|
||||
### Architecture in one paragraph
|
||||
|
||||
@@ -59,6 +59,7 @@ Traffic Inspector supports **5 simultaneous capture sources**. Each is independe
|
||||
**Note:** `source` = `"custom-host"`
|
||||
|
||||
Example use cases:
|
||||
|
||||
- Monitor `api.openai.com` from Python scripts
|
||||
- Debug `my-internal-llm.company.com`
|
||||
- Capture traffic from mobile devices on the same network (via ARP spoofing — advanced)
|
||||
@@ -87,13 +88,15 @@ export HTTPS_PROXY=http://127.0.0.1:8080
|
||||
|
||||
**Source:** OS-level proxy settings (applies to all apps on the machine)
|
||||
**Mechanism:** Uses OS APIs to redirect all HTTP/HTTPS traffic through the HTTP_PROXY listener:
|
||||
|
||||
- **macOS:** `networksetup -setwebproxy / -setsecurewebproxy`
|
||||
- **Linux:** `gsettings set org.gnome.system.proxy` + `/etc/environment`
|
||||
- **Windows:** `netsh winhttp set proxy 127.0.0.1:8080`
|
||||
**Reach:** Every application on the machine that respects system proxy settings
|
||||
**Note:** `source` = `"system-proxy"`
|
||||
**Reach:** Every application on the machine that respects system proxy settings
|
||||
**Note:** `source` = `"system-proxy"`
|
||||
|
||||
**Safety mechanisms:**
|
||||
|
||||
- Auto-disable timer (default 30 min, configurable via `INSPECTOR_SYSTEM_PROXY_GUARD_MINUTES`)
|
||||
- Previous system proxy state is saved in DB and restored on revert
|
||||
- Dashboard shows "Reverting system proxy" prompt if user navigates away while active
|
||||
@@ -112,12 +115,12 @@ This is a substantial subsystem with its own dedicated operator guide — see **
|
||||
|
||||
### Capture mode comparison
|
||||
|
||||
| Mode | Setup | Sudo? | Reach | Notes |
|
||||
|------|-------|:-----:|-------|-------|
|
||||
| 1. AgentBridge | Automatic | Once (cert+hosts) | 9 IDE agents | Default on |
|
||||
| 2. Custom Hosts | Per-host input | Yes (hosts file) | Any app using that host | Persisted in DB |
|
||||
| 3. HTTP_PROXY | `export HTTPS_PROXY=...` | No | Apps respecting env | Port 8080, no TLS decrypt by default |
|
||||
| 4. System-wide | Toggle + confirm | Yes | All apps on machine | Auto-disable in 30 min |
|
||||
| Mode | Setup | Sudo? | Reach | Notes |
|
||||
| ----------------- | ----------------------------- | :---------------------: | --------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| 1. AgentBridge | Automatic | Once (cert+hosts) | 9 IDE agents | Default on |
|
||||
| 2. Custom Hosts | Per-host input | Yes (hosts file) | Any app using that host | Persisted in DB |
|
||||
| 3. HTTP_PROXY | `export HTTPS_PROXY=...` | No | Apps respecting env | Port 8080, no TLS decrypt by default |
|
||||
| 4. System-wide | Toggle + confirm | Yes | All apps on machine | Auto-disable in 30 min |
|
||||
| 5. TPROXY decrypt | Toggle (Linux + native addon) | Yes (root + CA install) | Any host on the target port | Decrypts arbitrary hosts; off by default — see [MITM-TPROXY-DECRYPT.md](../security/MITM-TPROXY-DECRYPT.md) |
|
||||
|
||||
---
|
||||
@@ -156,30 +159,30 @@ This is a substantial subsystem with its own dedicated operator guide — see **
|
||||
|
||||
### 3.3 Detail pane — 7 tabs
|
||||
|
||||
| Tab | Content | Notes |
|
||||
|-----|---------|-------|
|
||||
| **Conversation** | Multi-turn chat bubbles (system/user/assistant + tool_use/tool_result) | Normalized from any provider format; only shown for `detectedKind === "llm"` |
|
||||
| **Headers** | Request + response header tables | Sensitive headers (Authorization, Cookie, api-key) masked by default; "Show secrets" toggle |
|
||||
| **Request** | Raw body, JSON tree view, model field badge | Pretty-printed JSON or raw text |
|
||||
| **Response** | Raw body or SSE event list; toggle "Raw ↔ Merged" | SSE merger reconstructs final message from delta events |
|
||||
| **Timing** | Waterfall: proxy overhead vs upstream latency | Total, TTFB, and size |
|
||||
| **LLM Details** | Provider, model, messages count, tokens in/out, cost estimate, mapped target | Only shown for LLM requests |
|
||||
| **Stats** | Recharts: latency timeline, token bar chart, tool call scatter | Only shown when a recorded session is loaded |
|
||||
| Tab | Content | Notes |
|
||||
| ---------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
||||
| **Conversation** | Multi-turn chat bubbles (system/user/assistant + tool_use/tool_result) | Normalized from any provider format; only shown for `detectedKind === "llm"` |
|
||||
| **Headers** | Request + response header tables | Sensitive headers (Authorization, Cookie, api-key) masked by default; "Show secrets" toggle |
|
||||
| **Request** | Raw body, JSON tree view, model field badge | Pretty-printed JSON or raw text |
|
||||
| **Response** | Raw body or SSE event list; toggle "Raw ↔ Merged" | SSE merger reconstructs final message from delta events |
|
||||
| **Timing** | Waterfall: proxy overhead vs upstream latency | Total, TTFB, and size |
|
||||
| **LLM Details** | Provider, model, messages count, tokens in/out, cost estimate, mapped target | Only shown for LLM requests |
|
||||
| **Stats** | Recharts: latency timeline, token bar chart, tool call scatter | Only shown when a recorded session is loaded |
|
||||
|
||||
### 3.4 Toolbar controls
|
||||
|
||||
| Control | Action |
|
||||
|---------|--------|
|
||||
| ⎉ Pause | Stops rendering new requests; "X new" badge accumulates |
|
||||
| 🗑 Clear | Clears the UI list (server buffer is not affected) |
|
||||
| ⬇ Export .har | Downloads current filtered list as HAR file |
|
||||
| ● Record session | Starts a named recording session |
|
||||
| Profile selector | LLM only / Custom hosts / All |
|
||||
| Host filter | Substring match on `host` field |
|
||||
| Agent filter | Dropdown: All / per-agent |
|
||||
| Status filter | All / 2xx / 3xx / 4xx / 5xx / error |
|
||||
| Source filter | All / agent-bridge / custom-host / http-proxy / system-proxy / tproxy |
|
||||
| **Live** filter | Show only in-flight (open) requests — `liveOnly` toggle (see §4.6) |
|
||||
| Control | Action |
|
||||
| ---------------- | --------------------------------------------------------------------- |
|
||||
| ⎉ Pause | Stops rendering new requests; "X new" badge accumulates |
|
||||
| 🗑 Clear | Clears the UI list (server buffer is not affected) |
|
||||
| ⬇ Export .har | Downloads current filtered list as HAR file |
|
||||
| ● Record session | Starts a named recording session |
|
||||
| Profile selector | LLM only / Custom hosts / All |
|
||||
| Host filter | Substring match on `host` field |
|
||||
| Agent filter | Dropdown: All / per-agent |
|
||||
| Status filter | All / 2xx / 3xx / 4xx / 5xx / error |
|
||||
| Source filter | All / agent-bridge / custom-host / http-proxy / system-proxy / tproxy |
|
||||
| **Live** filter | Show only in-flight (open) requests — `liveOnly` toggle (see §4.6) |
|
||||
|
||||
### 3.5 Resizable panels
|
||||
|
||||
@@ -223,9 +226,9 @@ Converts OpenAI, Anthropic, and Gemini message formats to a single `NormalizedCo
|
||||
|
||||
```ts
|
||||
interface NormalizedConversation {
|
||||
request: NormalizedTurn[]; // messages / contents / prompt from request body
|
||||
response: NormalizedTurn[]; // assistant response (merged via sseMerger)
|
||||
contextKey: string | null; // SHA-256 system-prompt fingerprint
|
||||
request: NormalizedTurn[]; // messages / contents / prompt from request body
|
||||
response: NormalizedTurn[]; // assistant response (merged via sseMerger)
|
||||
contextKey: string | null; // SHA-256 system-prompt fingerprint
|
||||
}
|
||||
```
|
||||
|
||||
@@ -246,14 +249,14 @@ For LLM requests, the LLM Details tab extracts:
|
||||
|
||||
```ts
|
||||
interface LlmMetadata {
|
||||
provider: string | null; // "openai" | "anthropic" | "gemini" | ...
|
||||
apiKind: string | null; // "chat.completions" | "messages" | "embeddings" | ...
|
||||
model: string | null; // from request body or response
|
||||
messages: number; // turn count
|
||||
tokensIn: number | null; // usage.prompt_tokens / usage.input_tokens
|
||||
tokensOut: number | null; // usage.completion_tokens / usage.output_tokens
|
||||
streamed: boolean; // true if SSE response
|
||||
mappedTo: string | null; // x-omniroute-mapped header
|
||||
provider: string | null; // "openai" | "anthropic" | "gemini" | ...
|
||||
apiKind: string | null; // "chat.completions" | "messages" | "embeddings" | ...
|
||||
model: string | null; // from request body or response
|
||||
messages: number; // turn count
|
||||
tokensIn: number | null; // usage.prompt_tokens / usage.input_tokens
|
||||
tokensOut: number | null; // usage.completion_tokens / usage.output_tokens
|
||||
streamed: boolean; // true if SSE response
|
||||
mappedTo: string | null; // x-omniroute-mapped header
|
||||
costEstimateUsd: number | null; // estimated cost based on OmniRoute pricing
|
||||
}
|
||||
```
|
||||
@@ -286,7 +289,7 @@ pid?: number; // originating process id (Linux only)
|
||||
processName?: string; // originating process name (Linux only)
|
||||
```
|
||||
|
||||
`src/mitm/inspector/processAttribution.ts` maps the connection's *client*
|
||||
`src/mitm/inspector/processAttribution.ts` maps the connection's _client_
|
||||
ephemeral port to a PID + name by:
|
||||
|
||||
1. Reading `/proc/net/tcp` and `/proc/net/tcp6` to find the socket inode for the
|
||||
@@ -312,6 +315,7 @@ support is a follow-up).
|
||||
### 5.2 Viewing a recorded session
|
||||
|
||||
The **Sessions** dropdown in the toolbar lists saved sessions. Selecting one:
|
||||
|
||||
- Loads the session's snapshot (frozen state)
|
||||
- A banner shows: `Viewing recorded session "<name>" — [Back to live]`
|
||||
- The Stats tab becomes available with Recharts aggregates
|
||||
@@ -320,10 +324,10 @@ The **Sessions** dropdown in the toolbar lists saved sessions. Selecting one:
|
||||
|
||||
Each session can be exported as:
|
||||
|
||||
| Format | Use |
|
||||
|--------|-----|
|
||||
| Format | Use |
|
||||
| -------------------------- | ------------------------------------------------------------------------------- |
|
||||
| **HAR** (HTTP Archive 1.2) | Compatible with Chrome DevTools, Charles, Fiddler — import for offline analysis |
|
||||
| **JSONL** | One `InterceptedRequest` per line — compatible with `llm-interceptor` format |
|
||||
| **JSONL** | One `InterceptedRequest` per line — compatible with `llm-interceptor` format |
|
||||
|
||||
Export via `GET /api/tools/traffic-inspector/sessions/{id}/export.har` or the ⬇ button in the Sessions dropdown.
|
||||
|
||||
@@ -333,20 +337,20 @@ Export via `GET /api/tools/traffic-inspector/sessions/{id}/export.har` or the
|
||||
|
||||
Traffic Inspector shows **all intercepted HTTPS traffic**, including authorization headers and request bodies. The following controls are in place:
|
||||
|
||||
| Control | Details |
|
||||
|---------|---------|
|
||||
| **LOCAL_ONLY** | All routes and the WebSocket endpoint are loopback-only (enforced in `routeGuard.ts` before auth) |
|
||||
| **Secret masking** | `maskSecrets()` applied to all headers and bodies before `TrafficBuffer.push()` — enabled by default (`INSPECTOR_MASK_SECRETS=true`) |
|
||||
| **Body size cap** | Bodies > `INSPECTOR_MAX_BODY_KB` (default 1024 KB) are truncated with `"(truncated for performance)"` notice |
|
||||
| **Sensitive header masking** | `authorization`, `cookie`, `api-key`, `x-api-key`, `proxy-authorization` → `Bearer ***` in Headers tab; "Show secrets" toggle |
|
||||
| **CSP** | Strict Content Security Policy on Traffic Inspector pages to prevent XSS via injected response bodies |
|
||||
| **No persistence by default** | The `TrafficBuffer` is in-memory and lost on server restart. Sessions are persisted only when explicitly recorded |
|
||||
| Control | Details |
|
||||
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **LOCAL_ONLY** | All routes and the WebSocket endpoint are loopback-only (enforced in `routeGuard.ts` before auth) |
|
||||
| **Secret masking** | `maskSecrets()` applied to all headers and bodies before `TrafficBuffer.push()` — enabled by default (`INSPECTOR_MASK_SECRETS=true`) |
|
||||
| **Body size cap** | Bodies > `INSPECTOR_MAX_BODY_KB` (default 1024 KB) are truncated with `"(truncated for performance)"` notice |
|
||||
| **Sensitive header masking** | `authorization`, `cookie`, `api-key`, `x-api-key`, `proxy-authorization` → `Bearer ***` in Headers tab; "Show secrets" toggle |
|
||||
| **CSP** | Strict Content Security Policy on Traffic Inspector pages to prevent XSS via injected response bodies |
|
||||
| **No persistence by default** | The `TrafficBuffer` is in-memory and lost on server restart. Sessions are persisted only when explicitly recorded |
|
||||
|
||||
### Hard Rules applied
|
||||
|
||||
| Rule | Application |
|
||||
|------|-------------|
|
||||
| **#12** `sanitizeErrorMessage` | All HTTP error responses from Traffic Inspector routes are sanitized |
|
||||
| Rule | Application |
|
||||
| --------------------------------- | ------------------------------------------------------------------------------------- |
|
||||
| **#12** `sanitizeErrorMessage` | All HTTP error responses from Traffic Inspector routes are sanitized |
|
||||
| **#15 + #17** `isLocalOnlyPath()` | `/api/tools/traffic-inspector/` is LOCAL_ONLY + SPAWN_CAPABLE (system proxy commands) |
|
||||
|
||||
### Known limitations
|
||||
@@ -362,6 +366,7 @@ Traffic Inspector shows **all intercepted HTTPS traffic**, including authorizati
|
||||
### WebSocket disconnection
|
||||
|
||||
If the live tail shows "Disconnected":
|
||||
|
||||
1. Check the server is still running: `GET /api/tools/traffic-inspector/capture-modes`
|
||||
2. Reload the page — the WebSocket reconnects and receives a fresh snapshot
|
||||
3. If the server was restarted, the in-memory buffer was cleared — old entries are gone unless a session was recorded
|
||||
@@ -369,11 +374,13 @@ If the live tail shows "Disconnected":
|
||||
### Port 8080 conflict
|
||||
|
||||
If HTTP_PROXY mode fails to start:
|
||||
|
||||
```bash
|
||||
lsof -i :8080 # find the process
|
||||
```
|
||||
|
||||
Change the port:
|
||||
|
||||
```bash
|
||||
# .env
|
||||
INSPECTOR_HTTP_PROXY_PORT=8888
|
||||
@@ -384,17 +391,20 @@ INSPECTOR_HTTP_PROXY_PORT=8888
|
||||
If OmniRoute crashes while system-wide proxy mode is active:
|
||||
|
||||
**macOS:**
|
||||
|
||||
```bash
|
||||
networksetup -setwebproxystate Wi-Fi off
|
||||
networksetup -setsecurewebproxystate Wi-Fi off
|
||||
```
|
||||
|
||||
**Linux (GNOME):**
|
||||
|
||||
```bash
|
||||
gsettings set org.gnome.system.proxy mode 'none'
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
|
||||
```cmd
|
||||
netsh winhttp reset proxy
|
||||
```
|
||||
@@ -404,6 +414,7 @@ The dashboard will also offer "Revert system proxy" on next load if it detects t
|
||||
### Buffer full
|
||||
|
||||
When the buffer reaches `INSPECTOR_BUFFER_SIZE` (default 1000), new entries rotate out the oldest. If important requests are being lost:
|
||||
|
||||
- Increase `INSPECTOR_BUFFER_SIZE` (e.g., 5000) — trades memory for retention
|
||||
- Record a session to persist the relevant window to DB
|
||||
|
||||
@@ -417,43 +428,43 @@ Base path: `/api/tools/traffic-inspector/`
|
||||
|
||||
### Request management
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/requests` | List requests (filterable: `?profile=llm&host=&agent=&status=&source=&sessionId=`) |
|
||||
| GET | `/requests/{id}` | Single request details |
|
||||
| DELETE | `/requests` | Clear the in-memory buffer |
|
||||
| POST | `/requests/{id}/replay` | Re-execute the same request through OmniRoute router |
|
||||
| PUT | `/requests/{id}/annotation` | Save or update a note on a request |
|
||||
| Method | Path | Description |
|
||||
| ------ | --------------------------- | ---------------------------------------------------------------------------------- |
|
||||
| GET | `/requests` | List requests (filterable: `?profile=llm&host=&agent=&status=&source=&sessionId=`) |
|
||||
| GET | `/requests/{id}` | Single request details |
|
||||
| DELETE | `/requests` | Clear the in-memory buffer |
|
||||
| POST | `/requests/{id}/replay` | Re-execute the same request through OmniRoute router |
|
||||
| PUT | `/requests/{id}/annotation` | Save or update a note on a request |
|
||||
|
||||
### WebSocket
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/ws` | Live WebSocket stream. Sends `snapshot` on connect, then `new`/`update`/`clear` events |
|
||||
| Method | Path | Description |
|
||||
| ------ | ----- | -------------------------------------------------------------------------------------- |
|
||||
| GET | `/ws` | Live WebSocket stream. Sends `snapshot` on connect, then `new`/`update`/`clear` events |
|
||||
|
||||
### Export
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/export.har` | Export current filtered list as HAR 1.2 |
|
||||
| Method | Path | Description |
|
||||
| ------ | ------------- | --------------------------------------- |
|
||||
| GET | `/export.har` | Export current filtered list as HAR 1.2 |
|
||||
|
||||
### Custom hosts
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/hosts` | List custom hosts |
|
||||
| POST | `/hosts` | Add host (auto-edits `/etc/hosts`) |
|
||||
| DELETE | `/hosts/{host}` | Remove host |
|
||||
| PATCH | `/hosts/{host}` | Toggle `enabled` |
|
||||
| Method | Path | Description |
|
||||
| ------ | --------------- | ---------------------------------- |
|
||||
| GET | `/hosts` | List custom hosts |
|
||||
| POST | `/hosts` | Add host (auto-edits `/etc/hosts`) |
|
||||
| DELETE | `/hosts/{host}` | Remove host |
|
||||
| PATCH | `/hosts/{host}` | Toggle `enabled` |
|
||||
|
||||
### Capture modes
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/capture-modes` | State of the AgentBridge / custom-hosts / HTTP_PROXY / system-proxy modes + the `tls-intercept` toggle |
|
||||
| POST | `/capture-modes/http-proxy` | Start/stop HTTP_PROXY listener (`{action: "start"\|"stop"}`) |
|
||||
| POST | `/capture-modes/system-proxy` | Apply/revert system-wide proxy (`{action: "apply"\|"revert"}`) |
|
||||
| POST | `/capture-modes/tls-intercept` | Toggle HTTPS body decryption in proxy mode (`{enabled: boolean}`) |
|
||||
| Method | Path | Description |
|
||||
| ------ | ------------------------------ | ------------------------------------------------------------------------------------------------------ |
|
||||
| GET | `/capture-modes` | State of the AgentBridge / custom-hosts / HTTP_PROXY / system-proxy modes + the `tls-intercept` toggle |
|
||||
| POST | `/capture-modes/http-proxy` | Start/stop HTTP_PROXY listener (`{action: "start"\|"stop"}`) |
|
||||
| POST | `/capture-modes/system-proxy` | Apply/revert system-wide proxy (`{action: "apply"\|"revert"}`) |
|
||||
| POST | `/capture-modes/tls-intercept` | Toggle HTTPS body decryption in proxy mode (`{enabled: boolean}`) |
|
||||
|
||||
> **TPROXY decrypt** (capture mode 5) is driven by a **separate** route under the
|
||||
> AgentBridge prefix — `GET / POST / DELETE /api/tools/agent-bridge/tproxy` — not
|
||||
@@ -462,19 +473,19 @@ Base path: `/api/tools/traffic-inspector/`
|
||||
|
||||
### Sessions
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| POST | `/sessions` | Start recording (`{name?: string}`) |
|
||||
| PATCH | `/sessions/{id}` | Stop or rename (`{action: "stop"\|"rename", name?: string}`) |
|
||||
| GET | `/sessions` | List all saved sessions |
|
||||
| GET | `/sessions/{id}` | Session snapshot (all requests) |
|
||||
| DELETE | `/sessions/{id}` | Delete session |
|
||||
| GET | `/sessions/{id}/export.har` | Export session as HAR 1.2 |
|
||||
| Method | Path | Description |
|
||||
| ------ | --------------------------- | ------------------------------------------------------------ |
|
||||
| POST | `/sessions` | Start recording (`{name?: string}`) |
|
||||
| PATCH | `/sessions/{id}` | Stop or rename (`{action: "stop"\|"rename", name?: string}`) |
|
||||
| GET | `/sessions` | List all saved sessions |
|
||||
| GET | `/sessions/{id}` | Session snapshot (all requests) |
|
||||
| DELETE | `/sessions/{id}` | Delete session |
|
||||
| GET | `/sessions/{id}/export.har` | Export session as HAR 1.2 |
|
||||
|
||||
### Internal ingest (D4 fallback)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| POST | `/internal/ingest` | Accepts intercepted request from `server.cjs` passthrough path; requires `INSPECTOR_INTERNAL_INGEST_TOKEN` header |
|
||||
| Method | Path | Description |
|
||||
| ------ | ------------------ | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| POST | `/internal/ingest` | Accepts intercepted request from `server.cjs` passthrough path; requires `INSPECTOR_INTERNAL_INGEST_TOKEN` header |
|
||||
|
||||
Full OpenAPI schemas: `docs/openapi.yaml` → tag `Traffic Inspector`.
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
title: "Webhooks"
|
||||
version: 3.8.2
|
||||
lastUpdated: 2026-05-13
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Webhooks
|
||||
|
||||
> **Source of truth:** `src/lib/webhookDispatcher.ts`, `src/lib/db/webhooks.ts`, `src/app/api/webhooks/`
|
||||
> **Last updated:** 2026-05-13 — v3.8.0
|
||||
> **Last updated:** 2026-06-28 — v3.8.40
|
||||
|
||||
OmniRoute can fire HTTP webhooks on platform events. Use them to integrate with
|
||||
Slack, PagerDuty, Datadog, internal alerting services, or any HTTP receiver.
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
"OBSIDIAN_CONTEXT",
|
||||
"OPENCODE",
|
||||
"PLUGIN_MARKETPLACE",
|
||||
"PLUGINS",
|
||||
"PLUGIN_SDK",
|
||||
"SKILLS",
|
||||
"WEBHOOKS"
|
||||
]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Troubleshooting"
|
||||
version: 3.8.2
|
||||
lastUpdated: 2026-05-13
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Troubleshooting
|
||||
@@ -18,17 +18,17 @@ Common problems and solutions for OmniRoute.
|
||||
|
||||
**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 |
|
||||
| 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 [Quick Fixes](#quick-fixes) below, or ask on [Discord](https://discord.gg/EkzRkpzKYt).
|
||||
|
||||
@@ -55,7 +55,7 @@ Common problems and solutions for OmniRoute.
|
||||
|
||||
### 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 20, 22, or 24 patch level that falls below the patched security floor OmniRoute requires.
|
||||
**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:**
|
||||
|
||||
@@ -74,7 +74,7 @@ Common problems and solutions for OmniRoute.
|
||||
3. Reinstall OmniRoute: `npm install -g omniroute`
|
||||
4. Restart: `omniroute`
|
||||
|
||||
> **Supported secure versions:** `>=20.20.2 <21`, `>=22.22.2 <23`, or `>=24.0.0 <27`. Node.js 24.x LTS (Krypton) and Node.js 26 are fully supported.
|
||||
> **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"
|
||||
|
||||
@@ -100,7 +100,7 @@ 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 range is **`>=20.20.2 <21`, `>=22.22.2 <23`, or `>=24.0.0 <27`** (`engines` field in `package.json`). Node.js 24.x LTS (Krypton) and Node.js 26 are fully supported with `better-sqlite3` v12.x.
|
||||
> **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.
|
||||
|
||||
---
|
||||
|
||||
@@ -227,7 +227,7 @@ curl -s http://localhost:20128/api/cli-tools/openclaw-settings | jq '{installed,
|
||||
|
||||
1. Check usage stats in Dashboard → Usage
|
||||
2. Switch primary model to GLM/MiniMax
|
||||
3. Use free tier (Gemini CLI, Qoder) for non-critical tasks
|
||||
3. Use free tier (Qoder, Kiro) for non-critical tasks
|
||||
4. Set cost budgets per API key: Dashboard → API Keys → Budget
|
||||
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Claude Code CLI — Configuration with OmniRoute"
|
||||
version: 3.8.30
|
||||
lastUpdated: 2026-06-19
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Claude Code CLI — Configuration with OmniRoute
|
||||
@@ -32,15 +32,15 @@ omniroute launch --profile glm52 # Claude Code using glm/glm-5.2 via OmniRoute
|
||||
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) |
|
||||
| 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 |
|
||||
| `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.
|
||||
|
||||
@@ -70,8 +70,8 @@ profile per model at `~/.claude/profiles/<name>/settings.json`, reusing the
|
||||
"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"
|
||||
}
|
||||
"CLAUDE_CODE_AUTO_COMPACT_WINDOW": "190000",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "CLI Integrations — point any coding CLI at OmniRoute"
|
||||
version: 3.8.31
|
||||
lastUpdated: 2026-06-20
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# CLI Integrations
|
||||
@@ -49,7 +49,6 @@ server and writes the config locally.
|
||||
| `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 setup-gemini` | Gemini CLI (native) | `~/.gemini/settings.json` (`model`) + prints env recipe; base URL is env-only | `--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 |
|
||||
|
||||
@@ -65,7 +64,7 @@ Notes on flags (verified in the command source):
|
||||
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, Gemini. Those tools
|
||||
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`
|
||||
@@ -165,11 +164,6 @@ tool expects (verified in the command source):
|
||||
| `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 |
|
||||
| `setup-gemini` (`GOOGLE_GEMINI_BASE_URL`) | root | No — the genai SDK appends `/v1beta` |
|
||||
|
||||
> Gemini CLI caveat: a cached Google login can make the CLI ignore
|
||||
> `GOOGLE_GEMINI_BASE_URL`. Run it logged-out / API-key-only so the base URL takes
|
||||
> effect (`setup-gemini` prints this warning too).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Codex CLI — Configuration with OmniRoute"
|
||||
version: 3.8.29
|
||||
lastUpdated: 2026-06-18
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Codex CLI — Configuration with OmniRoute
|
||||
@@ -38,11 +38,11 @@ export OMNIROUTE_API_KEY="<YOUR_KEY>"
|
||||
|
||||
> **Common host options**
|
||||
>
|
||||
> | Access | URL |
|
||||
> |--------|-----|
|
||||
> | Access | URL |
|
||||
> | ------------- | ----------------------------- |
|
||||
> | Local network | `http://192.168.0.1:20128/v1` |
|
||||
> | Tailscale | `http://100.x.x.x:20128/v1` |
|
||||
> | Loopback | `http://localhost:20128/v1` |
|
||||
> | Tailscale | `http://100.x.x.x:20128/v1` |
|
||||
> | Loopback | `http://localhost:20128/v1` |
|
||||
|
||||
---
|
||||
|
||||
@@ -72,36 +72,36 @@ You never need a separate translation proxy when using OmniRoute. **All models u
|
||||
|
||||
### 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+). |
|
||||
| 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 |
|
||||
| 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.
|
||||
|
||||
@@ -113,11 +113,11 @@ You never need a separate translation proxy when using OmniRoute. **All models u
|
||||
|
||||
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 |
|
||||
| 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.
|
||||
@@ -128,13 +128,13 @@ Other providers use their own prefix (`kmc/`, `glm/`, `ds/`, `ollamacloud/`, `op
|
||||
|
||||
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 |
|
||||
| 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
|
||||
@@ -170,53 +170,53 @@ 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 (rápidos) — 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 |
|
||||
| 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` |
|
||||
| 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` |
|
||||
| Maximum thinking quality | `-p glm52max` or `-p mimo-pro` |
|
||||
| Cost-conscious | `-p gemma4` or `-p gptoss` |
|
||||
|
||||
---
|
||||
|
||||
@@ -267,12 +267,12 @@ 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 |
|
||||
| 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)
|
||||
|
||||
@@ -343,11 +343,11 @@ 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 |
|
||||
| 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
|
||||
@@ -358,16 +358,17 @@ 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 |
|
||||
| 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
|
||||
@@ -376,13 +377,13 @@ 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 |
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Cost & Spend Tracking"
|
||||
version: 3.8.24
|
||||
lastUpdated: 2026-06-13
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Cost & Spend Tracking
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "🐳 Docker Guide — OmniRoute"
|
||||
version: 3.8.2
|
||||
lastUpdated: 2026-05-13
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# 🐳 Docker Guide — OmniRoute
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
title: "Electron Desktop Guide"
|
||||
version: 3.8.2
|
||||
lastUpdated: 2026-05-13
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Electron Desktop Guide
|
||||
|
||||
> **Source of truth:** `electron/` workspace
|
||||
> **Last updated:** 2026-05-13 — v3.8.0
|
||||
> **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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "OmniRoute — Dashboard Features Gallery"
|
||||
version: 3.8.2
|
||||
lastUpdated: 2026-05-13
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# OmniRoute — Dashboard Features Gallery
|
||||
@@ -10,7 +10,7 @@ lastUpdated: 2026-05-13
|
||||
|
||||
Visual guide to every section of the OmniRoute dashboard.
|
||||
|
||||
> 📅 **Last updated:** 2026-05-13 — **v3.8.0**
|
||||
> 📅 **Last updated:** 2026-06-28 — **v3.8.40**
|
||||
|
||||
---
|
||||
|
||||
@@ -51,7 +51,7 @@ The v3.7.x → v3.8.0 cycle added zero-config auto routing, new providers, OAuth
|
||||
|
||||
## 🔌 Providers
|
||||
|
||||
Manage AI provider connections: OAuth providers (Claude Code, Codex, Gemini CLI), 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.
|
||||
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`.
|
||||
|
||||
@@ -128,7 +128,7 @@ Comprehensive settings panel with **7 tabs**:
|
||||
|
||||
## 🔧 CLI Tools
|
||||
|
||||
One-click configuration for AI coding tools: Claude Code, Codex CLI, Gemini CLI, OpenClaw, Kilo Code, Antigravity, Cline, Continue, Cursor, and Factory Droid. Features automated config apply/reset, connection profiles, and model mapping.
|
||||
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.
|
||||
|
||||

|
||||
|
||||
@@ -136,7 +136,7 @@ One-click configuration for AI coding tools: Claude Code, Codex CLI, Gemini CLI,
|
||||
|
||||
## 🤖 CLI Agents _(v2.0.11+)_
|
||||
|
||||
Dashboard for discovering and managing CLI agents. Shows a grid of 18 built-in agents (Codex, Claude, Goose, Gemini CLI, OpenClaw, Aider, OpenCode, Cline, Qwen Code, ForgeCode, Amazon Q, Open Interpreter, Cursor CLI, Warp, **Windsurf**, **Devin CLI**, **Kimi Coding**, **Command Code**) with:
|
||||
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.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Free Provider Rankings (Arena ELO)"
|
||||
version: 3.8.24
|
||||
lastUpdated: 2026-06-13
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Free Provider Rankings (Arena ELO)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user