fix(sse): run compression pipeline per turn in Codex Responses WS bridge (#8052) (#8154)

The Codex Responses-over-WebSocket bridge bypassed the whole prompt-compression
pipeline (and its analytics writes) that the HTTP/SSE path (chatCore.ts) runs on
every request, via two gaps:

1. prepare() in codex-responses-ws/route.ts never called anything from
   open-sse/services/compression/* — it authenticated, injected memory, applied
   reasoning-routing, then went straight to executor.transformRequest().
2. scripts/dev/responses-ws-proxy.mjs memoized the upstream connection in
   ensureUpstream() and only called the internal "prepare" action on the FIRST
   response.create of a WS session — every subsequent turn on a reused
   connection bypassed prepare() (and therefore compression) entirely.

Fix: a new compression.ts module wires the core compression pipeline (settings
resolution -> selectCompressionStrategy -> applyCompressionAsync ->
compression_analytics/compression_engine_breakdown writes, reusing
adaptBodyForCompression's existing Responses-API input[] adapter) into
prepare(); responses-ws-proxy.mjs now re-runs prepare() (via a new shared
runPrepare() helper) for every logical response.create turn on a reused
connection, not just the first, without recreating the upstream socket.

Regression test: tests/unit/responses-ws-proxy-compression-parity.test.ts
proves the reused-connection bypass by execution (RED: 1 prepare call for 2
turns; GREEN after the fix: 2 prepare calls for 2 turns).
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-22 11:27:57 -03:00
committed by GitHub
parent b954a3a60f
commit 98b1aa34b5
5 changed files with 419 additions and 44 deletions

View File

@@ -579,6 +579,52 @@ class ResponsesWsSession {
await this.forwardClientMessage(message);
}
// #8052: shared by ensureUpstream() (first turn — also owns socket creation) and
// forwardClientMessage() (subsequent turns on a reused connection). Calls the internal
// "prepare" action — auth/policy/memory/reasoning-routing/compression — and refreshes
// preparedContext, but never touches this.upstream/this.upstreamReady; the caller decides
// whether a new upstream socket is needed.
async runPrepare(message, responseBody) {
const prepared = await callInternal(this.fetchImpl, this.baseUrl, this.bridgeSecret, "prepare", {
requestUrl: this.requestUrl,
headers: getAuthHeaders(this.requestUrl, this.requestHeaders),
message,
response: responseBody,
});
if (!prepared.ok) {
const message2 =
prepared.json?.error?.message ||
prepared.json?.message ||
prepared.text ||
"Codex WS prepare failed";
const code = prepared.json?.error?.code || "codex_ws_prepare_failed";
const error = new Error(message2);
error.code = code;
error.status = prepared.status;
throw error;
}
this.preparedContext = {
upstreamUrl: toStringOrNull(prepared.json?.upstreamUrl),
connectionId: toStringOrNull(prepared.json?.connectionId),
account: toStringOrNull(prepared.json?.account),
provider: toStringOrNull(prepared.json?.provider) || "codex",
model: toStringOrNull(prepared.json?.model) || toStringOrNull(responseBody.model),
requestedModel: toStringOrNull(responseBody.model),
reasoningRouting:
prepared.json?.reasoningRouting &&
typeof prepared.json.reasoningRouting === "object" &&
!Array.isArray(prepared.json.reasoningRouting)
? prepared.json.reasoningRouting
: null,
serviceTier:
toStringOrNull(responseBody.service_tier) || toStringOrNull(responseBody.serviceTier),
};
return prepared;
}
async ensureUpstream(firstMessage) {
if (this.upstreamReady) return this.upstreamReady;
@@ -590,48 +636,7 @@ class ResponsesWsSession {
this.firstResponseBody ||= responseBody;
this.currentRequestBody = responseBody;
const prepared = await callInternal(
this.fetchImpl,
this.baseUrl,
this.bridgeSecret,
"prepare",
{
requestUrl: this.requestUrl,
headers: getAuthHeaders(this.requestUrl, this.requestHeaders),
message: firstMessage,
response: responseBody,
}
);
if (!prepared.ok) {
const message =
prepared.json?.error?.message ||
prepared.json?.message ||
prepared.text ||
"Codex WS prepare failed";
const code = prepared.json?.error?.code || "codex_ws_prepare_failed";
const error = new Error(message);
error.code = code;
error.status = prepared.status;
throw error;
}
this.preparedContext = {
upstreamUrl: toStringOrNull(prepared.json?.upstreamUrl),
connectionId: toStringOrNull(prepared.json?.connectionId),
account: toStringOrNull(prepared.json?.account),
provider: toStringOrNull(prepared.json?.provider) || "codex",
model: toStringOrNull(prepared.json?.model) || toStringOrNull(responseBody.model),
requestedModel: toStringOrNull(responseBody.model),
reasoningRouting:
prepared.json?.reasoningRouting &&
typeof prepared.json.reasoningRouting === "object" &&
!Array.isArray(prepared.json.reasoningRouting)
? prepared.json.reasoningRouting
: null,
serviceTier:
toStringOrNull(responseBody.service_tier) || toStringOrNull(responseBody.serviceTier),
};
const prepared = await this.runPrepare(firstMessage, responseBody);
const wsOptions = {
// #5591: chrome_149 is not a wreq-js 2.3.1 profile (max chrome_147); the
@@ -704,7 +709,16 @@ class ResponsesWsSession {
// turn's own request body so persistHistory() attaches the right
// clientRequest instead of always the first turn's.
const nextTurnBody = getResponseCreatePayload(message);
if (nextTurnBody !== null) this.currentRequestBody = nextTurnBody;
if (nextTurnBody !== null) {
this.currentRequestBody = nextTurnBody;
// #8052: a reused connection must re-run "prepare" (auth/policy/memory/
// reasoning-routing/compression) for every logical turn, not just the first —
// otherwise every turn after the first bypasses the whole pipeline. This reuses
// the already-established upstream transport; it must NOT recreate the socket.
const prepared = await this.runPrepare(message, nextTurnBody);
this.upstream.send(jsonStringifySafe(withPreparedResponseCreate(message, prepared.json.response)));
return;
}
this.upstream.send(jsonStringifySafe(message));
} catch (error) {
const code = error?.code || "upstream_websocket_connect_failed";