* chore(release): open v3.8.38 development cycle
* fix(executors): strip client_metadata for cerebras and mistral (#4727)
Integrated into release/v3.8.38 (leva 5)
* fix(codebuddy): only send reasoning params when client requests reasoning (#5019)
Integrated into release/v3.8.38 (leva 5)
* fix(sse): keep streaming for forceStream providers when client requests JSON (#5021)
Integrated into release/v3.8.38 (leva 5)
* fix(sse): guard non-JSON SSE lines and duplicate [DONE] (#4937)
Integrated into release/v3.8.38 (leva 5)
* feat(blackbox): refresh provider model catalog (#4935)
Integrated into release/v3.8.38 (leva 5)
* fix(sse): dedupe case-variant Anthropic version/beta headers (#4846)
Integrated into release/v3.8.38 (leva 5)
* feat(sse): Kiro inline <thinking> stream splitter (#4911)
Integrated into release/v3.8.38 (leva 5)
* feat(cursor): parse Composer DeepSeek-style inline tool calls (#4912)
Integrated into release/v3.8.38 (leva 5)
* feat(proxy): auth-less host:port batch import (#4938)
Integrated into release/v3.8.38 (leva 5)
* fix(oauth): support Kiro IDC (organization) token import (#4944)
Integrated into release/v3.8.38 (leva 5)
* fix(translator): preserve cache_control for DashScope OpenAI-compat providers (port from 9router#2069) (#5013)
Integrated into release/v3.8.38 (leva 5)
* fix(tts): resolve Gemini TTS models from catalog (#4934)
Integrated into release/v3.8.38 (leva 5)
* fix(sse): don't cool down the connection on a self-inflicted upstream timeout (504) (#5064)
Integrated into release/v3.8.38 (leva 5)
* fix(sse): robust Anthropic /v1/messages streaming — real ping keepalive + client-disconnect guard (#5063)
Integrated into release/v3.8.38 (leva 5)
* feat(video): add Alibaba DashScope (wan2.7-t2v) provider (#5051)
Integrated into release/v3.8.38 (leva 5)
* fix: preserve model hidden flags (isHidden) across model sync (#5086)
Integrated into release/v3.8.38 (leva 5)
* fix(models): derive model discovery config from registry modelsUrl (#5087)
Integrated into release/v3.8.38 (leva 5)
* fix(compression): replace fileURLToPath(import.meta.url) with runtime anchors for standalone bundle (#5089)
Integrated into release/v3.8.38 (leva 5)
* feat(cc): add summarized thinking display toggle (#5055)
Integrated into release/v3.8.38 (leva 5)
* Harden selected API error responses (#5032)
Integrated into release/v3.8.38 (leva 5)
* chore(quality): rebaseline file-size for leva 5 PR batch drift
6 frozen files grew from merged leva-5 PRs (cursor #4912, kiro #4911,
videoGeneration #5051, default #4727, base #4846, chat #5064); all covered
by per-PR tests. See _rebaseline_2026_06_26_leva5 in the baseline.
* feat(compression): compression playground (Play + Compare tabs) in the studio (#5080)
Integrated into release/v3.8.38
* fix(combo): fail over on empty-content 502 instead of exhausting the provider (#5085) (#5104)
* fix(dashboard): surface detailed credential-validation error in add-connection modal (#5088) (#5106)
* feat(providers): allow local/private provider URLs by default with scoped metadata-safe guard (#5066) (#5107)
* fix(diagnostics): treat non-streaming Claude messages shape as valid output (#5108) (#5116)
* fix(db): translate pt-BR SQLite driver-fallback log lines to English (#5103) (#5115)
* fix(sse): repair release base-reds — malformed-response false positives + header casing + stale tests (#5117)
Repairs the release/v3.8.38 base-reds; unblocks #5078.
* chore(quality): rebaseline file-size for responseSanitizer (#5117) + AddApiKeyModal drift
* fix(translator): forward image tool_result blocks as image_url (#5100)
Base-reds fixed (#5117); image tool_result→image_url. Integrated into release/v3.8.38.
* fix(responses): default text.format for openai-compatible responses providers (#5101)
Base-reds fixed (#5117); default text.format + file-size rebaseline. Integrated into release/v3.8.38.
* feat(dashboard): expose Fusion judgeModel + fusionTuning in the combo editor (#5074)
Base-reds fixed (#5117); Fusion editor + file-size rebaseline. Integrated into release/v3.8.38.
* feat(quota): add opt-in Codex/Claude auto-ping keepalive (#5102)
Base-reds fixed (#5117); auto-ping keepalive + file-size rebaseline. Integrated into release/v3.8.38.
* test(release): relocate 2 orphan test files into the collected flat tests/unit dir (#5120)
Unblocks Lint (test-discovery) on #5078. Integrated into release/v3.8.38.
* fix(translator): preserve reasoning-replay reasoning_content + repair 3 release-green test reds (#5122)
Repairs 3 release-green test reds + test-masking; unblocks #5078.
* test(golden): redact live Node version from provider translate-path snapshot (#5125)
Final golden unblock for #5078.
* test(golden): redact OmniRoute app version from translate-path snapshot (#5126)
Coverage shard golden unblock for #5078.
* Ignore disconnect races during in-band stream error handling (#5007)
Integrated into release/v3.8.38
* Track final connection IDs in failover logs (#5016)
Integrated into release/v3.8.38
* fix(sse): convert Gemini body to OpenAI format in antigravity MITM handler (#4845)
Integrated into release/v3.8.38 (rebased on tip, CHANGELOG re-injected)
* feat(providers): add ZenMux Free session-cookie provider (#5105)
Integrated into release/v3.8.38 (rebased on tip, CHANGELOG re-injected)
* feat(dashboard): click-to-edit model alias in provider page (#5119)
Integrated into release/v3.8.38 (rebased on tip, i18n scope verified, CHANGELOG re-injected)
* feat(mcp): web-session robustness — cookie dedup (PR6) + browser-pool observability (PR7) (#3368) (#5121)
Integrated into release/v3.8.38 (rebased on tip; cookie-dedup branch extracted to findExistingCookieConnection helper → complexity-neutral; CHANGELOG added)
* fix(usage): dedupe request-usage logging and debounce stats (#4940)
Integrated into release/v3.8.38 (rebased on tip; DB-handle hang was stale-base artifact — resetDbInstance already closes the handle, test green 5/5; file-size drift consolidated at release; CHANGELOG re-injected)
* fix(dashboard): key model visibility toggle on canonical providerId (#5091)
Integrated into release/v3.8.38 (retargeted main→release; .tsx visibility-key test green 2/2)
* chore(deps): bump actions/cache from 5.0.5 to 6.0.0 (#5112)
Integrated into release/v3.8.38 (retargeted main→release; workflow-only actions/cache bump — unit failures were stale main base-reds)
* fix(streaming): harden long OpenAI-compatible SSE streams (#5124)
Integrated into release/v3.8.38 (rebased on tip; streamHandler conflict with #5007 disconnect-guard resolved — both coexist, stream-handler 22/22 green)
* feat: Add Grok Build (xAI) provider with OAuth import-token flow (#5020)
Integrated into release/v3.8.38 (rebased on tip; Hard Rule #11 fix — Grok public client_id now via resolvePublicCred(grok_id), 3 literals removed; grok-oauth 7/7 + check:public-creds green)
* feat(providers): add Factory (factory.ai) as a subscription gateway provider (#5065)
Integrated into release/v3.8.38 (rebased on tip; added factory registry test for PR Test Policy + fixed check:env-doc-sync phantom FACTORY_API_KEY; factory loads in PROVIDERS, no Zod issue — that flag was a false positive)
* chore(test): reconcile golden snapshot + apikey count for new providers
#5020 (grok-cli), #5065 (factory), #5105 (zenmux-free) added providers but did
not regenerate tests/snapshots/provider/translate-path.json (now +3 entries) nor
bump the APIKEY_PROVIDERS count (159->160 for the factory gateway). Test-only
reconciliation; no production change.
* fix(resilience): harden quota and model lockout edge cases (#5093)
Integrated into release/v3.8.38 (rebased on tip). TRUST-BUT-VERIFY: dropped the PR's 0dd7df641 'fix unit gates' commit which reverted #5122 reasoning-replay (preserveReasoningContent) + re-introduced #4849 O(n^2) growth, and restored 5 tests it had realigned. Kept only the 3 declared resilience fixes (quota cutoff guard, gemini MIME, model-lockout maxCooldownMs); 23/23 green.
* Hydrate quota cache and scope auto combo candidates (#5015)
Integrated into release/v3.8.38 (rebased on tip). Kept core quota-cache hydration + auto-combo candidate scoping + combos UI; dropped out-of-scope toolCloaking refactor (conflicted with #4813 stripEnumDescriptions — took tip) and the unrelated sse-auth test split. Added quota-cache-hydrate-5015 regression test (Rule #18); combo-account-allowlist 8/8 + hydration 2/2 green.
* chore(quality): reconcile complexity + file-size baselines for v3.8.38 owner-PR batch
complexity 1972->1978 (+6) and file-size providers.ts 1093->1107 / usageHistory.ts
934->983 — drift from the /review-prs merge batch (#4845/#5105/#5020/#4940/#5093/
#5015 + #5121 cookie-dedup helper extraction). check:complexity/check:file-size do
not run on the PR->release fast-path, so the branch accrued unmeasured; all legit
feature/fix growth, not regression. See per-key justifications in each baseline.
* fix(security): exact-host Anthropic baseUrl check (CodeQL js/incomplete-url-substring-sanitization #674) (#5130)
The anthropic-compatible Bearer-fallback gate decided whether a configured baseUrl
targeted the official api.anthropic.com host via a substring `.includes("api.anthropic.com")`.
A look-alike upstream such as `https://api.anthropic.com.evil.test` or
`https://evil.test/?x=api.anthropic.com` matched the substring and was wrongly treated as
official, suppressing the Bearer fallback meant for third-party gateways
(CodeQL #674, js/incomplete-url-substring-sanitization, high).
Replace the substring test with an exported `isOfficialAnthropicBaseUrl()` helper that
parses the URL and compares the hostname for exact equality. Empty baseUrl stays official;
scheme-less hosts are parsed with an assumed https://; an unparseable baseUrl falls back to
third-party (Bearer emitted) as the safer default. Behavior for legitimate official/third-party
baseUrls is unchanged.
Adds tests/unit/anthropic-official-baseurl-host.test.ts covering official, look-alike,
scheme-less, and unparseable inputs plus a static guard that the substring pattern is gone.
* fix(proxy): repair one-click Deno & Cloudflare relay deployments (#5128) (#5132)
* fix(services): embed WS proxy honours LIVE_WS_HOST; reject empty messages early (#5110) (#5133)
* fix(api): resolve /v1/models/{id} case-insensitively (#5082) (#5135)
* fix(providers): add MiniMax M3 & Nemotron 3 Ultra to Cline catalog (#3321) (#5136)
* fix(proxy): make SOCKS5 handshake timeout tunable via SOCKS_HANDSHAKE_TIMEOUT_MS (#5109) (#5137)
* feat(sidebar): add support for colored menu icons (#3812)
Integrated into release/v3.8.38 (recreated on tip — fork had unrelated history; added getSidebarIconAccent regression test, Rule #18). Clean 2-file UI feature.
* fix(providers): complete grok-cli OAuth wiring + zenmux-free web-session metadata
Base-red repair for #5020 (grok-cli) and #5105 (zenmux-free), surfaced by the
full CI on the release PR (#5078) — the PR->release fast-path does not run the
oauth-providers-config / web-session-credentials / provider-consistency gates.
- grok-cli: register in OAUTH_PROVIDERS (providers.ts canonical list, fixes
check:provider-consistency), add OAUTH_PROVIDER_IDS.GROK_CLI + GROK_CLI_CONFIG
in oauth constants (provider config now sourced there, not a local literal),
align oauth-providers-config.test.ts (EXPECTED_PROVIDER_KEYS + config map).
- zenmux-free: declare its web-session credential requirement (full Cookie header)
in WEB_SESSION_CREDENTIAL_REQUIREMENTS.
Local: oauth-providers-config 27/27, web-session-credentials 4/4, grok-cli-oauth
7/7, check:provider-consistency OK, +115 OAUTH_PROVIDERS tests green.
* Fix resilience settings page response mapping (#5139)
Integrated into release/v3.8.38. Thanks @rdself for the fix and the regression test.
* fix(kiro): retire claude-sonnet-4.5 from catalog + pin 400 model-unavailable test (#5140)
Extracted the real change from #5140 (the bot PR regenerated the entire
freeModelCatalog.data.ts + touched package-lock.json; only the targeted
edits are kept here):
- remove claude-sonnet-4.5 from the Kiro registry entry
- remove the matching kiro free-model catalog row
- pin Kiro's verbatim 400 "Invalid model..." to isModelUnavailableError
Closes #4484
* fix(sidebar): drop orphan `settings` accent color (typecheck:core red) (#5142)
SIDEBAR_ICON_ACCENTS is typed Partial<Record<HideableSidebarItemId, string>>,
but `settings` is not a hideable item id (only `settings-general`,
`settings-appearance`, … and `context-settings` exist; there is no item with
`id: "settings"`), so the accent was unreachable. It broke `typecheck:core`
on the release tip ("'settings' does not exist in type …", introduced by
#3812 colored menu icons). Removing the orphan key restores a clean
typecheck:core (rc=0).
* feat: salvage batch 2 — diagnostics null-guard (#5096) + observed quota reset windows (#5025) (#5141)
* fix(diagnostics): null-guard content blocks in detectMalformedNonStream
A null (or non-object) entry in a Claude-native `content` array made the
non-stream classifier throw `TypeError: Cannot read properties of null
(reading 'type')`, crashing the malformed-response detection path. Guard
before type-asserting each block: a null/non-object block is simply skipped.
Two regression tests added (null block among valid blocks → null; only-null
blocks → empty_choices).
Salvaged from closed PR #5096 (base-stale; only the defensive guard — the
Claude-shape recognition it also carried already landed via #5108).
Co-authored-by: herjarsa <herjarsa@users.noreply.github.com>
* feat(quota): persist observed provider quota reset windows
Adds `provider_quota_reset_events` (migration 108) + `db/quotaResetEvents.ts`
to record real upstream weekly-quota window transitions whenever a quota
refresh shows the reset rolling to a new cycle (different day, later resetAt).
`apiKeyUsageLimits` now prefers the observed window start over the inferred
`resetAt − 7d`, falling back to snapshot inference when no event is recorded
yet. `quotaCache.setQuotaCache` records the transition opportunistically.
`recordProviderQuotaResetEventIfChanged` only fires for the primary weekly
window (not daily/sonnet), is idempotent (INSERT OR IGNORE on the unique
window key), and no-ops when the reset didn't actually roll. 4 unit tests
(tests/unit/lib/quota-reset-events.test.ts).
Salvaged from closed PR #5025 (which bundled this with two unrelated
features + a colliding migration 104). Renumbered to 108; module re-exported
from localDb (Rule #2).
Co-authored-by: Witroch4 <175152067+Witroch4@users.noreply.github.com>
---------
Co-authored-by: herjarsa <herjarsa@users.noreply.github.com>
Co-authored-by: Witroch4 <175152067+Witroch4@users.noreply.github.com>
* docs(i18n): sync 3.8.38 CHANGELOG section to 41 mirrors (unblock docs-accuracy) (#5144)
The root CHANGELOG [3.8.38] section grew with this cycle's merged PRs, but the
docs/i18n/<lang>/CHANGELOG.md mirrors were not re-synced — drifting >25% in body
size and failing check:docs-sync (the "Docs accuracy" fast-gate step) for every
open PR against the release.
Ran scripts/release/sync-changelog-i18n.mjs 3.8.38 3.8.37 to copy the root
[3.8.38] section into all 41 mirrors. check:docs-all now passes (exit 0).
Sections are copied verbatim; the per-language translation pass runs at release
time via i18n:run — this only restores the size-sync the gate enforces.
* feat(compression): pure per-step fidelity checker (4 invariants, fail-open)
* feat(compression): fidelityGate config + rejected breakdown fields
* feat(compression): wire per-step fidelity gate into stacked pipeline (opt-in)
* feat(compression): preview route accepts fidelityGate flag (playground)
* feat(compression): playground fidelity-gate toggle + lane rejection display
* docs(compression): note fidelityGate advanced thresholds are intentionally API-omitted
* refactor(compression): extract fidelity-gate step helpers to shrink strategySelector (file-size gate)
bodyToText and gateAdvance moved to fidelityGateStep.ts; StackAccumulator exported.
strategySelector: 889->854 (-35). Residual +6 vs pre-Milestone-B frozen 848 is the
irreducible StackOptions.fidelityGate field + two stacked-loop dispatch reads + import.
Baseline updated to 854 with justification. No cycle introduced (import type only).
940 compression tests pass; typecheck clean.
* test(usage): wire usageHistoryDedup under unit runner brace-list (#5145)
Integrated into release/v3.8.38.
* feat: salvage batch from closed stale PRs (#5038, #5057, #5076) (#5138)
Integrated into release/v3.8.38.
* test(combo): deterministic routing-decision matrix for all 17 strategies (#5146)
Integrated into release/v3.8.38.
* feat(compression): fuzzy near-duplicate dedup (session-dedup 2nd pass + playground toggle) (#5143)
Integrated into release/v3.8.38.
* chore(quality): rebaseline file-size for sidebarVisibility.ts + chat.ts drift (#5147)
Mid-cycle drift on release/v3.8.38 from already-merged PRs that the fast-path
(PR->release skips check:file-size) let accumulate without a bump:
- src/shared/constants/sidebarVisibility.ts 1100->1198 (#3812 colored menu
icons, per-item accent map; #5142 dropped one orphan, net still above frozen)
- src/sse/handlers/chat.ts 1560->1575 (#5064 self-inflicted-timeout cooldown
skip + #5124 long OpenAI-compatible SSE hardening + #5110 embed-WS
LIVE_WS_HOST honour / early empty-message reject)
Each covered by its own PR tests; structural shrink of chat.ts tracked in #3501.
Unblocks the Fast Quality Gates for PRs targeting release/v3.8.38.
* chore(release): finalize v3.8.38 CHANGELOG + cycle reconciliation
- Reconcile [3.8.38]: +18 bullets (compression fidelity-gate/fuzzy-dedup #5143,
quota keepalive #5102, web-session robustness #5121, MiniMax/Nemotron #5136,
model-visibility #5091, failover logs #5016, disconnect races #5007, sidebar
orphan #5142, SRE playbooks salvage #5138, new Security #5130 + Maintenance roll-up)
- Credit salvaged-PR authors (@JxnLexn / @KooshaPari / @herjarsa / @Witroch4)
- Remove phantom bullet for CLOSED-not-merged #5092 (setup aggregator never landed)
- Fix isHidden bullet PR citation #4389 -> #5086 (@herjarsa)
- Back-fill forgotten v3.8.36 bullet: #5026 crypto.randomUUID ID-gen (@hamsa0x7)
- Sync 41 i18n CHANGELOG mirrors; README What's New -> v3.8.38
- Rebaseline cycle drift: eslint 3987->4002, cognitive 833->841, dead-exports
345->346, cyclomatic 1978->1980 (file-size handled by #5147)
* fix(i18n): add missing English UI labels (#5153)
Integrated into release/v3.8.38
* Preserve non-stream reasoning fields for compatible clients (#5155)
Integrated into release/v3.8.38
* feat(compression): ionizer engine — lossy JSON-array sampling reversible via CCR (#5148)
Integrated into release/v3.8.38
* test(combo): gated live smoke for combo strategies (in-process + VPS HTTP) (#5151)
Integrated into release/v3.8.38
* test: refresh release expectations to match current code (#5150)
Integrated into release/v3.8.38 (test-only base-red alignment extracted from #5150)
---------
Co-authored-by: Éder Costa <eder.almeida.costa@gmail.com>
Co-authored-by: José Victor Ferreira <root@josevictor.me>
Co-authored-by: Hernan Javier Ardila Sanchez <hjasgr@gmail.com>
Co-authored-by: fulorgnas <46461624+fulorgnas@users.noreply.github.com>
Co-authored-by: Randi <55005611+rdself@users.noreply.github.com>
Co-authored-by: Jan Leon <Jan.gaschler@gmail.com>
Co-authored-by: R. Beltran <rbeltran8000@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: KooshaPari <42529354+KooshaPari@users.noreply.github.com>
Co-authored-by: Ramel Tecnologia - Rafa Martins <146174365+rafacpti23@users.noreply.github.com>
Co-authored-by: herjarsa <herjarsa@users.noreply.github.com>
Co-authored-by: Witroch4 <175152067+Witroch4@users.noreply.github.com>
16 KiB
title, version, lastUpdated
| title | version | lastUpdated |
|---|---|---|
| Stealth Guide | 3.8.2 | 2026-05-13 |
Stealth Guide
Source of truth:
open-sse/utils/tlsClient.ts,open-sse/services/{chatgptTlsClient,claudeCodeCCH,claudeCodeFingerprint,claudeCodeObfuscation,claudeCodeCompatible,antigravityObfuscation}.ts,open-sse/config/cliFingerprints.ts,src/mitm/Last updated: 2026-05-13 — v3.8.0 Audience: Engineers maintaining provider-specific stealth integrations.
OmniRoute integrates with providers whose edges actively fingerprint non-official clients (TLS JA3/JA4, header ordering, JSON body shape, integrity tokens). This page documents the stealth surfaces OmniRoute exposes and where they are implemented.
Legal and Ethical Notice
Stealth features exist so OmniRoute can act as a compatibility layer between user-owned official accounts (Claude Code CLI, ChatGPT Desktop/Web, Antigravity, Cursor, etc.) and OmniRoute's unified API. They are not for evading fraud detection, sharing credentials, or violating provider Terms of Service. The maintainers expect operators to comply with the upstream ToS they signed when creating accounts.
TLS Fingerprinting Layer
open-sse/utils/tlsClient.ts — wreq-js (Chrome 124)
Lazy-loaded wreq-js session that impersonates Chrome 124 on macOS. Used as a generic JA3/JA4 wrapper for upstreams behind Cloudflare. Falls back to native fetch when wreq-js is not installed (available = false).
- Singleton session:
browser: "chrome_124", os: "macos" - Proxy resolution (priority):
HTTPS_PROXY→HTTP_PROXY→ALL_PROXY(also lower-case) - Timeout:
TLS_CLIENT_TIMEOUT_MS(inherits fromFETCH_TIMEOUT_MS, default 600000) wreq-jsResponse is fetch-compatible (headers,text(),json(),clone(),body).
open-sse/services/chatgptTlsClient.ts — tls-client-node (Firefox 148)
Dedicated TLS impersonator for chatgpt.com. ChatGPT's Cloudflare config pins cf_clearance to JA3/JA4 + HTTP/2 SETTINGS frame ordering — undici's handshake gets cf-mitigated: challenge even with valid cookies.
- Profile:
firefox_148(must match the Firefox 148User-Agentsent) - Mode:
runtimeMode: "native"(koffi-loaded shared library; avoids managed sidecar HTTP) withRandomTLSExtensionOrder: truetlsFetchChatGpt(url, options)supports streaming (writes body to temp file, tailed asReadableStream)- Hang detection:
raceWithTimeout+TlsClientHangErrortriggersresetClientCache()so the next call respawns the binding - Proxy resolution (priority): per-call
proxyUrl→OMNIROUTE_TLS_PROXY_URL→HTTPS_PROXY/HTTP_PROXY/ALL_PROXY(the native binding does not read these envs itself; it must be threaded through) - Errors:
TlsClientUnavailableError(binary missing),TlsClientHangError(binding deadlocked)
Claude Code Stealth Bundle
When cliCompatMode is on, OmniRoute reshapes outgoing Claude requests so they are indistinguishable from claude-cli traffic. Three modules collaborate:
claudeCodeFingerprint.ts
Computes the 3-char cc_version fingerprint embedded in the billing header:
SHA256(SALT + msg[4] + msg[7] + msg[20] + version)[:3]
FINGERPRINT_SALT = "59cf53e54c78"(hardcoded; matches official client)- Inputs: chars at index 4, 7, 20 of the first user message text + version string
- Output: 3-char hex prefix
claudeCodeCCH.ts (Client Content Hash)
Server-side integrity check the official Claude Code CLI computes via Bun/Zig. OmniRoute reimplements with xxhash-wasm:
- Serialize body with
cch=00000;placeholder xxhash64(bytes, seed) & 0xFFFFF- Zero-padded 5-char lowercase hex
- Replace
cch=00000;with the computed token
Constants:
- Seed:
0x6e52736ac806831e - Pattern:
/\bcch=([0-9a-f]{5});/
claudeCodeObfuscation.ts
Inserts a Unicode zero-width joiner (U+200D) after the first character of "sensitive" client names so upstream filters cannot grep them. Default word list:
opencode, open-code, cline, roo-cline, roo_cline, cursor, windsurf,
aider, continue.dev, copilot, avante, codecompanion
Applied to: system blocks, all messages[].content, and tools[].description / tools[].function.description. Operator-overridable via setSensitiveWords().
claudeCodeCompatible.ts — anthropic-compatible-cc-* providers
For third-party Anthropic relays that only accept "real Claude Code" traffic:
CLAUDE_CODE_COMPATIBLE_USER_AGENT = "claude-cli/2.1.187 (external, sdk-cli)"CLAUDE_CODE_COMPATIBLE_STAINLESS_PACKAGE_VERSION = "0.94.0"CLAUDE_CODE_COMPATIBLE_STAINLESS_RUNTIME_VERSION = "v24.3.0"anthropic-beta = "claude-code-20250219,interleaved-thinking-2025-05-14,effort-2025-11-24"by default- The per-connection "Enable redact-thinking beta" toggle adds
redact-thinking-2026-02-12when a CC Compatible upstream specifically requires redacted thinking streams - The per-connection "Enable summarized thinking display" toggle stores
providerSpecificData.requestDefaults.summarizeThinkingand addsdisplay: "summarized"to CC Compatible thinking requests that did not already set a display mode CONTEXT_1M_BETA_HEADER = "context-1m-2025-08-07"(Opus/Sonnet 4.x family)- Default path:
/v1/messages?beta=true
Sister modules in the same bundle:
claudeCodeConstraints.ts— temperature + cache-control rulesclaudeCodeToolRemapper.ts— tool-name remappingclaudeCodeExtraRemap.ts— extra payload normalization
Antigravity Stealth
antigravityObfuscation.ts
Same zero-width-joiner trick as Claude Code, but with an expanded word list that also masks: claude code, claude-code, kilo code, kilocode, omniroute. Mirrors ZeroGravity's ZEROGRAVITY_SENSITIVE_WORDS and CLIProxyAPI's cloak system.
antigravityHeaderScrub.ts
Strips Stainless SDK markers (x-stainless-lang, x-stainless-package-version, x-stainless-os, x-stainless-arch, x-stainless-runtime, x-stainless-runtime-version, x-stainless-timeout, x-stainless-retry-count, x-stainless-helper-method) before forwarding.
⚠️ Risk: ANTIGRAVITY_CREDITS=always (account-ban hot spot)
ANTIGRAVITY_CREDITS=always (consumed by open-sse/executors/antigravity.ts) routes every request through Antigravity AI Credit Overages (paid Google credits) instead of letting Google's free-tier quota gate things. This is documented as a feature, but it is the single most common ToS-violation report we see — multiple Google Ultra accounts have been banned with 403 / "service disabled for ToS violation" / insufficient_quota after running for a few hours with =always.
The upstream enforcement is on Google's side, not anything OmniRoute can prevent. The env var name and the existing docs make it sound like a safe knob to flip; it isn't.
Why this draws abuse detection more aggressively than free-tier-only usage:
- Sustained automated spend on a single Google account flags differently than free-tier hits-quota-and-stops.
- Credit overages have no rate ceiling, so a misconfigured client can burn through several hundred USD in minutes and look like API-key resale or bot traffic.
- Multiple OmniRoute users hitting overage credits in parallel from the same external IP compounds the signal.
Recommended posture:
- Default to
ANTIGRAVITY_CREDITS=retry— overages are used only when free-tier returns 429, not on every request. This is the safer of the two non-zero modes. - Spread load across providers via Auto-Combo (
model: "auto"orkr/glm/etc-combo) instead of saturating a single Antigravity account. - Set per-connection RPM limits in the Antigravity provider's edit page (Dashboard → Providers → Antigravity → connection → rate limit). 30–60 RPM is a defensible upper bound for sustained use.
- Use distinct upstream IPs per Antigravity account when possible (residential proxies aimed at the same account from many users compounds the abuse signal).
- If banned: appeal via
support.google.com→ "Restore Workspace/Account access" with the exactquota_exceeded/service disabledresponse body Google sent. Restoration is not guaranteed.
This warning is also surfaced inline in the dashboard near the Antigravity provider edit screen when ANTIGRAVITY_CREDITS is set to always (or will be in v3.8.0; tracked separately).
Touch points:
open-sse/executors/antigravity.ts— readsprocess.env.ANTIGRAVITY_CREDITSsrc/lib/oauth/providers/antigravity.ts— credential plumbing- Original incident report: Discussion #1183
CLI Fingerprint Registry — open-sse/config/cliFingerprints.ts
Per-provider table that pins exact header ordering and JSON body field ordering captured from mitmproxy traces of the official CLIs. Currently registered: codex, claude, plus runtime-derived profiles in providerHeaderProfiles.ts for antigravity, qwen, github.
interface CliFingerprint {
headerOrder: string[]; // case-sensitive
bodyFieldOrder: string[]; // top-level JSON keys
userAgent?: string | (() => string);
extraHeaders?: Record<string, string>;
}
Toggle per provider via env (see below). When disabled, headers/body keys appear in whatever order Node/JSON gave them — easy to fingerprint.
MITM Proxy (Antigravity, Linux/macOS/Windows)
For CLIs whose binaries cannot be redirected via OPENAI_BASE_URL, OmniRoute runs a local TLS-terminating proxy. Endpoints live under src/app/api/cli-tools/antigravity-mitm/.
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /api/cli-tools/antigravity-mitm |
Status — running, pid, dnsConfigured, certExists |
| POST | /api/cli-tools/antigravity-mitm |
Start MITM (requires apiKey + sudoPassword) |
| DELETE | /api/cli-tools/antigravity-mitm |
Stop MITM |
| GET | /api/cli-tools/antigravity-mitm/alias |
List model aliases |
| PUT | /api/cli-tools/antigravity-mitm/alias |
Save model aliases for a tool |
Target intercepted host: daily-cloudcode-pa.googleapis.com (Antigravity's upstream).
Start sequence (src/mitm/manager.ts::startMitm)
- Generate self-signed cert via
selfsigned(RSA-2048, SHA-256, 1y) —cert/generate.ts - Install cert to system trust store —
cert/install.ts - Add hosts entry
127.0.0.1 daily-cloudcode-pa.googleapis.com—dns/dnsConfig.ts - Spawn
src/mitm/server.cjswithROUTER_API_KEY+MITM_LOCAL_PORT(default443) - Persist PID to
<DATA_DIR>/mitm/.mitm.pid
Linux dynamic trust-store detection — cert/install.ts
getLinuxCertConfig() walks a priority list and picks the first existing directory:
| Distro family | Directory | Update command |
|---|---|---|
| Debian / Ubuntu | /usr/local/share/ca-certificates |
update-ca-certificates |
| Arch / CachyOS / Manjaro | /etc/ca-certificates/trust-source/anchors |
update-ca-trust |
| Fedora / RHEL / CentOS | /etc/pki/ca-trust/source/anchors |
update-ca-trust |
| openSUSE | /etc/pki/trust/anchors |
update-ca-certificates |
Cert filename: omniroute-mitm.crt. Fingerprint match via getCertFingerprint() (SHA-1 of DER).
Additionally, updateNssDatabases() installs into per-user NSS DBs when certutil is available: ~/.pki/nssdb, ~/snap/chromium/.../nssdb, all Firefox profiles (including snap), under the nickname OmniRoute MITM Root CA.
macOS / Windows
- macOS:
security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain - Windows: elevated PowerShell →
certutil -addstore Root
Auth
All MITM endpoints require management auth (requireCliToolsAuth). The sudo password is cached in module scope (never globalThis) and cleared on stopMitm().
User-Agent Overrides — env vars (.env.example section 12)
| Variable | Default |
|---|---|
CLAUDE_USER_AGENT |
claude-cli/2.1.187 (external, cli) |
CODEX_USER_AGENT |
codex-cli/0.142.0 (Windows 10.0.26200; x64) |
GITHUB_USER_AGENT |
GitHubCopilotChat/0.45.1 |
ANTIGRAVITY_USER_AGENT |
antigravity/2.0.1 darwin/arm64 |
KIRO_USER_AGENT |
AWS-SDK-JS/3.0.0 kiro-ide/1.0.0 |
QODER_USER_AGENT |
Qoder-Cli |
QWEN_USER_AGENT |
QwenCode/0.15.9 (linux; x64) |
CURSOR_USER_AGENT |
Cursor/3.3 |
GEMINI_CLI_USER_AGENT |
google-api-nodejs-client/10.3.0 |
Consumed by open-sse/executors/base.ts::buildHeaders() via dynamic lookup. Bump these when providers release new CLI versions — stale UA strings start getting rejected as outdated clients.
CLI Compatibility Mode Toggles (.env.example section 13)
| Variable | Effect |
|---|---|
CLI_COMPAT_CODEX=1 |
Codex fingerprint |
CLI_COMPAT_CLAUDE=1 |
claude-cli fingerprint |
CLI_COMPAT_GITHUB=1 |
GitHub Copilot Chat fingerprint |
CLI_COMPAT_ANTIGRAVITY=1 |
Antigravity fingerprint |
CLI_COMPAT_KIRO=1 |
Kiro |
CLI_COMPAT_CURSOR=1 |
Cursor |
CLI_COMPAT_KIMI_CODING=1 |
Kimi Coding |
CLI_COMPAT_KILOCODE=1 |
KiloCode |
CLI_COMPAT_CLINE=1 |
Cline |
CLI_COMPAT_QWEN=1 |
Qwen Code |
CLI_COMPAT_ALL=1 |
Enable all of the above |
The provider IP is always preserved — the toggle only reshapes the request wire image, it does not switch IP egress.
Inbound Header Sanitization
OmniRoute scrubs inbound client headers before forwarding so a request that arrives from Cursor doesn't leak User-Agent: Cursor/X.Y.Z to a Claude upstream. See src/shared/constants/upstreamHeaders.ts for the denylist, kept in lockstep with the Zod schemas and unit tests.
Updating Fingerprints When a Provider Rotates
- Capture official CLI traffic with
mitmproxy(TLS interception + dump) - Extract JA3/JA4 and the literal header order
- Update the relevant
CLI_FINGERPRINTS[...]entry - Bump matching
*_USER_AGENTdefault in.env.example - If TLS handshake itself changed: update
chatgptTlsClient.ts::CHATGPT_PROFILEor wreq-jsbrowser:option - Run
chatgptTlsClient.test.tsand a manual canary against the live provider - Ship in a patch release; document in
CHANGELOG.md
Tests
open-sse/services/__tests__/chatgptTlsClient.test.ts— proxy resolution priority, abort handling, hang recoverytests/unit/anthropic-cache-fingerprint.test.ts— fingerprint determinismtests/unit/chatgpt-web.test.ts— end-to-end stealth path for ChatGPT
See Also
- RESILIENCE_GUIDE.md — what happens when a stealth path gets a
403 - TROUBLESHOOTING.md
- ENVIRONMENT.md — full env reference
- CLI-TOOLS.md — operator view of the MITM workflow