diff --git a/CHANGELOG.md b/CHANGELOG.md index ddb3135b06..e9404280bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ ### 🔧 Bug Fixes +- **providers (codex):** fix the **Codex Responses WebSocket** path (`/v1/responses`), which regressed in v3.8.40 with a client-visible `Invalid JSON body` and bypassed the configured proxy. (1) #5591 — PR #5237 bumped the impersonation TLS profile to `chrome_149`, but `wreq-js@2.3.1` only supports up to `chrome_147`; the unknown profile produced a degenerate fingerprint and ChatGPT rejected the upstream upgrade. The Codex WS path is reverted to the proven `chrome_142` (the v3.8.39 value), and the over-bumped `grok-web`/`claude-web` profiles (masked by their circuit-breaker but silently dropping TLS impersonation) are restored to `chrome_146`. A new regression guard asserts every configured `chrome_*` profile exists in the installed `wreq-js` typings (`tests/unit/tls-profiles-valid-5591.test.mjs`). (2) #5611 — the upstream `wreq-js.websocket()` connect ignored the Proxy Registry, so a no-direct-egress Docker container failed with a DNS error; the prepare route now resolves the Global/provider proxy and threads it through to the WS connect. Regression guard in `tests/unit/responses-ws-proxy.test.mjs`. ([#5591](https://github.com/diegosouzapw/OmniRoute/issues/5591), [#5611](https://github.com/diegosouzapw/OmniRoute/issues/5611)) + - **providers (GLM):** GLM **5.1 / 5.2** now keep the `system` role instead of having the system prompt folded into the first user turn. `roleNormalizer.ts` matched every `glm*` id with a blanket `startsWith("glm")` / `startsWith("glm-")` prefix, so the next-generation models — which z.ai documents as supporting the `system` role (GLM > 5.0) — were normalized as if they rejected it, degrading instruction-following. The matcher is now version-aware: it strips the system role only for bare `glm`, the 4.x family, and the 5.0 generation, and preserves it for `glm-5.1`/`glm-5.2` (and the Fireworks `glm-5p1` point alias). The ZenMux vendor-prefixed `z-ai/glm-*` compressed-history rule and the ERNIE rule are unchanged. Regression guards in `tests/unit/role-normalizer.test.ts`. ([#5610](https://github.com/diegosouzapw/OmniRoute/issues/5610)) - **Security hardening follow-ups (v3.8.15):** the `auth_token` cookie now sets an explicit 30-day `maxAge` so sessions persist as intended (Seg3); the management bootstrap warns at boot when `INITIAL_PASSWORD` is left at the insecure `CHANGEME` default (Seg2); VS Code path-token endpoints (`/api/v1/vscode/raw/[token]`) emit a once-per-process security warning since the API key travels in the URL and can leak via logs/proxies (Seg4); the system version route resolves the real global install path via `npm root -g` instead of a hardcoded `/app` (Bug3); and auto-update mode detection segment-matches `node_modules` instead of substring-matching, eliminating false "global install" positives (Bug1). diff --git a/open-sse/services/claudeTlsClient.ts b/open-sse/services/claudeTlsClient.ts index 9e09e832ac..499c32e130 100644 --- a/open-sse/services/claudeTlsClient.ts +++ b/open-sse/services/claudeTlsClient.ts @@ -20,7 +20,7 @@ import { randomUUID } from "node:crypto"; let clientPromise: Promise | null = null; let exitHookInstalled = false; -const CLAUDE_PROFILE = "chrome_149"; // matches the Vivaldi/Chrome 149 UA we send +const CLAUDE_PROFILE = "chrome_146"; // closest supported wreq-js profile (chrome_149 absent in 2.3.1, #5591) const DEFAULT_TIMEOUT_MS = Number.parseInt(process.env.OMNIROUTE_CLAUDE_TLS_TIMEOUT_MS || "", 10) || 60_000; // Grace period added to the binding's wire-level timeout before our JS-level diff --git a/open-sse/services/grokTlsClient.ts b/open-sse/services/grokTlsClient.ts index cdc888f896..bd24b37a87 100644 --- a/open-sse/services/grokTlsClient.ts +++ b/open-sse/services/grokTlsClient.ts @@ -25,7 +25,7 @@ import { randomUUID } from "node:crypto"; let clientPromise: Promise | null = null; let exitHookInstalled = false; -const GROK_PROFILE = "chrome_149"; // closest Chrome profile to the UA we send +const GROK_PROFILE = "chrome_146"; // closest supported wreq-js profile (chrome_149 absent in 2.3.1, #5591) const DEFAULT_TIMEOUT_MS = Number.parseInt(process.env.OMNIROUTE_GROK_TLS_TIMEOUT_MS || "", 10) || 60_000; // Grace period added to the binding's wire-level timeout before our JS-level diff --git a/scripts/dev/responses-ws-proxy.mjs b/scripts/dev/responses-ws-proxy.mjs index 5fa562d752..411cb78828 100644 --- a/scripts/dev/responses-ws-proxy.mjs +++ b/scripts/dev/responses-ws-proxy.mjs @@ -615,11 +615,17 @@ class ResponsesWsSession { toStringOrNull(responseBody.service_tier) || toStringOrNull(responseBody.serviceTier), }; - const upstream = await this.wsFactory(prepared.json.upstreamUrl, { - browser: prepared.json.browser || "chrome_149", + const wsOptions = { + // #5591: chrome_149 is not a wreq-js 2.3.1 profile (max chrome_147); the + // prepare route now sends chrome_142, this fallback matches it. + browser: prepared.json.browser || "chrome_142", os: prepared.json.os || "windows", headers: prepared.json.headers || {}, - }); + }; + // #5611: forward the configured proxy so the upstream WS connect honors the + // Proxy Registry in no-direct-egress deployments. + if (prepared.json.proxy) wsOptions.proxy = prepared.json.proxy; + const upstream = await this.wsFactory(prepared.json.upstreamUrl, wsOptions); upstream.onmessage = (event) => { if (this.closed) return; diff --git a/src/app/api/internal/codex-responses-ws/route.ts b/src/app/api/internal/codex-responses-ws/route.ts index c9676a59c3..ec1d710981 100644 --- a/src/app/api/internal/codex-responses-ws/route.ts +++ b/src/app/api/internal/codex-responses-ws/route.ts @@ -18,6 +18,8 @@ import { } from "@/lib/memory/settings"; import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts"; import { logger } from "@omniroute/open-sse/utils/logger.ts"; +import { resolveProxy } from "@omniroute/open-sse/utils/networkProxy.ts"; +import { proxyConfigToUrl } from "@omniroute/open-sse/utils/proxyDispatcher.ts"; const CODEX_RESPONSES_WS_URL = "wss://chatgpt.com/backend-api/codex/responses"; const executor = new CodexExecutor(); @@ -408,16 +410,32 @@ async function prepare(body: JsonRecord) { const headers = normalizeUpstreamHeaders(executor.buildHeaders(refreshedCredentials, true)); + // #5611: apply the configured Global/provider proxy to the upstream Codex + // Responses WebSocket too. The downstream client→OmniRoute hop works, but the + // upstream wreq-js.websocket() connect previously ignored the Proxy Registry, + // so a no-direct-egress container failed with a DNS lookup error. + let proxy: string | undefined; + try { + proxy = proxyConfigToUrl(await resolveProxy(provider)) || undefined; + } catch (err) { + logger.warn(`[codex-responses-ws] proxy resolution failed: ${sanitizeErrorMessage(err)}`); + } + return NextResponse.json({ ok: true, upstreamUrl: CODEX_RESPONSES_WS_URL, - browser: "chrome_149", + // #5591: chrome_149 does not exist in wreq-js 2.3.1 (max chrome_147) → the + // native layer yields a degenerate TLS fingerprint and ChatGPT rejects the + // upgrade ("Invalid JSON body"). chrome_142 is the profile that shipped in + // v3.8.39 and is confirmed working against this upstream. + browser: "chrome_142", os: "windows", connectionId: refreshedCredentials.connectionId, provider, account: refreshedCredentials.email || null, model, headers, + proxy, response: transformed, }); } diff --git a/tests/unit/responses-ws-proxy.test.mjs b/tests/unit/responses-ws-proxy.test.mjs index e9de940650..95890e344b 100644 --- a/tests/unit/responses-ws-proxy.test.mjs +++ b/tests/unit/responses-ws-proxy.test.mjs @@ -78,6 +78,8 @@ test("responses ws proxy prepares and forwards OpenAI Responses websocket events connectionId: "conn_1", provider: "codex", account: "codex@example.com", + // #5611: prepare resolves the configured proxy and threads it through. + proxy: "http://test-proxy:8888", model: "gpt-5.5", response: { ...body.response, @@ -133,6 +135,11 @@ test("responses ws proxy prepares and forwards OpenAI Responses websocket events wsFactory: async (url, options) => { assert.equal(url, "wss://chatgpt.com/backend-api/codex/responses"); assert.equal(options.headers.Authorization, "Bearer upstream-token"); + // #5591: prepare omits `browser`, so the fallback must be the supported + // chrome_142 (not the non-existent chrome_149). + assert.equal(options.browser, "chrome_142"); + // #5611: the configured proxy from prepare must reach the upstream connect. + assert.equal(options.proxy, "http://test-proxy:8888"); return fakeUpstream; }, }); diff --git a/tests/unit/tls-profiles-valid-5591.test.mjs b/tests/unit/tls-profiles-valid-5591.test.mjs new file mode 100644 index 0000000000..87f2a41e11 --- /dev/null +++ b/tests/unit/tls-profiles-valid-5591.test.mjs @@ -0,0 +1,57 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +// #5591 regression guard: every chrome_* TLS impersonation profile referenced in +// the source must be a real wreq-js BrowserProfile. PR #5237 set them to +// "chrome_149", which does not exist in wreq-js 2.3.1 (the union tops out at +// chrome_147) — the native layer then produced a degenerate fingerprint and the +// Codex Responses WebSocket upstream rejected the upgrade ("Invalid JSON body"). +// This test reads the supported set straight from the installed wreq-js type +// definitions, so it stays correct as the dependency is upgraded. + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", ".."); + +function supportedProfiles() { + const dts = fs.readFileSync( + path.join(ROOT, "node_modules", "wreq-js", "dist", "wreq-js.d.ts"), + "utf8" + ); + return new Set([...dts.matchAll(/chrome_(\d+)/g)].map((m) => `chrome_${m[1]}`)); +} + +// Source files that hand a `browser`/PROFILE value to wreq-js. +const SOURCES = [ + "src/app/api/internal/codex-responses-ws/route.ts", + "scripts/dev/responses-ws-proxy.mjs", + "open-sse/services/grokTlsClient.ts", + "open-sse/services/claudeTlsClient.ts", +]; + +// Strip comments before scanning — explanatory comments may name the bad +// profile ("chrome_149 absent in 2.3.1") without it ever reaching wreq-js. +function stripComments(line) { + return line.replace(/\/\*.*?\*\//g, "").replace(/\/\/.*$/, ""); +} + +test("#5591 all configured chrome_* TLS profiles exist in wreq-js", () => { + const supported = supportedProfiles(); + assert.ok(supported.size > 0, "expected to parse chrome_* profiles from wreq-js d.ts"); + + for (const rel of SOURCES) { + const lines = fs.readFileSync(path.join(ROOT, rel), "utf8").split("\n"); + lines.forEach((line, i) => { + const code = stripComments(line); + for (const m of code.matchAll(/chrome_(\d+)/g)) { + const profile = `chrome_${m[1]}`; + assert.ok( + supported.has(profile), + `${rel}:${i + 1} uses ${profile} which is NOT a wreq-js BrowserProfile ` + + `(supported: ${[...supported].sort().join(", ")})` + ); + } + }); + } +});