fix: restore Codex Responses WS TLS profile + apply proxy (#5591, #5611) (#5668)

This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-30 12:41:14 -03:00
committed by GitHub
parent 9be5528548
commit 98ebba1801
7 changed files with 96 additions and 6 deletions

View File

@@ -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).

View File

@@ -20,7 +20,7 @@ import { randomUUID } from "node:crypto";
let clientPromise: Promise<unknown> | 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

View File

@@ -25,7 +25,7 @@ import { randomUUID } from "node:crypto";
let clientPromise: Promise<unknown> | 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

View File

@@ -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;

View File

@@ -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,
});
}

View File

@@ -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;
},
});

View File

@@ -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(", ")})`
);
}
});
}
});