Files
2026-07-13 13:39:12 +08:00

419 lines
14 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import http from "node:http";
const { createResponsesWsProxy } = await import("../../scripts/dev/responses-ws-proxy.mjs");
function listen(server) {
return new Promise((resolve) => {
server.listen(0, "127.0.0.1", () => {
const address = server.address();
resolve(address.port);
});
});
}
function close(server) {
return new Promise((resolve) => {
server.close(() => resolve());
});
}
function readRequestBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
req.on("error", reject);
});
}
function waitFor(predicate, { timeoutMs = 3000, intervalMs = 10 } = {}) {
const startedAt = Date.now();
return new Promise((resolve, reject) => {
const timer = setInterval(() => {
try {
const value = predicate();
if (value) {
clearInterval(timer);
resolve(value);
return;
}
if (Date.now() - startedAt >= timeoutMs) {
clearInterval(timer);
reject(new Error("Timed out waiting for condition"));
}
} catch (error) {
clearInterval(timer);
reject(error);
}
}, intervalMs);
});
}
test("responses ws proxy prepares and forwards OpenAI Responses websocket events", async () => {
const internalRequests = [];
const upstreamSends = [];
const downstreamMessages = [];
const server = http.createServer(async (req, res) => {
const url = new URL(req.url || "/", `http://${req.headers.host}`);
if (url.pathname === "/api/internal/codex-responses-ws") {
const body = JSON.parse((await readRequestBody(req)) || "{}");
internalRequests.push(body);
if (body.action === "authenticate") {
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: true, authenticated: true, authType: "api_key" }));
return;
}
if (body.action === "prepare") {
res.writeHead(200, { "content-type": "application/json" });
res.end(
JSON.stringify({
ok: true,
upstreamUrl: "wss://chatgpt.com/backend-api/codex/responses",
headers: { Authorization: "Bearer upstream-token" },
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,
model: "gpt-5.5",
stream: undefined,
},
})
);
return;
}
if (body.action === "log") {
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: true, logged: true }));
return;
}
}
res.writeHead(404, { "content-type": "application/json" });
res.end(JSON.stringify({ error: "not_found" }));
});
const fakeUpstream = {
send(data) {
upstreamSends.push(JSON.parse(data));
setTimeout(() => {
fakeUpstream.onmessage?.({
data: JSON.stringify({
type: "response.completed",
response: {
id: "resp_1",
model: "gpt-5.5",
status: "completed",
usage: { input_tokens: 29, output_tokens: 42, total_tokens: 71 },
},
}),
});
}, 10);
},
close() {},
onmessage: null,
onerror: null,
onclose: null,
};
const port = await listen(server);
const baseUrl = `http://127.0.0.1:${port}`;
const proxy = createResponsesWsProxy({
baseUrl,
bridgeSecret: "bridge-secret",
pingIntervalMs: 1000,
idleTimeoutMs: 10000,
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;
},
});
server.on("upgrade", async (req, socket, head) => {
const handled = await proxy.handleUpgrade(req, socket, head);
if (!handled && !socket.destroyed) {
socket.destroy();
}
});
const ws = new WebSocket(`ws://127.0.0.1:${port}/api/v1/responses?api_key=local-token`);
ws.addEventListener("message", (event) => {
downstreamMessages.push(JSON.parse(String(event.data)));
});
await new Promise((resolve) => ws.addEventListener("open", resolve, { once: true }));
ws.send(
JSON.stringify({
type: "response.create",
model: "gpt-5.5",
input: [{ role: "user", content: "hello" }],
reasoning: { effort: "xhigh" },
})
);
await waitFor(() => downstreamMessages.find((entry) => entry.type === "response.completed"));
const logRequest = await waitFor(() => internalRequests.find((entry) => entry.action === "log"));
assert.equal(internalRequests[0].action, "authenticate");
assert.equal(internalRequests[1].action, "prepare");
assert.equal(upstreamSends.length, 1);
assert.equal(upstreamSends[0].type, "response.create");
assert.equal(upstreamSends[0].model, "gpt-5.5");
assert.equal(upstreamSends[0].reasoning.effort, "xhigh");
assert.equal("stream" in upstreamSends[0], false);
assert.equal(logRequest.transport, "responses_websocket");
assert.equal(logRequest.status, 200);
assert.equal(logRequest.success, true);
assert.equal(logRequest.connectionId, "conn_1");
assert.equal(logRequest.provider, "codex");
assert.equal(logRequest.model, "gpt-5.5");
assert.equal(logRequest.requestedModel, "gpt-5.5");
assert.equal(logRequest.clientRequest.model, "gpt-5.5");
assert.equal(logRequest.responseBody.usage.input_tokens, 29);
assert.equal(logRequest.responseBody.usage.output_tokens, 42);
assert.equal(logRequest.terminalMessage.type, "response.completed");
ws.close();
await close(server);
});
test("responses ws proxy logs prepare failures to request history", async () => {
const internalRequests = [];
const downstreamMessages = [];
const server = http.createServer(async (req, res) => {
const url = new URL(req.url || "/", `http://${req.headers.host}`);
if (url.pathname === "/api/internal/codex-responses-ws") {
const body = JSON.parse((await readRequestBody(req)) || "{}");
internalRequests.push(body);
if (body.action === "authenticate") {
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: true, authenticated: true, authType: "api_key" }));
return;
}
if (body.action === "prepare") {
res.writeHead(503, { "content-type": "application/json" });
res.end(
JSON.stringify({
error: {
code: "codex_credentials_unavailable",
message: "No available Codex OAuth connection for Responses WebSocket",
},
})
);
return;
}
if (body.action === "log") {
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: true, logged: true }));
return;
}
}
res.writeHead(404, { "content-type": "application/json" });
res.end(JSON.stringify({ error: "not_found" }));
});
const port = await listen(server);
const proxy = createResponsesWsProxy({
baseUrl: `http://127.0.0.1:${port}`,
bridgeSecret: "bridge-secret",
pingIntervalMs: 1000,
idleTimeoutMs: 10000,
wsFactory: async () => {
throw new Error("prepare failure should not connect upstream");
},
});
server.on("upgrade", async (req, socket, head) => {
const handled = await proxy.handleUpgrade(req, socket, head);
if (!handled && !socket.destroyed) {
socket.destroy();
}
});
const ws = new WebSocket(`ws://127.0.0.1:${port}/api/v1/responses?api_key=local-token`);
ws.addEventListener("message", (event) => {
downstreamMessages.push(JSON.parse(String(event.data)));
});
await new Promise((resolve) => ws.addEventListener("open", resolve, { once: true }));
ws.send(
JSON.stringify({
type: "response.create",
model: "gpt-5.5",
input: [{ role: "user", content: "hello" }],
})
);
await waitFor(() => downstreamMessages.find((entry) => entry.type === "response.failed"));
const logRequest = await waitFor(() => internalRequests.find((entry) => entry.action === "log"));
assert.equal(logRequest.transport, "responses_websocket");
assert.equal(logRequest.status, 503);
assert.equal(logRequest.success, false);
assert.equal(logRequest.errorCode, "codex_credentials_unavailable");
assert.match(logRequest.errorMessage, /No available Codex OAuth connection/);
assert.equal(logRequest.clientRequest.model, "gpt-5.5");
assert.equal(logRequest.terminalMessage.type, "response.failed");
ws.close();
await close(server);
});
test("responses ws proxy serializes client frames while upstream prepare is pending", async () => {
const internalRequests = [];
const upstreamSends = [];
const server = http.createServer(async (req, res) => {
const url = new URL(req.url || "/", `http://${req.headers.host}`);
if (url.pathname === "/api/internal/codex-responses-ws") {
const body = JSON.parse((await readRequestBody(req)) || "{}");
internalRequests.push(body);
if (body.action === "authenticate") {
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: true, authenticated: true, authType: "api_key" }));
return;
}
if (body.action === "prepare") {
await new Promise((resolve) => setTimeout(resolve, 50));
res.writeHead(200, { "content-type": "application/json" });
res.end(
JSON.stringify({
ok: true,
upstreamUrl: "wss://chatgpt.com/backend-api/codex/responses",
headers: { Authorization: "Bearer upstream-token" },
response: { ...body.response, model: "gpt-5.5", stream: undefined },
})
);
return;
}
}
res.writeHead(404, { "content-type": "application/json" });
res.end(JSON.stringify({ error: "not_found" }));
});
const fakeUpstream = {
send(data) {
upstreamSends.push(JSON.parse(data));
},
close() {},
onmessage: null,
onerror: null,
onclose: null,
};
const port = await listen(server);
const proxy = createResponsesWsProxy({
baseUrl: `http://127.0.0.1:${port}`,
bridgeSecret: "bridge-secret",
pingIntervalMs: 1000,
idleTimeoutMs: 10000,
wsFactory: async () => fakeUpstream,
});
server.on("upgrade", async (req, socket, head) => {
const handled = await proxy.handleUpgrade(req, socket, head);
if (!handled && !socket.destroyed) {
socket.destroy();
}
});
const ws = new WebSocket(`ws://127.0.0.1:${port}/api/v1/responses?api_key=local-token`);
await new Promise((resolve) => ws.addEventListener("open", resolve, { once: true }));
ws.send(
JSON.stringify({
type: "response.create",
model: "gpt-5.5",
input: [{ role: "user", content: "first" }],
})
);
ws.send(JSON.stringify({ type: "response.cancel", response_id: "resp_1" }));
await waitFor(() => upstreamSends.length === 2);
assert.equal(internalRequests.filter((entry) => entry.action === "prepare").length, 1);
assert.equal(upstreamSends[0].type, "response.create");
assert.equal(upstreamSends[0].input[0].content, "first");
assert.equal(upstreamSends[1].type, "response.cancel");
assert.equal(upstreamSends[1].response_id, "resp_1");
ws.close();
await close(server);
});
test("responses ws proxy closes oversized client messages with 1009", async () => {
const server = http.createServer(async (req, res) => {
const url = new URL(req.url || "/", `http://${req.headers.host}`);
if (url.pathname === "/api/internal/codex-responses-ws") {
const body = JSON.parse((await readRequestBody(req)) || "{}");
if (body.action === "authenticate") {
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: true, authenticated: true, authType: "api_key" }));
return;
}
}
res.writeHead(404, { "content-type": "application/json" });
res.end(JSON.stringify({ error: "not_found" }));
});
const port = await listen(server);
const proxy = createResponsesWsProxy({
baseUrl: `http://127.0.0.1:${port}`,
bridgeSecret: "bridge-secret",
pingIntervalMs: 1000,
idleTimeoutMs: 10000,
maxMessageBytes: 64,
wsFactory: async () => {
throw new Error("oversized input should close before upstream connect");
},
});
server.on("upgrade", async (req, socket, head) => {
const handled = await proxy.handleUpgrade(req, socket, head);
if (!handled && !socket.destroyed) {
socket.destroy();
}
});
const ws = new WebSocket(`ws://127.0.0.1:${port}/api/v1/responses?api_key=local-token`);
const closed = new Promise((resolve) => ws.addEventListener("close", resolve, { once: true }));
await new Promise((resolve) => ws.addEventListener("open", resolve, { once: true }));
ws.send(
JSON.stringify({
type: "response.create",
model: "gpt-5.5",
input: [{ role: "user", content: "x".repeat(128) }],
})
);
const event = await closed;
assert.equal(event.code, 1009);
await close(server);
});