diff --git a/.env.example b/.env.example index cf147c7f22..ebb1f7073c 100644 --- a/.env.example +++ b/.env.example @@ -2232,3 +2232,30 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis # Used by: open-sse/services/usage/hyperagent.ts # ───────────────────────────────────────────────────────────────────────────── # HYPERAGENT_USAGE_URL=https://hyperagent.com/api/settings/billing/usage + +# ───────────────────────────────────────────────────────────────────────────── +# Browser-login VNC sessions (optional — src/lib/vncSession/manifest.ts) +# Containerized Chromium+VNC used for interactive browser-login credential +# capture via /api/vnc-session. All optional — defaults target the bundled +# `omniroute-vnc-chromium:local` image; override only for a custom image, ports, +# or lifecycle tuning. +# ───────────────────────────────────────────────────────────────────────────── +# OMNIROUTE_VNC_IMAGE=omniroute-vnc-chromium:local +# OMNIROUTE_DOCKER_BIN=docker +# OMNIROUTE_VNC_CONTAINER_VNC_PORT=3000 +# OMNIROUTE_VNC_CONTAINER_CDP_PORT=9223 +# OMNIROUTE_VNC_CONTAINER_PROFILE_DIR=/config +# OMNIROUTE_VNC_PROFILE_DIR= +# OMNIROUTE_VNC_IDLE_MS=600000 +# OMNIROUTE_VNC_MAX_MS=1800000 +# OMNIROUTE_VNC_MAX_SESSIONS=4 +# OMNIROUTE_VNC_READY_MS=45000 +# OMNIROUTE_VNC_HARVEST_MS=20000 +# OMNIROUTE_VNC_CHROMIUM_ARGS=--remote-debugging-port=9222 --no-first-run --no-default-browser-check + +# ───────────────────────────────────────────────────────────────────────────── +# Data-dir alias (optional — open-sse/services/notionThreadSessions.ts) +# Legacy fallback for DATA_DIR, checked only after DATA_DIR and +# OMNIROUTE_DATA_DIR are both unset. Locates the Notion web-thread session cache. +# ───────────────────────────────────────────────────────────────────────────── +# VIBEPROXY_DATA_DIR= diff --git a/README.md b/README.md index b0d070df0a..8715a6ca9a 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,27 @@
+# 🆓 Works the second you install it — no keys, no config + +
+ +> **Install → point your tool at the endpoint → it already answers.** OmniRoute ships with keyless free providers (OpenCode Free, Felo) already wired into the `auto` combo, so a **fresh install responds out of the box** — no API key, no signup, no configuration. + +```bash +# Fresh install, zero credentials — `auto` already works: +curl http://localhost:20128/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{"model":"auto","messages":[{"role":"user","content":"Hello!"}]}' +``` + +- ✅ **`auto` responds immediately** — OmniRoute builds a virtual combo from the built-in keyless free providers and routes to a healthy one, with no setup. +- ➕ **Add more providers anytime** — drop in a Claude / GPT / Gemini key (or any of the 278 providers) from the dashboard and they join the `auto` pool automatically. +- 🎛️ **Build your own free combos** — chain your free tiers with any of the 18 routing strategies so you never run out of quota. + +Prefer a specific free backend? Call it directly, e.g. `oc/…` (OpenCode Free) or `felo/…` (Felo). Then graduate to `auto` and let OmniRoute pick. + +
+ # 💥 The Promise
@@ -265,7 +286,7 @@ All **18** strategies — mix & match per combo step: - **🤖 One-command CLI/agent setup** — `setup-*` configures 12+ coding tools; `omniroute launch` / `launch-codex` are zero-config. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md) - **🛰️ Remote mode** — drive a remote OmniRoute with scoped tokens (`connect` / `contexts` / `tokens`) + an `antigravity` OAuth helper for VPS installs. → [Remote Mode](docs/guides/REMOTE-MODE.md) - **🧭 Smarter auto-routing** — `auto/:` combos, **Fusion** (model panel + judge), task-aware routing, per-request model / mode / USD-budget overrides. → [Auto-Combo](docs/routing/AUTO-COMBO.md) -- **🗜️ Pluggable compression** — 11 composable engines + Compression Studios: LLMLingua-2, two-tier Ultra, omniglyph, per-step fidelity gate, GCF v3.2, drag-reorder editor. → [Compression](docs/compression/COMPRESSION_ENGINES.md) +- **🗜️ Pluggable compression** — 12 composable engines + Compression Studios: LLMLingua-2, two-tier Ultra, omniglyph, per-step fidelity gate, GCF v3.2, drag-reorder editor. → [Compression](docs/compression/COMPRESSION_ENGINES.md) - **🕵️ Transparent MITM decrypt (TPROXY)** — capture CLIs that ignore proxy env vars, with a per-SNI CA + trust-store installer. → [MITM/TPROXY](docs/security/MITM-TPROXY-DECRYPT.md) - **💸 Cost telemetry everywhere** — `X-OmniRoute-*` cost/usage headers on every endpoint, cache-HIT savings header, per-key USD spend quotas. → [API Reference](docs/reference/API_REFERENCE.md) - **🧠 Memory you control** — off by default, opt-in int8 vector quantization + typed decay, per-request `x-omniroute-no-memory`. → [Memory](docs/frameworks/MEMORY.md) @@ -488,24 +509,26 @@ claude mcp add-server omniroute --type http --url http://localhost:20128/api/mcp -> **Why use many tokens when few tokens do the trick?** Every request passes through OmniRoute's compression pipeline **transparently** — no client changes. It's now a **stack of 11 composable engines** that run in order and mix & match per routing combo — building on ideas from [RTK](https://github.com/rtk-ai/rtk), [Caveman](https://github.com/JuliusBrussee/caveman) (⭐ 90K+), [LLMLingua-2](https://github.com/microsoft/LLMLingua), and [Troglodita](https://github.com/leninejunior/troglodita) (PT-BR). +> **Why use many tokens when few tokens do the trick?** Every request passes through OmniRoute's compression pipeline **transparently** — no client changes. It's now a **stack of 12 composable engines** that run in order and mix & match per routing combo — building on ideas from [RTK](https://github.com/rtk-ai/rtk), [Caveman](https://github.com/JuliusBrussee/caveman) (⭐ 90K+), [LLMLingua-2](https://github.com/microsoft/LLMLingua), and [Troglodita](https://github.com/leninejunior/troglodita) (PT-BR). -### 🧱 The 11-engine stack +### 🧱 The 12-engine stack Engines run in pipeline order; each is independently toggleable and configurable per combo: -| # | Engine | What it does | -| --- | ----------------- | ----------------------------------------------------------------------------------------------------------------------- | -| 1 | **Session-Dedup** | Drops content repeated across turns (content-addressed, cross-turn) | -| 2 | **CCR** | Archives large blocks behind retrieve markers, fetched on demand | -| 3 | **RTK** | Smart tool-result filtering, dedup & truncation (command-aware) | -| 4 | **Headroom** | Lossless tabular compaction of homogeneous JSON arrays, flat or nested (~30%), via a vendored **GCF** codec (spec v3.2) | -| 5 | **Relevance** | Extractive sentence scoring against the last user query | -| 6 | **Caveman** | Rule-based prose compression (~65–75% on output) | -| 7 | **LLMLingua-2** | ML semantic pruning via MobileBERT ONNX — code-safe, async | -| 8 | **Lite** | Whitespace + image-URL trimming (latency-light baseline) | -| 9 | **Aggressive** | Summarization + progressive aging of old turns | -| 10 | **Ultra** | Heuristic token pruning with an optional small-model (SLM) tier | +| # | Engine | What it does | +| --- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| 1 | **Session-Dedup** | Drops content repeated across turns (content-addressed, cross-turn) | +| 2 | **CCR** | Archives large blocks behind retrieve markers, fetched on demand | +| 3 | **Lite** | Whitespace + image-URL trimming (latency-light baseline) | +| 4 | **RTK** | Smart tool-result filtering, dedup & truncation (command-aware) | +| 5 | **Responses Tool Output** | Lossless-first JSON + bounded diagnostic compression for shell/patch/search/build outputs (Responses API) | +| 6 | **Headroom** | Lossless tabular compaction of homogeneous JSON arrays, flat or nested (~30%), via a vendored **GCF** codec (spec v3.2) | +| 7 | **Relevance** | Extractive sentence scoring against the last user query | +| 8 | **Caveman** | Rule-based prose compression (~65–75% on output) | +| 9 | **Aggressive** | Summarization + progressive aging of old turns | +| 10 | **LLMLingua-2** | ML semantic pruning via MobileBERT ONNX — code-safe, async | +| 11 | **Ultra** | Heuristic token pruning with an optional small-model (SLM) tier | +| 12 | **OmniGlyph** | Experimental context-as-image encoding routed to Claude Fable 5 (most aggressive; opt-in) | Code blocks, URLs and structured data are **always preserved** byte-perfect. **One-click presets** combine the engines: @@ -538,7 +561,7 @@ Code blocks, URLs and structured data are **always preserved** byte-perfect. **O ### 📖 How it works — pipeline, architecture & savings math -OmniRoute compression pipeline: a client request of 10,000 tokens passes through 11 stacked engines — Session-Dedup, CCR, RTK, Headroom, Relevance, Caveman, LLMLingua-2, Omniglyph, Lite, Aggressive, Ultra — and reaches the provider at about 1,080 tokens, up to 95% saved. Code, URLs and JSON are always preserved byte-perfect. +OmniRoute compression pipeline: a client request of 10,000 tokens passes through 12 stacked engines — Session-Dedup, CCR, Lite, RTK, Responses Tool Output, Headroom, Relevance, Caveman, Aggressive, LLMLingua-2, Ultra, OmniGlyph — and reaches the provider at about 1,080 tokens, up to 95% saved. Code, URLs and JSON are always preserved byte-perfect. Default stacked combo runs `RTK → Caveman`. When both act on the same tool/context payload, savings compound: diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index d3dd45fe02..46b60e976a 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -1152,6 +1152,26 @@ Provider quota endpoints, network tunnels (Tailscale, Ngrok, MITM debug proxy), | `OMNIROUTE_ROTATE_400_THRESHOLD` | `1` | `open-sse/services/rotationConfig.ts` | Number of `400` errors within `OMNIROUTE_ROTATE_400_WINDOW_SECONDS` required before the account is rotated (only consulted when `OMNIROUTE_ROTATE_ON_400=true`). | | `OMNIROUTE_ROTATE_400_WINDOW_SECONDS` | `120` | `open-sse/services/rotationConfig.ts` | Sliding window (seconds) over which `400` errors are counted toward `OMNIROUTE_ROTATE_400_THRESHOLD`. | +### Browser-Login VNC Sessions & Data-Dir Alias + +Containerized Chromium+VNC used for interactive browser-login credential capture (`/api/vnc-session`), plus a legacy `DATA_DIR` alias. All optional — the VNC defaults target the bundled `omniroute-vnc-chromium:local` image and are only overridden for a custom container image, ports, or lifecycle tuning. + +| Variable | Default | Source File | Description | +| --- | --- | --- | --- | +| `OMNIROUTE_VNC_IMAGE` | `omniroute-vnc-chromium:local` | `src/lib/vncSession/manifest.ts` | Docker image tag for the Chromium+VNC login container. Build `docker/vnc-browser/chromium` or point this at a custom image. | +| `OMNIROUTE_DOCKER_BIN` | `docker` | `src/lib/vncSession/manifest.ts` | Container runtime binary used to launch the VNC container (e.g. set to `podman`). | +| `OMNIROUTE_VNC_CONTAINER_VNC_PORT` | `3000` | `src/lib/vncSession/manifest.ts` | VNC/noVNC port exposed inside the container. | +| `OMNIROUTE_VNC_CONTAINER_CDP_PORT` | `9223` | `src/lib/vncSession/manifest.ts` | Chrome DevTools Protocol port inside the container. | +| `OMNIROUTE_VNC_CONTAINER_PROFILE_DIR` | `/config` | `src/lib/vncSession/manifest.ts` | Chromium profile directory path inside the container. | +| `OMNIROUTE_VNC_PROFILE_DIR` | `$HOME/.omniroute/browser-login-profiles` | `src/lib/vncSession/manifest.ts` | Host directory holding persisted browser-login profiles. | +| `OMNIROUTE_VNC_IDLE_MS` | `600000` (10 min) | `src/lib/vncSession/manifest.ts` | Idle timeout (ms) before an inactive VNC session is reaped. | +| `OMNIROUTE_VNC_MAX_MS` | `1800000` (30 min) | `src/lib/vncSession/manifest.ts` | Hard cap (ms) on a single VNC session's lifetime. | +| `OMNIROUTE_VNC_MAX_SESSIONS` | `4` | `src/lib/vncSession/manifest.ts` | Maximum number of concurrent VNC sessions. | +| `OMNIROUTE_VNC_READY_MS` | `45000` | `src/lib/vncSession/manifest.ts` | Timeout (ms) waiting for the containerized browser to become CDP-ready. | +| `OMNIROUTE_VNC_HARVEST_MS` | `20000` | `src/lib/vncSession/manifest.ts` | Timeout (ms) for harvesting the captured session/cookies after login completes. | +| `OMNIROUTE_VNC_CHROMIUM_ARGS` | `--remote-debugging-port=9222 --no-first-run --no-default-browser-check` | `src/lib/vncSession/manifest.ts` | Extra command-line flags passed to the containerized Chromium. | +| `VIBEPROXY_DATA_DIR` | _(unset)_ | `open-sse/services/notionThreadSessions.ts` | **Legacy alias** for `DATA_DIR`, checked only after both `DATA_DIR` and `OMNIROUTE_DATA_DIR` are unset. Locates the Notion web-thread session cache (`/notion-web-thread-sessions.json`). | + --- ## 26. Test & E2E Harness diff --git a/open-sse/services/autoCombo/virtualFactory.ts b/open-sse/services/autoCombo/virtualFactory.ts index bd48f8dbdf..b81ac27d37 100644 --- a/open-sse/services/autoCombo/virtualFactory.ts +++ b/open-sse/services/autoCombo/virtualFactory.ts @@ -134,8 +134,19 @@ function hasUsableConnectionCredential(conn: VirtualFactoryConn): boolean { const SYNTHETIC_NOAUTH_CONNECTION_ID = "noauth"; +// Allowlist of no-auth (keyless) providers permitted to enter the `auto`/`auto-*` +// candidate pool. Narrowed to the backends verified to answer without any +// configuration on our reference egress (VPS .15): `opencode` and `felo-web` +// both return 200 there, while duckduckgo-web (429/VQD rate limit), theoldllm +// (403 Vercel egress block), chipotle (502), aihorde (401, anon key rejected) +// and the others are unreliable. The excluded providers stay fully usable via +// direct `/` calls — they are just kept OUT of auto-routing until +// re-verified. Re-add an id here to bring it back into every auto/* pool. +const AUTO_COMBO_NOAUTH_ALLOWLIST = new Set(["opencode", "felo-web"]); + function isChatAutoComboNoAuthProvider(providerDef: NoAuthProviderDefinition): boolean { if (providerDef.noAuth !== true) return false; + if (!AUTO_COMBO_NOAUTH_ALLOWLIST.has(providerDef.id)) return false; if (!Array.isArray(providerDef.serviceKinds) || providerDef.serviceKinds.length === 0) return true; return providerDef.serviceKinds.includes("llm"); diff --git a/tests/unit/noauth-autocombo-allowlist.test.ts b/tests/unit/noauth-autocombo-allowlist.test.ts new file mode 100644 index 0000000000..21b3ad98af --- /dev/null +++ b/tests/unit/noauth-autocombo-allowlist.test.ts @@ -0,0 +1,107 @@ +/** + * Auto-combo no-auth allowlist — the `auto`/`auto-*` candidate pool must only + * pull in no-auth (keyless) providers verified to work without any credential on + * our reference egress. As of this change that allowlist is narrowed to + * `opencode` and `felo-web`: on the reference VPS (.15) they are the only no-auth + * backends that answer 200 with zero configuration. The other no-auth providers + * (duckduckgo-web, theoldllm, chipotle, aihorde) stay OUT of every auto/* pool + * until re-verified — they remain usable via direct `/` calls, they + * are just not auto-routed to. + * + * Regression guard: AUTO_COMBO_NOAUTH_ALLOWLIST in + * open-sse/services/autoCombo/virtualFactory.ts drives what belongs here. + */ +import test 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-noauth-allowlist-")); +const ORIGINAL_DATA_DIR = process.env.DATA_DIR; + +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +const virtualFactory = await import("../../open-sse/services/autoCombo/virtualFactory.ts"); + +async function resetStorage() { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +} + +test.beforeEach(async () => { + await resetStorage(); +}); + +test.after(async () => { + await resetStorage(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + + if (ORIGINAL_DATA_DIR === undefined) { + delete process.env.DATA_DIR; + } else { + process.env.DATA_DIR = ORIGINAL_DATA_DIR; + } +}); + +const ALLOWED_NOAUTH_PROVIDERS = ["opencode", "felo-web"]; +const EXCLUDED_NOAUTH_PROVIDERS = ["duckduckgo-web", "theoldllm", "chipotle", "aihorde"]; + +test("fresh install: the allowlisted no-auth providers are present in the auto-combo pool", async () => { + const combo = await virtualFactory.createVirtualAutoCombo(undefined); + + for (const providerId of ALLOWED_NOAUTH_PROVIDERS) { + const models = combo.models.filter((m: { providerId: string }) => m.providerId === providerId); + assert.ok( + models.length >= 1, + `allowlisted no-auth provider "${providerId}" must be present in the fresh-install ` + + `auto-combo pool. Pool providers: ${JSON.stringify([...new Set(combo.models.map((m: { providerId: string }) => m.providerId))])}` + ); + assert.ok( + models.every((m: { connectionId: string }) => m.connectionId === "noauth"), + `all "${providerId}" models must use the synthetic noauth connection` + ); + assert.ok( + combo.autoConfig.candidatePool.includes(providerId), + `"${providerId}" must be in the candidatePool` + ); + } +}); + +test("fresh install: non-allowlisted no-auth providers are EXCLUDED from the auto-combo pool", async () => { + const combo = await virtualFactory.createVirtualAutoCombo(undefined); + + for (const providerId of EXCLUDED_NOAUTH_PROVIDERS) { + const present = combo.models.some((m: { providerId: string }) => m.providerId === providerId); + assert.equal( + present, + false, + `no-auth provider "${providerId}" must NOT be auto-routed to (not in allowlist), ` + + `but it appeared in the pool. Pool: ${JSON.stringify(combo.models.map((m: { model: string }) => m.model))}` + ); + assert.ok( + !combo.autoConfig.candidatePool.includes(providerId), + `"${providerId}" must not be in the candidatePool` + ); + } +}); + +test("fresh install: EVERY synthetic-noauth candidate belongs to an allowlisted provider (no other keyless provider leaks in)", async () => { + const combo = await virtualFactory.createVirtualAutoCombo(undefined); + + const noauthProviders = [ + ...new Set( + combo.models + .filter((m: { connectionId: string }) => m.connectionId === "noauth") + .map((m: { providerId: string }) => m.providerId) + ), + ].sort(); + assert.deepEqual( + noauthProviders, + [...ALLOWED_NOAUTH_PROVIDERS].sort(), + `only the allowlisted providers may be no-auth (connectionId="noauth") candidates on a ` + + `fresh install, but found: ${JSON.stringify(noauthProviders)}` + ); +}); diff --git a/tests/unit/virtual-auto-combo.test.ts b/tests/unit/virtual-auto-combo.test.ts index 6383613097..4bb7e1fb49 100644 --- a/tests/unit/virtual-auto-combo.test.ts +++ b/tests/unit/virtual-auto-combo.test.ts @@ -203,38 +203,31 @@ test("createVirtualAutoCombo includes no-auth OpenCode Free without provider_con assert.ok(combo.autoConfig.candidatePool.includes("opencode")); }); -test("createVirtualAutoCombo includes all chat-capable no-auth providers without connections", async () => { +test("createVirtualAutoCombo restricts the no-auth pool to the allowlist", async () => { + // Policy: the no-auth (keyless) auto-combo allowlist is narrowed to `opencode` + // and `felo-web` (open-sse/services/autoCombo/virtualFactory.ts::AUTO_COMBO_NOAUTH_ALLOWLIST) — + // the keyless backends verified to work without configuration on our reference + // egress. The others stay usable via direct `/` calls but must + // NOT be auto-routed to. Dedicated guard: + // tests/unit/noauth-autocombo-allowlist.test.ts. const combo: VirtualComboResult = await virtualFactory.createVirtualAutoCombo("fast"); - // Each noAuth provider should have multiple models (not just the first) - const ddgwModels = combo.models.filter((m) => m.providerId === "duckduckgo-web"); - assert.ok(ddgwModels.length >= 1, "duckduckgo-web should have at least one model"); - assert.ok( - ddgwModels.every((m) => m.connectionId === "noauth"), - "all ddgw models should use noauth connection" - ); - assert.ok( - ddgwModels.some((m) => m.model.startsWith("ddgw/")), - "ddgw models should have correct prefix" - ); + for (const allowed of ["opencode", "felo-web"]) { + const models = combo.models.filter((m) => m.providerId === allowed); + assert.ok(models.length >= 1, `${allowed} should have at least one model`); + assert.ok( + models.every((m) => m.connectionId === "noauth"), + `all ${allowed} models should use noauth connection` + ); + } - const tllmModels = combo.models.filter((m) => m.providerId === "theoldllm"); - assert.ok(tllmModels.length >= 1, "theoldllm should have at least one model"); - assert.ok( - tllmModels.every((m) => m.connectionId === "noauth"), - "all tllm models should use noauth connection" - ); - assert.ok( - tllmModels.some((m) => m.model === "tllm/GPT_5_4"), - "tllm should include GPT_5_4" - ); - - const chipotleModels = combo.models.filter((m) => m.providerId === "chipotle"); - assert.ok(chipotleModels.length >= 1, "chipotle should have at least one model"); - assert.ok( - chipotleModels.every((m) => m.connectionId === "noauth"), - "all chipotle models should use noauth connection" - ); + for (const excluded of ["duckduckgo-web", "theoldllm", "chipotle", "aihorde"]) { + assert.equal( + combo.models.some((model) => model.providerId === excluded), + false, + `no-auth provider "${excluded}" must be excluded from the auto-combo pool (not in allowlist)` + ); + } assert.equal( combo.models.some((model) => model.providerId === "veoaifree-web"),