mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 21:02:12 +03:00
* chore(release): open v3.8.22 development cycle * refactor(dashboard): extract ProviderDetailPageClient — #3501 Phase 0 (#3633) #3501 Phase 0: extract ProviderDetailPageClient + smoke test. Co-authored-by: oyi77 <oyi77@users.noreply.github.com> * refactor(dashboard): extract auth-import modals — #3501 Phase 1a (#3634) #3501 Phase 1a: extract 3 auth-import modal clusters. Co-authored-by: oyi77 <oyi77@users.noreply.github.com> * fix(db): reclassify localDb unexported modules as intentionally-internal (#3499) (#3635) Closes #3499 — reclassify localDb unexported modules as intentionally-internal (audit + honest gate framing). * refactor(db): move call_logs aggregations into callLogStats db module (#3500) (#3636) #3500 slice 1: call_logs aggregations → src/lib/db/callLogStats.ts (Rule #5). Byte-identical queries; TDD 6/6. * refactor(dashboard): extract EditCompatibleNodeModal — #3501 Phase 1b (#3638) #3501 Phase 1b: extract EditCompatibleNodeModal (cycle-safe via leaf constants module). Co-authored-by: oyi77 <oyi77@users.noreply.github.com> * refactor(db): move community_servers SQL into gamification db module (#3500 slice 3) (#3639) #3500 slice 3: community_servers SQL → gamification db module. * refactor(db): move usage_history SQL into usageAnalytics module (#3500 slice 2) (#3644) #3500 slice 2: usage_history/daily_usage_summary SQL → usageAnalytics db module. * refactor(db): move skills UPDATE + db-backups SQL into db modules (#3500 slice 5) (#3647) #3500 slice 5: skills UPDATE (allowlist) + db-backups SQL → db modules. * refactor(db): move usage_logs/semantic_cache/proxy_logs SQL into db modules (#3500 slice 4) (#3648) #3500 slice 4: usage_logs/semantic_cache/proxy_logs SQL → db modules. All internal routes done (2 external by-design remain). * chore(db-gate): reclassify external-DB reads, fully close #3500 (#3649) Closes #3500: reclassify external-DB reads; all internal raw-SQL migrated to db/ modules. * refactor(dashboard): extract pure helpers to providerPageHelpers — #3501 Phase 2 (#3653) #3501 Phase 2: extract pure helpers to providerPageHelpers (leaf, cycle-safe). Co-authored-by: oyi77 <oyi77@users.noreply.github.com> * refactor(dashboard): extract remaining shared helpers to providerPageHelpers — #3501 Phase 2b (#3658) #3501 Phase 2b: extract remaining shared helpers to providerPageHelpers (leaf, cycle-safe). Heavy modals unblocked. Co-authored-by: oyi77 <oyi77@users.noreply.github.com> * fix(reasoning): replay reasoning_content on plain DeepSeek turns (#1682) (#3632) Integrated into release/v3.8.22 * fix(kiro): route enterprise IAM Identity Center accounts to their regional endpoint (#3631) Integrated into release/v3.8.22 * refactor: small code cleanup (#3523) Integrated into release/v3.8.22 * fix(combo): skip same-provider targets on 408/500/502/503/504/524 errors (#3637) Integrated into release/v3.8.22 — circuit-breaker guard added in review (#1731v2) * feat(providers): add MiMoCode free-tier provider with bootstrap JWT auth (#3659) Integrated into release/v3.8.22 — page.tsx conflict resolved + NoAuthAccountCard re-applied to ProviderDetailPageClient in review. MiMoCode endpoint validated live. * Log Responses WebSocket calls in history (#3616) Integrated into release/v3.8.22 — Codex Responses WebSocket call history logging. * Add Claude Code routing preference for unprefixed Claude models (#3540) Integrated into release/v3.8.22 — page.tsx conflict resolved (re-applied toggle to ProviderDetailPageClient) + disable-test updated for catalog drift in review. * docs(changelog): credit #3632/#3631/#3637/#3659/#3540/#3616/#3523 (v3.8.22 targeted review round) * fix(mimocode): add required authHeader:"none" to registry entry (#3659 follow-up) The mimocode RegistryEntry omitted the required authHeader field, which broke typecheck:core (TS2741). Match the no-auth convention (authType:"none" + authHeader:"none") used by veoaifree-web and other free providers. Follow-up to #3659 (@pizzav-xyz). * fix(responses): detect stream readiness for tool-call-only and object-less chunks (#3612) (#3661) Closes #3612 * fix(mitm): remove duplicated 'Command failed:' error prefix (#3641) (#3662) Closes #3641 * fix(cli): honor HERMES_HOME for Hermes Agent config path (#3628) (#3663) Closes #3628 * fix(api): fetch live OpenCode model catalog for no-auth model picker (#3611) (#3664) Closes #3611 * fix(api): flag provider topology error state by current status, not stale history (#3619) (#3666) Closes #3619 * fix(electron): launch peer-stamping server-ws.mjs entrypoint to avoid 403 LOCAL_ONLY (#3386) (#3665) Closes #3386 * fix(dashboard): restore home topology live in-flight pulse (#3507) (#3667) Closes #3507 * fix(oauth): name Kiro/AWS auto-imported accounts and dedupe by profileArn (#3615) (#3671) Closes #3615 * fix(resilience): clear stale transient connection cooldowns on startup (#3625) (#3672) Closes #3625 * fix(i18n): use logical CSS direction utilities for sidebar and key overlays (RTL #3541) (#3670) Closes #3541 * fix(dashboard): honor auto-hide and switch to visible filter on passthrough Test-all (#3610) (#3669) Closes #3610 * refactor(dashboard): extract AddApiKeyModal + EditConnectionModal — #3501 Phase 1c (#3674) #3501 Phase 1c: extract AddApiKeyModal, EditConnectionModal, WebSessionCredentialGuide into components/; god-component 10,166->8,092 LOC. Reconciles the v3.8.22 file-size drift for this file. Co-authored-by: oyi77 <oyi77@users.noreply.github.com> * docs(changelog): reconcile v3.8.22 — credit #3621/#3622 + MiMoCode follow-up roll-up * refactor(dashboard): extract ConnectionRow + ModelCompatPopover + SiliconFlowEndpointModal — #3501 Phase 1d (#3676) #3501 Phase 1d: god-component 8,092->6,838 LOC. Co-authored-by: oyi77 <oyi77@users.noreply.github.com> * feat(obsidian): add WebDAV config route + encrypt creds at rest (#3485 part 1) (#3677) Part 1 of #3485. Adds /api/settings/obsidian/webdav (GET/POST/DELETE) wiring the ready obsidianSync lib, encrypts webdav password + obsidian token at rest, removes the duplicate UI block, drops the KNOWN_MISSING entry. WebDAV file server is part 2. * feat(obsidian): add /api/v1/webdav file server for Obsidian vault sync (#3485 part 2) (#3678) Part 2 of #3485. WebDAV server (PROPFIND/GET/PUT/DELETE/MKCOL/MOVE/OPTIONS) handled in the custom server layer (standalone-server-ws.mjs) since the App Router cannot export WebDAV methods. Basic-Auth (constant-time), path-traversal hardened, password decrypt ported from encryption.ts (parity-tested), DATA_DIR resolution parity-tested against dataPaths.ts. End-to-end Obsidian-over-Tailscale validation is a live VPS step (Rule #18). * fix(combo): stop premature context compaction — real auto-combo windows + per-target compression limit (#3680) Integrated into release/v3.8.22 * feat(dashboard): deactivate/activate accounts from the quota overview (#3675) Integrated into release/v3.8.22 * fix(dashboard): close review gaps in bulk provider connection actions (#3271 follow-up) (#3673) Integrated into release/v3.8.22 — page.tsx conflict (god-component split #3501) resolved by re-applying the bulk-action deltas to ProviderDetailPageClient.tsx * refactor(dashboard): extract useModelCompatState hook + model sections — #3501 Phase 1e (#3683) #3501 Phase 1e: extract useModelCompatState hook (unblocks the model sections) + ModelRow/PassthroughModelsSection/PassthroughModelRow/CustomModelsSection/CompatibleModelsSection. god-component 6,838->4,921 LOC. Co-authored-by: oyi77 <oyi77@users.noreply.github.com> * refactor(dashboard): extract useProviderConnections/Settings/Models hooks — #3501 Phase 1f (#3684) #3501 Phase 1f: god-component 4,948->4,062 LOC. Connection state+handlers, settings, and model metadata moved into hooks/. Co-authored-by: oyi77 <oyi77@users.noreply.github.com> * chore(release): v3.8.22 CHANGELOG + env-doc sync - Set release date in CHANGELOG [3.8.22] to 2026-06-11 - Add HERMES_HOME to .env.example (from #3628/#3663) - Add HERMES_HOME + OMNIROUTE_PREFER_CLAUDE_CODE_FOR_UNPREFIXED_CLAUDE_MODELS to ENVIRONMENT.md (#3628/#3540) * docs(changelog): credit #3673 + #3675 — leninejunior bulk-actions + quota-toggle --------- Co-authored-by: oyi77 <oyi77@users.noreply.github.com> Co-authored-by: Abhishek Divekar <adivekar@utexas.edu> Co-authored-by: NOXX - Commiter <artur1992123@mail.ru> Co-authored-by: Nicolas Lorin <androw95220@gmail.com> Co-authored-by: Hernan Javier Ardila Sanchez <hjasgr@gmail.com> Co-authored-by: PizzaV <103120356+pizzav-xyz@users.noreply.github.com> Co-authored-by: kkkayye <98376609+kkkayye@users.noreply.github.com> Co-authored-by: Witroch4 <witalo_rocha@hotmail.com> Co-authored-by: Lenine Júnior <lenine@engrene.com.br>
412 lines
13 KiB
JavaScript
412 lines
13 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",
|
|
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");
|
|
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);
|
|
});
|