* test(base): realign six suites with contracts that #9100/#8990/#9009 deliberately changed Continuing the base-red drain — every one of these reproduces on the pure tip. - tests/snapshots/provider/translate-path.json: regenerated via UPDATE_GOLDEN=1. The diff is ADDITION-ONLY — the unorouter block from #9009; no existing provider entry changed. 3/3. - tests/unit/provider-models-route.test.ts:ff012ff420added onboardUser as a bootstrap fallback next to loadCodeAssist; the mock now excludes it from the discovery-URL ledger like it already excluded loadCodeAssist, otherwise it consumed the injected 503 and the retry assertion misfired. 59/59. - tests/unit/responses-commentary-passthrough-6199.test.ts: #8990 (c996dc93c2) deliberately preserves `tools` on the TERMINAL response.completed snapshot (Codex CLI rebuilds its tool list from it); the assertion now pins the echoed tools instead of their absence. Still stripped on created/in_progress. 7/7. - tests/unit/vision-compression-authoritative-capability-7237.test.ts:68cb678780added the 'gpt-5' fragment, so the heuristic-vs-spec DRIFT this suite documented no longer exists; the cases now guard the agreement, keep a conservative-for-unknown-ids probe, and reproduce the strip-bug shape with an explicit false instead of deriving it. 4/4. - tests/unit/provider-limits-proxy-fail-closed.test.ts + tests/unit/image-generation-route.test.ts: #9100 made the proxy reachability probe NON-BLOCKING (optimistic dispatch; the probe aborts only in-flight requests — its own t14 sibling was updated to this exact pattern). Instant mocks therefore won the race and the PROXY_UNREACHABLE 503 became unobservable (a success or a generic 502). The mocks now stay in flight (never-resolving, so the aborted continuation cannot reach the restored real fetch), and the fail-closed proof is the settled rejection itself plus zero egress AFTER the fast-fail. Production fail-closed semantics are unchanged — the proxy dispatch path still throws; only the mock timing was stale. 3/3 and 20/20. Refs #9298 * fix(guardrails): forward the router deps seam through callVisionModel tests/unit/guardrails/vision-bridge-sse-and-reasoning.test.ts was 7/7 red on any clean box (CI shard 3/4): callVisionModel() called getBestVisionModel()/ getFallbackModels() WITHOUT the routers' existing VisionBridgeRouterDeps seam, so the credential check always hit the live connections DB — no vision-capable connection meant 'No vision-capable provider connected' before the mocked fetch was ever reached, and on a dev box auto-selection could swap the fixed model under the assertions. The routers already accepted deps; only the forwarding was missing. Added the optional 5th param (backward compatible — the sole production caller, visionBridge.ts, injects its own callVisionModel and is unaffected) and the suite now pins selection with hasUsableCredentials: async () => null (indeterminate → the fixed model is honored, DB untouched). 7/7. Sibling suites re-run green: vision-bridge-callmodel 2/2, visionBridge 25/25, visionBridgeHelpers.callVisionModel 8/8, visionBridgeRouter 10/10, vision-bridge-cc-no-reroute 8/8. Refs #9298 * fix(db,combo): clear the NEW base-reds the 08-06 merge batch introduced The tip moved while the first sweep PR (#9600) was in review, and three fresh base-reds landed with it — same classes as before, all reproduced on the pure tip9995bc4893: 1. ANOTHER migration collision: #9061 shipped 134_ccr_blocks.sql onto the slot 134_proxy_logs_egress_ip.sql (#9291) has held since 08-04. getMigrationFiles() throws on collision, so every DB-touching test died at bootstrap again. Renumbered to 139 (next free slot). No retroactive guard needed this time: both statements are IF NOT EXISTS, and no DB can have applied it as 134 — the runner refused to run at all while the collision existed. 2. BROKEN IMPORT killing the combo module graph: #8894 imported preferAntigravityConnectionsWithStoredProject from ../antigravityProjectPersistence.ts — a module that exists NOWHERE in the repo (it came from an unmerged sibling branch). Anything importing quotaStrategies.ts died with ERR_MODULE_NOT_FOUND. Implemented the helper in the real persistence module (antigravityProjectPersist.ts, #8491) with the semantics the call site needs — prefer connections that already carry a stored projectId, never emptying the pool — and pointed the import there. New regression suite tests/unit/antigravity-prefer-stored-project.test.ts (5/5), including an import-graph probe that reproduces the break shape. 3. Sibling-test drift from #9106 (gemini-3.1-pro-high now user-callable): its own suites were updated but provider-models-route.test.ts was not. Expected discovery list realigned; testFrozen 1784->1787 justified in the baseline (irreducible +2 after comment compression; gate counts split-newlines). Also regenerated tests/snapshots/provider/translate-path.json — addition-only: devin-cli-agentic, raycast, regolo (today's provider merges), zero removals. image-generation-route 20/20 (was import-dead), provider-models-route 59/59, antigravity-prefer-stored-project 5/5, provider-translate-path-golden 3/3. Refs #9298 * fix(changelog): convert the #9415 fragment to the required bullet shape Another base-red from the 08-06 batch:bd4407cb64landed changelog.d/features/9415-newapi-sub2api-aggregator-balance.md as YAML frontmatter + a prose paragraph. Every other fragment in changelog.d/ is a single markdown bullet, and both consumers enforce that — scripts/check/check-changelog-integrity.mjs:97 and the release aggregator (scripts/release/aggregate-changelog.mjs:57) reject anything that does not start with '- ', so 'Merge integrity (changelog + generated skills)' was red for every PR targeting the release branch. Rewritten as a bullet with the standard issue link, preserving the feature description (aggregator gateway toggle, /api/user/self balance read, dashboard badge, quota-preflight skip, NEWAPI_AGGREGATOR_BALANCE flag default off, quotaPerUnit override). Swept the rest of changelog.d/ — this was the only malformed fragment. check:changelog-integrity OK. Refs #9298 * fix(types,docs): clear the 5 typecheck errors and the fabricated env vars on the base Third pass over the base-reds, from the 2026-08-06T22:51Z verdict on #9298 — it reported "Typecheck (core)" with only the FIRST error; there are five, all on the pure tip9995bc4893. Two are real production defects. **Real bugs** - open-sse/services/compression/engines/ccr/index.ts:295 called enforceGlobalBudget(entry.bytes) against an (owner, bytes) signature. The `bytes` argument arrived undefined, so `ccrTotalBytes + undefined` is NaN, `NaN > MAX` is false (the eviction loop exits immediately) and `NaN <= MAX` is false (the re-admit is refused). The #9061 durable tier therefore NEVER repopulated its in-memory map: every retrieve after a restart or an eviction re-read from SQLite forever, and evictions could not prefer the owning principal. Fixed and pinned by a new case in tests/unit/ccr-durable-store-9061.test.ts (11/11) — verified failing against the buggy call and passing against the fix. - open-sse/services/combo/fusionPanel.ts:54 read `step.model` after #8894 widened ComboStep with ComboProviderWildcardStep (which carries modelPattern, not model), so a wildcard step in a fusion panel pushed `undefined` onto the panel. Now resolved through getComboModelString(), which already handles every step shape and returns null for the ones without a concrete model id. **Type-only** - accountSemaphore.ts:203 — isBypassed() returns a plain boolean and cannot narrow `number | null` (an `x is null | undefined` predicate would be unsound: 0 bypasses too). Added resolveActiveCap(), the narrowing companion isBypassed is now defined in terms of; the acquire path uses the narrowed value. - comboStructure.ts:140 — same #8894 widening: `prompt` only exists on a model step, so it is now read under a kind check. - firecrawlQuotaFetcher.ts:136 — the function returns full FirecrawlQuota objects but was annotated Promise<QuotaInfo | null>, which made the custom-base literal an excess-property error. Widened to the accurate type (FirecrawlQuota extends QuotaInfo, so callers are unaffected). **Fabricated docs (the "Docs sync + fabricated-docs (strict)" HARD failure)** docs/ops/VM_DEPLOYMENT_GUIDE.md recommended OMNIROUTE_MAX_POOL_SIZE and OMNIROUTE_DB_POOL_SIZE (#9471). Neither is read anywhere in the codebase. Replaced with the two knobs that do exist and are already documented in ENVIRONMENT.md: OMNIROUTE_MEMORY_MB and OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT. typecheck:core 5 errors -> 0. check:fabricated-docs + check:env-doc-sync OK. accountSemaphore 6/6, ccr-durable-store 11/11, ccr-protocol 9/9, combo-fusion-strategy 10/10, combo-fusion-comboref 5/5, combo-fusion-warn 4/4, firecrawl-executor 7/7, executor-firecrawl-fetch 4/4. Refs #9298 * fix(tests): type the #3440 vertex helpers instead of `any` (the 3 base ESLint errors) The "ESLint errors: 3 error(s)" HARD failure in the #9298 verdict is tests/unit/vertex-functioncall-id-3440.test.ts lines 32/41/50: the three find*(result: any) walkers. `@typescript-eslint/no-explicit-any` is an ERROR in tests/ (and open-sse/) since #6218, and this file landed on 2026-08-04 without a suppressions entry, so every run of `lint:json --max-warnings 0` failed. That step prints nothing on failure, which is why the gate looked like a silent crash across the open PRs. Replaced with a GeminiRequestLike interface describing exactly what the three walkers traverse (contents[].parts[]), so the assertions keep their meaning and nothing is cast away. eslint on the file: clean. Suite: 6/6. Refs #9298 * docs(proxy): use an RFC 5737 documentation IP in the proxy examples The #9298 verdict headlines its docs failure with `L810 [stale-version] 1.2.3: const removed = await failOneproxyProxy("1.2.3.4", 8080)`. That is a false positive: check-deprecated-versions.mjs matches `/\bv?[12]\.\d+\.\d+\b/`, and the example IP literal 1.2.3.4 contains "1.2.3". Swapped both occurrences in PROXY_GUIDE.md (and its pl mirror) for 203.0.113.7, from the RFC 5737 documentation range that exists precisely for examples — it cannot collide with a version pattern and is the correct thing to print in docs regardless. Drift count 64 -> 62; no gate threshold was touched. The gate that actually FAILED under "Docs sync + fabricated-docs (strict)" was check:fabricated-docs (the invented pool env vars), fixed in the previous commit; this one removes the misleading line the verdict quotes. * test(base): allowlist probeUtils and realign the #7849 suite to the replacement bound Two more base-reds, both visible only after the migration collision stopped killing the shards. **check-db-rules — src/lib/db/probeUtils.ts not classified** #9541 added probeUtils.ts (transient-error retry for the SQLite corruption probe). It is imported ONLY by src/lib/db/core.ts, exactly like its siblings schemaColumns / optimizationSettings / providerNodeSelect, so re-exporting it through localDb.ts would push callers toward the barrel-import anti-pattern the gate exists to prevent. Added to INTENTIONALLY_INTERNAL with that rationale. check-db-rules 22/22, check:db-rules exit 0. **session-dedup-memory-7849 — pinned a mechanism that was replaced**7f36b192f0(#7855 follow-up) swapped the shared "suffix work budget" for the MAX_SUFFIX_STARTS / MAX_TOTAL_BLOCK_BYTES guards and deleted both the budget and its SUFFIX_WORK_BUDGET_WARNING string. It updated session-dedup.test.ts but not this sibling, so 3 of its 4 cases asserted a warning that can no longer be emitted. Realigned to the contract that actually survives — which is the invariant #7849 was opened for, not the mechanism: - the pathological pair must stay BOUNDED (completes in <4s, body intact) — measured at ~280ms on the current guards; - it must FAIL OPEN — original body returned by identity, compressed false, stats null (the explanatory zero-savings stats belonged to the removed budget path, which skipped before producing any); - the 512 MiB child fixture must still exit 0 with the full engine chain (session-dedup, lite, rtk, headroom, caveman) — that IS the OOM guard — and session-dedup must still report its skip, now pinned by prefix since the reason string moved with the mechanism. No threshold was loosened and no case was deleted: 4/4 here, 8/8 on the sibling session-dedup.test.ts. Refs #9298 * docs(mcp): bump the tool count to 105 and realign two vitest count pins Three more base-reds from the same 08-06 batch, all count/contract drift that the merged PRs left in sibling files. **Docs Gates (fast-path) — 3 STRICT drifts** check:docs-counts measures the MCP tool set from live code: it is 105 now (#8925 added omniroute_create_combo), while README.md, AGENTS.md and docs/frameworks/MCP-SERVER.md still claimed 104. Updated all five occurrences (two of them inside SVG alt text). check:docs-all exits 0. **Vitest (fast-path) — 2 failures** - open-sse/mcp-server/__tests__/essentialTools.test.ts pinned 11 phase-1 tools; #8925 shipped omniroute_create_combo as phase 1, making it 12. Verified by enumerating MCP_ESSENTIAL_TOOLS directly. - tests/unit/autoCombo/provider-family-combos.test.ts pinned the auto/glm provider set to [auggie, glm, zai]. #8914 (Devin ACP bridge) added devin-cli-agentic, whose catalog (registry/devin/catalog.ts:90-93) advertises the glm-5-2* line — so it belongs in the family pool for exactly the reason the test's own comment gives for auggie: a no-auth backend that genuinely serves a family model is a legitimate member. Expected set updated, invariant unchanged. npm run test:vitest 36/36 files, 340/340 tests. Refs #9298 * fix(combo,usage,oauth): drain the base-reds the shard fix exposed With the migration collision and the broken import out of the way the four unit shards actually run, and a further layer of base-reds became visible on the pure tip9995bc4893. Three are production defects. **Production defects** - open-sse/services/combo/runtimeUnitCapacity.ts:58 called resolveComboTargets() WITHOUT the hidden-model snapshot, so it fell back to the default getHiddenModelsByProvider() — a fresh full key_value read PER nested combo-ref unit, on every request. #8878 threaded the snapshot through the other call sites and missed this one. Threaded it from executeRuntimeUnitCombo (and from the dispatchPrelude call site), restoring the one-snapshot-per-request invariant combo-hidden-leaf-routing.test.ts pins. 9/9. - open-sse/services/usage/firecrawl.ts silently ignored its own `apiKey` parameter:91bb6aa619moved the fetch to fetchFirecrawlQuota(connectionId, connection), which reads the key off the connection record, so any caller passing the key directly got "Firecrawl API key not available". The explicit key is now merged into the connection passed down. firecrawl-usage 8/8. - src/lib/oauth/constants/oauth.ts was missing a RAYCAST entry in PROVIDERS while src/lib/oauth/providers/index.ts registers `raycast` (#8895), so every consumer reading PROVIDERS did not know Raycast Pro exists. Also added its OAUTH_TEST_CONFIG entry (checkExpiry only — it is an `import_token` provider with refreshToken always null), which #8408's guard explicitly requires rather than grandfathering. oauth-providers-config 25/25, oauth-test-config-8408 2/2. **Count / contract drift from the same batch** - feature flags 45 -> 46, APIKEY_PROVIDERS 197 -> 198 (Raycast Pro #8895), unique MCP tools 107 -> 108. Each re-derived from the source of truth. - vi + pt-BR locales: translated the 8 keys #9415 added (providers.newApiAggregator* and providers.modelTestQuotaTooltip) instead of relaxing the parity guard. i18n-vi 5/5, i18n-pt-br 3/3. - login-bootstrap-route: #9491 added `authenticated` to the require-login payload so /login can redirect an active session; the three deepEqual bodies now carry it. 10/10. **Flaky-by-construction, made deterministic** tests/unit/chat-combo-live-test.test.ts asserted the early-keepalive frame with a 100ms mocked upstream while resolveKeepaliveThreshold() is 2000ms for openai/*. It only ever passed while unrelated handler latency happened to push the total past the threshold — incidental, not deterministic, and it stopped holding once the handler got faster. The mock now sleeps 2400ms so the slow path is guaranteed and the assertion means what it says. 5/5. typecheck:core exit 0. check:file-size (base-relative) OK. Refs #9298 * test(base): run the orphaned #8890 suite and realign three mechanism pins **check:test-discovery — a suite that had NEVER executed** #8890 landed open-sse/services/__tests__/fail-fast-concurrency-gate.test.ts into a directory no runner collects (only one explicit file from that folder is in vitest.mcp.config.ts), so it ran zero times since it merged. Wired it into the runner AND into check-test-discovery.mjs's mirrored collector list, which the gate keeps in sync deliberately. It passes 4/4 now that it actually runs — test:vitest goes 36 -> 37 files, 340 -> 344 tests. **check-db-rules-classification** — 37 -> 38 audited modules, adding probeUtils alongside the INTENTIONALLY_INTERNAL entry from the previous commit. **ratelimit-reservoir-refresh** — #9604 (rolling RPM leases) DELETED Bottleneck's fixed-window reservoir, so currentReservoir() is null and the poll for `reservoir === 2` could never settle. It updated several sibling suites but not this one. The pin on the removed mechanism is gone; what remains is the invariant the original Bottleneck heartbeat bug actually broke and that #9529 opened this test for — after a header-learned updateSettings() the limiter must keep admitting work, proven by racing a post-exhaustion request against a 5s timer. 1/1. **translator-openai-to-gemini** — #9568 (c9a3361e5a) made buildChangedToolNameMap emit IDENTITY entries too, because Gemini lowercases tool names in functionCall responses and the response translator needs a key to map them back. Any request carrying tools therefore carries `_toolNameMap` in the Antigravity envelope now. Expected key list updated and the map's contents asserted explicitly rather than left implicit. 45/45. Refs #9298 * fix(db): restore node-backed synced catalogs and realign the #8944 context hints **Production regression from #9294 (d69f521491)** lookupModelMeta moved from getSyncedAvailableModels(providerId) to getActiveSyncedCatalog(providerId). The new reader unions models only from rows in `provider_connections` with isActive = 1 — but a provider NODE lives in `provider_nodes` and NEVER has a connections row, so filtering by active connection ids silently dropped every node's synced catalog. The consequence was not just a missing list: lookupModelMeta reads that catalog for RUNTIME METADATA, so for openai-compatible nodes it took out - `supportedThinkingEfforts`, which is what splitSyncedEffortSuffix needs — so `<prefix>/<model>-high` stopped resolving to the base id and the effort was never derived (#7694), and - `contextWindow` / `maxInputTokens`, used by the combo context-window filter. getActiveSyncedCatalog now falls back to the provider-wide key_value set — the exact pre-#9294 source — when no active connection carries a catalog, and marks that fallback explicitly NON-authoritative. #9294's live-catalog gating is about what an active connection actually serves, so a node-backed catalog informs metadata while never being able to reject a model as unavailable. `available` therefore stays fail-open for nodes, as it was before. sync-reasoning-supported-efforts-7694 23/23 (was 21/2). live-model-catalog-reconciliation-8926 11/11 and combo-provider-wildcard 23/23 confirm #9294's own coverage is untouched. **#8944 sibling-test drift**714a315a1a("Treat context metadata as a routing hint") deliberately turned the context-window check from a HARD filter into an ordering hint: a catalog-too-small target is demoted, not removed, because a stale catalog entry must never delete the only target that could accept the request at runtime. The PR updated one case in this suite and left three asserting the old drop behaviour. Realigned to the new contract — the too-small target must lose the ordering to the fitting one while remaining present — and renamed them from "still rejects"/"still dropped" to "is demoted"/"ordered last" so the names stop describing the removed behaviour. 14/14. **file-size** tests/unit/translator-openai-to-gemini.test.ts testFrozen 1616 -> 1619: the frozen value sat exactly at the base size, so the 3 lines the previous commit's _toolNameMap alignment needs could not fit. Justified in the baseline. typecheck:core exit 0. Refs #9298 * chore(stryker): register the two covering suites missing from tap.testFiles check:mutation-test-coverage flags any unit test that covers a mutated module but is absent from stryker.conf.json tap.testFiles — without the entry its mutant kills do not count toward the module's score. - tests/unit/antigravity-prefer-stored-project.test.ts covers open-sse/services/combo/quotaStrategies.ts (added earlier in this PR). - tests/unit/executor-devin-cli-agentic-acp.test.ts covers src/sse/services/auth.ts — pre-existing drift, same gate, same fix. Inserted in alphabetical position only; the rest of the file is byte-identical (it is not prettier-formatted upstream and reformatting it is out of scope here). Refs #9298 * fix(db): drop the never-wired getSessionModelUsageCounts (knip regression) The dead-code ratchet only ran once the earlier Fast Quality Gates steps stopped failing, and it lands at 228 vs baseline 227. The extra symbol is src/lib/db/contextHandoffs.ts::getSessionModelUsageCounts, added by #8894 "for least-used strategy" and never wired: the least-used branch in applyStrategyOrdering.ts uses the pre-existing sortTargetsByUsage(), and the helper has no caller in src/, open-sse/ or tests/. It is the same incomplete-PR shape as that PR's import of a module which does not exist in the repo (fixed earlier in this branch). Removed rather than baselined — bumping the ratchet would loosen the gate, and removal is exactly the remedy the gate prescribes. Same treatment the Dario installer's never-wired uninstall() got in #9600. The implementation is recoverable froma598fbb090whenever someone actually wires a session-aware least-used strategy. check:dead-code 228 -> 227 (baseline untouched). check:db-rules exit 0. context-handoff 13/13, db-context-handoffs 7/7, service-context-handoff 11/11. Refs #9298 * fix(security): embed the Raycast signature secret via resolvePublicCred (HR#11) The secret-scan ratchet only ran once the earlier Fast Quality Gates steps stopped failing, and it lands at 1 finding vs baseline 0. The finding is open-sse/services/raycast.ts:19 — RAYCAST_DEFAULT_SIG_SECRET, a 64-hex request-signature secret that #8895 committed as a bare string literal. It is genuinely public (community-extracted from the Raycast macOS client; the SAME value ships to every install, it is not a per-user credential), which is exactly the category Hard Rule #11 governs: public upstream credentials MUST go through resolvePublicCred() (open-sse/utils/publicCreds.ts), never a literal — see docs/security/PUBLIC_CREDS.md. So the fix is the mandated pattern, not a .gitleaks.toml allowlist entry: added `raycast_sig_secret` to EMBEDDED_DEFAULTS as the XOR-masked byte sequence and resolved it with the existing RAYCAST_SIG_SECRET env override. The providerSpecificData.sigSecret override is untouched. Verified the decoded value is byte-identical to the literal it replaces. check:secrets secretFindings 1 -> 0. check:public-creds exit 0. publicCreds 12/12, raycast-auth 6/6, raycast-local-extract 1/1, trae-publiccred 3/3. typecheck:core exit 0. Refs #9298 --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
33 KiB
title, version, lastUpdated
| title | version | lastUpdated |
|---|---|---|
| 🌐 OmniRoute Proxy Guide | 3.8.40 | 2026-06-28 |
🌐 OmniRoute Proxy Guide
Bypass geographic blocks, protect your identity, and route AI traffic through any proxy — with zero configuration complexity.
OmniRoute includes a full-featured proxy management system that lets you route upstream AI provider traffic through HTTP, HTTPS, or SOCKS5 proxies. Whether you're in a blocked region, need IP rotation, or want stealth fingerprinting — this guide covers everything.
Table of Contents
- Why Use Proxies?
- Architecture Overview
- 4-Level Proxy System
- Proxy Registry (CRUD)
- 1proxy Free Marketplace
- Proxy Rotation
- Anti-Detection & Stealth
- Upstream Proxy Modes
- Dashboard UI
- API Reference
- Environment Variables
- Troubleshooting
Why Use Proxies?
Many AI providers restrict access by geographic region. Developers in Russia, China, Iran, Cuba, Turkey, and other countries encounter errors like:
unsupported_country_region_territory
Even outside blocked regions, proxies are useful for:
| Use Case | Description |
|---|---|
| Geographic bypass | Access OpenAI, Anthropic, Codex, Copilot from blocked countries |
| IP rotation | Distribute requests across multiple IPs to avoid rate limiting |
| Privacy | Hide your real IP from upstream providers |
| Compliance | Route traffic through specific jurisdictions |
| Testing | Simulate requests from different regions |
Architecture Overview
┌───────────────────────────────────────────────────────────────┐
│ OmniRoute Server │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Proxy │ │ Proxy │ │ Proxy │ │
│ │ Registry │───▶│ Dispatcher │───▶│ Fetch (undici) │ │
│ │ (SQLite) │ │ (cached) │ │ │ │
│ └─────────────┘ └──────────────┘ └────────┬─────────┘ │
│ ▲ │ │
│ │ ▼ │
│ ┌──────┴──────┐ ┌──────────────────┐ │
│ │ 1proxy Sync │ │ Upstream │ │
│ │ (free pool) │ │ Provider API │ │
│ └─────────────┘ └──────────────────┘ │
└───────────────────────────────────────────────────────────────┘
Key Components
| Component | File | Role |
|---|---|---|
| Proxy Registry | src/lib/db/proxies.ts |
CRUD for proxy entries + scope assignments |
| Proxy Dispatcher | open-sse/utils/proxyDispatcher.ts |
Creates undici ProxyAgent/SOCKS dispatchers with caching |
| Proxy Fetch | open-sse/utils/proxyFetch.ts |
Wraps fetch() with proxy dispatcher injection |
| Settings Route | src/app/api/settings/proxy/route.ts |
Legacy proxy config API (GET/PUT/DELETE) |
| Management Route | src/app/api/v1/management/proxies/route.ts |
Registry CRUD API (GET/POST/PATCH/DELETE) |
| 1proxy DB | src/lib/db/oneproxy.ts |
Free proxy marketplace persistence |
| 1proxy Sync | src/lib/oneproxySync.ts |
Fetches proxies from 1proxy API |
| 1proxy Rotator | src/lib/oneproxyRotator.ts |
Rotation strategies (quality/random/sequential) |
4-Level Proxy System
OmniRoute supports proxy configuration at four independent scopes, resolved in priority order:
Priority Resolution Order (highest → lowest):
1. 🔵 Account/Connection Proxy → per API key / OAuth connection
2. 🟡 Provider Proxy → per provider (e.g., all OpenAI traffic)
3. 🟠 Combo Proxy → per combo/routing configuration
4. 🟢 Global Proxy → all traffic, all providers
How Resolution Works
When OmniRoute sends a request to an upstream provider, it calls resolveProxyForConnectionFromRegistry() which checks each level in order:
- Account-level — Is there a proxy assigned to this specific connection ID?
- Provider-level — Is there a proxy assigned to this provider (e.g.,
openai)? - Global-level — Is there a global proxy configured?
- No proxy — Direct connection to the provider.
The first match wins. This means you can set a global proxy as a fallback but override it for specific providers or connections.
What Gets Proxied
| Traffic Type | Proxied? | Notes |
|---|---|---|
| Chat completions | ✅ | All /v1/chat/completions requests |
| Embeddings | ✅ | /v1/embeddings |
| Image generation | ✅ | /v1/images/generations |
| Audio (TTS/STT) | ✅ | /v1/audio/* |
| OAuth token exchange | ✅ | Solves unsupported_country_region_territory |
| Connection tests | ✅ | "Test Connection" button uses proxy |
| Token refresh | ✅ | Background OAuth renewal |
| Model sync | ✅ | Model listing and discovery |
Proxy Registry (CRUD)
The proxy registry is a SQLite table (proxy_registry) that stores all your proxies. Each proxy has:
| Field | Type | Description |
|---|---|---|
id |
UUID | Unique identifier |
name |
String | Human-readable label |
type |
String | Protocol: http, https, socks5 |
host |
String | Proxy hostname or IP |
port |
Integer | Port number |
username |
String | Auth username (encrypted at rest) |
password |
String | Auth password (encrypted at rest) |
region |
String | Geographic region label |
notes |
String | Free-text notes |
status |
String | active or inactive |
source |
String | manual or oneproxy |
Creating a Proxy
Via Dashboard:
- Go to Settings → Proxy
- Click Add Proxy
- Fill in the type, host, port, and optional auth credentials
- Save
Via API:
curl -X POST http://localhost:20128/api/v1/management/proxies \
-H "Content-Type: application/json" \
-d '{
"name": "US Proxy",
"type": "http",
"host": "proxy.example.com",
"port": 8080,
"username": "user",
"password": "pass",
"region": "US"
}'
Updating a Proxy
curl -X PATCH http://localhost:20128/api/v1/management/proxies \
-H "Content-Type: application/json" \
-d '{
"id": "proxy-uuid-here",
"host": "new-proxy.example.com",
"port": 9090
}'
Note: Credentials are preserved unless you explicitly send non-empty replacements. Sending empty strings for
username/passwordwill keep the stored values.
Deleting a Proxy
# Fails if proxy is assigned to any scope
curl -X DELETE "http://localhost:20128/api/v1/management/proxies?id=proxy-uuid"
# Force delete (removes assignments too)
curl -X DELETE "http://localhost:20128/api/v1/management/proxies?id=proxy-uuid&force=1"
Listing Proxies
curl "http://localhost:20128/api/v1/management/proxies?limit=50&offset=0"
Assigning Proxies to Scopes
# Assign to global scope
curl -X PUT http://localhost:20128/api/settings/proxy \
-H "Content-Type: application/json" \
-d '{"level": "global", "proxy": {"type":"http","host":"proxy.example.com","port":8080}}'
# Assign to a specific provider
curl -X PUT http://localhost:20128/api/settings/proxy \
-H "Content-Type: application/json" \
-d '{"level": "provider", "id": "openai", "proxy": {"type":"socks5","host":"socks.example.com","port":1080}}'
# Assign to a specific connection/key
curl -X PUT http://localhost:20128/api/settings/proxy \
-H "Content-Type: application/json" \
-d '{"level": "key", "id": "connection-uuid", "proxy": {"type":"http","host":"key-proxy.com","port":3128}}'
Resolving Effective Proxy
Check which proxy would be used for a given connection:
curl "http://localhost:20128/api/settings/proxy?resolve=connection-uuid"
Returns the resolved proxy with its level (account, provider, or global) and source.
Bulk Assignment
Assign one proxy to multiple providers or connections at once:
curl -X POST http://localhost:20128/api/v1/management/proxies/bulk-assign \
-H "Content-Type: application/json" \
-d '{
"scope": "provider",
"scopeIds": ["openai", "anthropic", "codex"],
"proxyId": "proxy-uuid"
}'
Import/Export
Proxies are included in the Backup/Restore system. When you export your OmniRoute configuration:
- Go to Dashboard → Settings → Backup
- Click Export — proxy registry and assignments are included
- To restore, click Import and upload the backup file
The proxy registry also supports upsert by host+port — if you import a proxy that already exists (same host and port), it updates instead of creating a duplicate.
Legacy Migration
If you configured proxies in an older version (pre-registry), OmniRoute automatically migrates them:
Legacy key_value store → proxy_registry + proxy_assignments
This happens once on first startup after upgrade. Use migrateLegacyProxyConfigToRegistry({ force: true }) to re-run.
1proxy Free Proxy Marketplace
OmniRoute integrates with the 1proxy community platform to provide access to hundreds of free, validated proxies from around the world. This is perfect for users who don't have their own proxy infrastructure.
How It Works
┌─────────────┐ Sync ┌─────────────────┐ Rotate ┌──────────┐
│ 1proxy API │ ────────────▶ │ proxy_registry │ ────────────▶ │ Provider │
│ (external) │ up to 500 │ source=oneproxy │ by quality │ API │
└─────────────┘ proxies └─────────────────┘ └──────────┘
- Sync — OmniRoute fetches validated proxies from the 1proxy API
- Store — Proxies are saved in the same
proxy_registrytable withsource = 'oneproxy' - Filter — Filter by protocol, country, quality score
- Rotate — Pick the best proxy using quality, random, or sequential strategies
- Auto-degrade — Failed proxies get their quality score reduced; below threshold → marked inactive
Syncing Proxies
Via Dashboard:
- Go to Settings → 1proxy tab
- Click "Sync Now"
- View stats: total proxies, active count, average quality, by-country breakdown
Via API:
# Trigger sync
curl -X POST http://localhost:20128/api/settings/oneproxy \
-H "Content-Type: application/json" \
-d '{}'
# Response:
# { "success": true, "added": 127, "updated": 45, "failed": 2, "total": 172 }
Filtering Proxies
# Filter by protocol
curl "http://localhost:20128/api/settings/oneproxy?protocol=socks5"
# Filter by country
curl "http://localhost:20128/api/settings/oneproxy?countryCode=US"
# Filter by minimum quality score
curl "http://localhost:20128/api/settings/oneproxy?minQuality=80"
# Combine filters
curl "http://localhost:20128/api/settings/oneproxy?protocol=http&countryCode=DE&minQuality=70"
Proxy Quality Scores
Each 1proxy proxy comes with metadata:
| Field | Description |
|---|---|
qualityScore |
0-100 rating from 1proxy validation |
latencyMs |
Measured network latency |
anonymity |
transparent, anonymous, or elite |
googleAccess |
Whether the proxy can access Google services |
countryCode |
Two-letter ISO country code |
lastValidated |
Timestamp of last validation |
Quality scores are dynamically adjusted:
- Failed requests reduce the score by 10 points
- Score drops to ≤10 → proxy is marked
inactive - Inactive proxies are excluded from rotation
Rotation Strategies
# Rotate by quality (best proxy first) — default
curl -X POST http://localhost:20128/api/settings/oneproxy/rotate \
-H "Content-Type: application/json" \
-d '{"strategy": "quality"}'
# Random rotation
curl -X POST http://localhost:20128/api/settings/oneproxy/rotate \
-d '{"strategy": "random"}'
# Sequential (least recently validated first)
curl -X POST http://localhost:20128/api/settings/oneproxy/rotate \
-d '{"strategy": "sequential"}'
Circuit Breaker
The 1proxy sync has a built-in circuit breaker:
- After 5 consecutive sync failures, further sync attempts are blocked
- Reset with:
resetOneproxyCircuitBreaker()or restart the server - Sync status is available at
GET /api/settings/oneproxy?action=status
Clearing 1proxy Proxies
# Delete a single 1proxy proxy
curl -X DELETE "http://localhost:20128/api/settings/oneproxy?id=proxy-uuid"
# Clear ALL 1proxy proxies (manual proxies are untouched)
curl -X DELETE "http://localhost:20128/api/settings/oneproxy?clearAll=1"
Anti-Detection & Stealth
OmniRoute doesn't just route traffic through a proxy — it makes the traffic look legitimate:
TLS Fingerprint Spoofing
Uses wreq-js to generate browser-like TLS fingerprints, bypassing bot detection systems that flag non-browser TLS handshakes.
CLI Fingerprint Matching
The CLI Fingerprint Toggle (Settings → Security) reorders HTTP headers and JSON body fields to match the exact signature of native CLI binaries (Claude Code, Codex, etc.). This works on top of the proxy:
Your IP (blocked) → Proxy IP (US) → Provider API
+ TLS spoof
+ CLI fingerprint
You get both IP masking and request authenticity simultaneously.
Proxy IP Preservation
Color-coded badges in the dashboard show which proxy level is active:
| Badge | Level | Meaning |
|---|---|---|
| 🟢 | Global | All traffic goes through this proxy |
| 🟡 | Provider | Only this provider's traffic is proxied |
| 🔵 | Connection | This specific key/account uses this proxy |
The badge also shows the resolved proxy IP for verification.
Upstream Proxy Modes
For providers that use the CLIProxyAPI pattern, OmniRoute supports three upstream proxy modes:
| Mode | Description |
|---|---|
native |
OmniRoute handles proxy routing directly (default) |
cliproxyapi |
Delegates to an external CLIProxyAPI instance |
fallback |
Tries native first, falls back to CLIProxyAPI |
Configure per-provider:
curl -X PUT "http://localhost:20128/api/upstream-proxy/openai" \
-H "Content-Type: application/json" \
-d '{"mode": "native", "enabled": true}'
Dashboard UI
Settings → Proxy Tab
- Global proxy configuration (set once for all traffic)
- Per-provider proxy overrides
- Per-connection proxy assignments
- Connection test through configured proxy
- Color-coded badges showing active proxy level
Settings → 1proxy Tab
- Sync Now button to fetch free proxies
- Stats cards: Total, Active, Avg Quality, Last Sync
- Filters: Protocol, Country Code, Min Quality
- Proxy table with host, protocol, country, quality score, latency, anonymity, Google access
- Sync status panel with success/failure tracking and consecutive failure count
- Clear All to remove all 1proxy entries
API Reference
Proxy Settings API
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/settings/proxy |
Get full proxy config |
GET |
/api/settings/proxy?level=global |
Get global proxy |
GET |
/api/settings/proxy?level=provider&id=openai |
Get provider proxy |
GET |
/api/settings/proxy?resolve=connectionId |
Resolve effective proxy |
PUT |
/api/settings/proxy |
Update proxy config |
DELETE |
/api/settings/proxy?level=provider&id=openai |
Remove proxy at level |
Proxy Registry API
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/management/proxies |
List all proxies |
GET |
/api/v1/management/proxies?id=uuid |
Get proxy by ID |
GET |
/api/v1/management/proxies?id=uuid&where_used=1 |
Get proxy assignments |
POST |
/api/v1/management/proxies |
Create proxy |
PATCH |
/api/v1/management/proxies |
Update proxy |
DELETE |
/api/v1/management/proxies?id=uuid |
Delete proxy |
DELETE |
/api/v1/management/proxies?id=uuid&force=1 |
Force delete |
POST |
/api/v1/management/proxies/bulk-assign |
Bulk assign |
GET |
/api/v1/management/proxies/assignments |
List assignments |
GET |
/api/v1/management/proxies/health |
Proxy health stats |
Tunnels API
For exposing your OmniRoute instance to the public internet (Cloudflare/ngrok/Tailscale) instead of routing outbound through a proxy, see TUNNELS_GUIDE.md. The tunnel REST API lives under /api/tunnels/{cloudflared,ngrok,tailscale}/* and is orthogonal to the outbound proxy chain documented above.
1proxy API
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/settings/oneproxy |
List 1proxy proxies |
GET |
/api/settings/oneproxy?action=stats |
Get stats + sync status |
GET |
/api/settings/oneproxy?action=status |
Get sync status only |
POST |
/api/settings/oneproxy |
Trigger sync |
POST |
/api/settings/oneproxy/rotate |
Rotate to next proxy |
DELETE |
/api/settings/oneproxy?id=uuid |
Delete one |
DELETE |
/api/settings/oneproxy?clearAll=1 |
Clear all |
Upstream Proxy API
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/upstream-proxy/:providerId |
Get upstream proxy config |
PUT |
/api/upstream-proxy/:providerId |
Set upstream proxy mode |
DELETE |
/api/upstream-proxy/:providerId |
Remove upstream proxy config |
Environment Variables
| Variable | Default | Description |
|---|---|---|
ENABLE_SOCKS5_PROXY |
true |
Enable SOCKS5 proxy support (default true in .env.example) |
ONEPROXY_ENABLED |
true |
Enable 1proxy integration |
ONEPROXY_API_URL |
https://1proxy-api.aitradepulse.com |
1proxy API endpoint |
ONEPROXY_MAX_PROXIES |
500 |
Maximum proxies to sync |
ONEPROXY_MIN_QUALITY_THRESHOLD |
50 |
Minimum quality score to import |
Troubleshooting
"SOCKS5 proxy is disabled"
Set ENABLE_SOCKS5_PROXY=true in your .env file and restart.
"socket hang up" errors through proxy
This is normal with cheap proxies that drop idle connections. OmniRoute already handles this by:
- Disabling keep-alive on proxy connections (
keepAliveTimeout: 1) - Disabling pipelining (
pipelining: 0) - Caching dispatchers to avoid repeated handshakes
If it persists, try a different proxy or use the 1proxy rotation feature.
"unsupported_country_region_territory" during OAuth
Make sure the proxy is configured before starting the OAuth flow. OmniRoute routes OAuth token exchange through the configured proxy. Set a global or provider-level proxy first, then connect.
Proxy not being used
Check the resolution order:
- Verify with
GET /api/settings/proxy?resolve=your-connection-id - Check if the proxy
statusisactive(notinactive) - Ensure the proxy assignment scope matches your connection
1proxy sync failing
Check the sync status:
curl "http://localhost:20128/api/settings/oneproxy?action=status"
If consecutiveFailures >= 5, the circuit breaker has tripped. Restart the server to reset, or wait for manual reset.
Database Schema
proxy_registry Table
CREATE TABLE proxy_registry (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'http',
host TEXT NOT NULL,
port INTEGER NOT NULL,
username TEXT DEFAULT '',
password TEXT DEFAULT '',
region TEXT,
notes TEXT,
status TEXT DEFAULT 'active',
source TEXT NOT NULL DEFAULT 'manual', -- 'manual' or 'oneproxy'
quality_score INTEGER, -- 0-100 (1proxy only)
latency_ms INTEGER, -- milliseconds (1proxy only)
anonymity TEXT, -- transparent/anonymous/elite
google_access INTEGER DEFAULT 0, -- can access Google? (1proxy)
last_validated TEXT, -- ISO timestamp (1proxy)
country_code TEXT, -- ISO 2-letter code (1proxy)
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
proxy_assignments Table
CREATE TABLE proxy_assignments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
proxy_id TEXT NOT NULL REFERENCES proxy_registry(id),
scope TEXT NOT NULL, -- 'global', 'provider', 'account', 'combo'
scope_id TEXT, -- provider ID, connection ID, or combo ID
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(scope, scope_id)
);
Proxy Health Checking (v3.8.16+)
OmniRoute's proxy fast-fail mechanism (src/lib/proxyHealth.ts) detects dead proxies in <2s via a quick TCP connection check, then caches the result to avoid per-request overhead.
How It Works
Request ──▶ ProxyHealthCache.get(url)
│
├─ Cache hit + fresh? ──▶ return cached status
│
└─ Cache miss / stale? ──▶ TCP connect to host:port
(timeout: FAST_FAIL_TIMEOUT_MS)
──▶ cache for HEALTH_CACHE_TTL_MS
──▶ return result
Without this, a dead proxy would block every request for the full PROXY_TIMEOUT_MS (default 30s) before failing.
Tunable Environment Variables
| Variable | Default | Purpose |
|---|---|---|
PROXY_FAST_FAIL_TIMEOUT_MS |
2000 |
TCP connection timeout per health check |
PROXY_HEALTH_CACHE_TTL_MS |
30000 |
How long a health result is cached |
Recommended values:
| Scenario | Fast-fail timeout | Cache TTL | Reasoning |
|---|---|---|---|
| High-throughput API gateway | 1500ms | 60000ms | Aggressive fail-fast, longer cache to reduce checks |
| Geo-distributed nodes | 3000ms | 15000ms | Slower networks need more time; shorter cache for fast failover |
| Dev / testing | 1000ms | 10000ms | Quick iteration on local proxies |
| Stealth / anti-detection | 2500ms | 45000ms | Avoid rapid probing that could trigger rate limits |
Inspecting Proxy Health
import { getAllProxyHealthStatuses, invalidateProxyHealth } from "omniroute/proxyHealth";
const statuses = getAllProxyHealthStatuses();
for (const s of statuses) {
console.log(`${s.proxyUrl} → healthy=${s.healthy}, stale=${s.stale}`);
}
// Force re-check a specific proxy
invalidateProxyHealth("http://user:pass@203.0.113.7:8080");
The stale flag is true when the cache entry has exceeded HEALTH_CACHE_TTL_MS and the next request will trigger a fresh check.
Per-Proxy Type Defaults
The health check uses sensible defaults based on the URL scheme:
| Scheme | Default port |
|---|---|
http:// |
8080 |
https:// |
443 |
socks5:// / socks5h:// |
1080 |
Custom ports in the URL (http://host:9999) always take precedence over the scheme default.
Proxy Analytics & Observability
OmniRoute tracks per-proxy usage to help operators diagnose routing patterns, latency spikes, and recurring failures.
What's Tracked
For every request through a configured proxy, OmniRoute records:
| Metric | Description |
|---|---|
proxy_url |
Full proxy URL (with auth credentials masked) |
provider |
Upstream provider ID (openai, anthropic, etc.) |
latency_ms |
Total round-trip time including proxy handshake |
connect_ms |
TCP connect time only |
status |
HTTP status code from upstream |
error |
Error class if request failed |
timestamp |
ISO 8601 UTC |
Accessing the Data
# Recent proxy events
curl -H "Authorization: Bearer $OMNIROUTE_KEY" \
"http://localhost:20128/api/usage/proxy-logs?limit=100"
The real endpoint is /api/usage/proxy-logs (see src/app/api/usage/proxy-logs/route.ts). This endpoint supports:
GET /api/usage/proxy-logs— retrieve proxy logsDELETE /api/usage/proxy-logs— clear all proxy logs
Aggregate stats can be queried directly from the proxy_logs table via SQL if needed. The dashboard UI may offer aggregate views.
Common Patterns
Detect a flapping proxy (alternates between success/failure):
SELECT proxy_url,
COUNT(*) AS total,
SUM(CASE WHEN status >= 500 THEN 1 ELSE 0 END) AS errors,
ROUND(100.0 * SUM(CASE WHEN status >= 500 THEN 1 ELSE 0 END) / COUNT(*), 1) AS error_pct
FROM proxy_logs
WHERE timestamp > datetime('now', '-1 hour')
GROUP BY proxy_url
HAVING error_pct > 5
ORDER BY error_pct DESC;
Find slow proxies (p95 latency > 2s):
WITH ranked AS (
SELECT proxy_url, latency_ms,
PERCENT_RANK() OVER (PARTITION BY proxy_url ORDER BY latency_ms) AS pct
FROM proxy_logs
WHERE timestamp > datetime('now', '-24 hour')
)
SELECT proxy_url, latency_ms
FROM ranked
WHERE pct >= 0.95
ORDER BY latency_ms DESC;
Rotation Strategy Decision Tree
When multiple proxies are assigned to a scope, OmniRoute uses a rotation strategy to pick which one to use for each request. The strategy is configured at the scope level (global, per-provider, per-account, per-combo).
Available Strategies
| Strategy | When to use | Trade-off |
|---|---|---|
quality (default) |
Production with mixed-quality proxies | Favors high-rated proxies; may starve low-rated ones |
random |
Load distribution, privacy | Even distribution; ignores quality signals |
sequential |
Debugging, deterministic testing | Cycles through proxies in order; easy to reason about |
Decision Tree
Do you have quality scores for your proxies?
│
┌───────────┴───────────┐
│ │
YES NO
│ │
Are all proxies │
roughly equal │
in quality? │
│ │
┌────┴────┐ │
│ │ │
YES NO Use
│ │ `random`
│ │ (even spread
│ │ builds quality
│ │ data over time)
│ │
│ Use `quality`
│ (best for
│ mixed quality)
│
Use `random`
(spread load
evenly)
Configuring Rotation Strategy
import { rotateOneproxyProxy } from "omniroute/oneproxyRotator";
// In a one-off script
const proxy = await rotateOneproxyProxy({ strategy: "quality" });
if (proxy) {
console.log(`Selected: ${proxy.host}:${proxy.port}, quality=${proxy.qualityScore}`);
}
Resetting Sequential Index
When using sequential strategy, the internal index accumulates. To reset:
import { resetSequentialIndex } from "omniroute/oneproxyRotator";
resetSequentialIndex();
Useful when:
- Restarting a load test
- Recovering from a proxy outage (so you don't cycle through dead ones first)
- Manually rebalancing after adding new proxies
Marking a Proxy as Failed
When a proxy consistently fails, mark it manually so the rotator will skip it:
import { failOneproxyProxy } from "omniroute/oneproxyRotator";
const removed = await failOneproxyProxy("203.0.113.7", 8080);
if (removed) {
console.log("Proxy marked as failed; rotator will skip it");
}
The proxy is not deleted — it's marked unhealthy and won't be selected until the next successful health check (via proxyHealth.ts) or manual reset.
📖 Related documentation:
- User Guide — General setup and configuration
- API Reference — Full API documentation
- Environment Config — All environment variables