mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-21 22:52:19 +03:00
feat(codex): support GPT-5.5 responses websocket (#1573)
Integrated into release/v3.7.0
This commit is contained in:
19
tests/unit/cli-model-config-schema.test.ts
Normal file
19
tests/unit/cli-model-config-schema.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { cliModelConfigSchema } from "../../src/shared/validation/schemas.ts";
|
||||
|
||||
test("cliModelConfigSchema accepts Codex xhigh reasoning effort", () => {
|
||||
const result = cliModelConfigSchema.safeParse({
|
||||
baseUrl: "http://localhost:20128/api/v1",
|
||||
apiKey: "sk_omniroute",
|
||||
model: "gpt-5.5",
|
||||
reasoningEffort: "xhigh",
|
||||
wireApi: "responses",
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
if (result.success) {
|
||||
assert.equal(result.data.reasoningEffort, "xhigh");
|
||||
}
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
CodexExecutor,
|
||||
encodeResponseSseEvent,
|
||||
getCodexModelScope,
|
||||
getCodexRateLimitKey,
|
||||
getCodexResetTime,
|
||||
@@ -272,6 +273,42 @@ test("CodexExecutor.transformRequest lets model suffix beat connection reasoning
|
||||
assert.equal(result.reasoning.effort, "high");
|
||||
});
|
||||
|
||||
test("CodexExecutor.transformRequest keeps gpt-5.5 as the model and applies xhigh reasoning", () => {
|
||||
const executor = new CodexExecutor();
|
||||
const result = executor.transformRequest(
|
||||
"gpt-5.5",
|
||||
{ model: "gpt-5.5", input: [], reasoning_effort: "xhigh" },
|
||||
false,
|
||||
{}
|
||||
);
|
||||
|
||||
assert.equal(result.model, "gpt-5.5");
|
||||
assert.equal(result.reasoning.effort, "xhigh");
|
||||
});
|
||||
|
||||
test("CodexExecutor maps Codex websocket error events to response.failed SSE", () => {
|
||||
const raw = JSON.stringify({
|
||||
type: "error",
|
||||
status_code: 429,
|
||||
error: {
|
||||
type: "usage_limit_reached",
|
||||
message: "The usage limit has been reached",
|
||||
},
|
||||
});
|
||||
|
||||
const result = encodeResponseSseEvent(raw);
|
||||
assert.equal(result.terminal, true);
|
||||
assert.match(result.sse, /^event: response\.failed/m);
|
||||
|
||||
const dataLine = result.sse.split("\n").find((line) => line.startsWith("data: "));
|
||||
assert.ok(dataLine);
|
||||
const payload = JSON.parse(dataLine.slice("data: ".length));
|
||||
assert.equal(payload.type, "response.failed");
|
||||
assert.equal(payload.response.status, "failed");
|
||||
assert.equal(payload.response.error.code, "usage_limit_reached");
|
||||
assert.equal(payload.response.error.status_code, 429);
|
||||
});
|
||||
|
||||
test("CodexExecutor.transformRequest does not apply connection reasoning defaults when Thinking Budget is not passthrough", () => {
|
||||
const executor = new CodexExecutor();
|
||||
setThinkingBudgetConfig({ mode: ThinkingMode.AUTO });
|
||||
|
||||
@@ -43,6 +43,18 @@ test("getModelInfoCore resolves codex-auto-review to codex", async () => {
|
||||
assert.equal(info.model, "codex-auto-review");
|
||||
});
|
||||
|
||||
test("getModelInfoCore resolves gpt-5.5 to codex", async () => {
|
||||
const info = await getModelInfoCore("gpt-5.5", {});
|
||||
assert.equal(info.provider, "codex");
|
||||
assert.equal(info.model, "gpt-5.5");
|
||||
});
|
||||
|
||||
test("getModelInfoCore resolves explicit gpt-5.5 Codex model", async () => {
|
||||
const info = await getModelInfoCore("cx/gpt-5.5", {});
|
||||
assert.equal(info.provider, "codex");
|
||||
assert.equal(info.model, "gpt-5.5");
|
||||
});
|
||||
|
||||
test("getModelInfoCore returns explicit ambiguity metadata for ambiguous unprefixed model", async () => {
|
||||
const info = await getModelInfoCore("claude-haiku-4.5", {});
|
||||
assert.equal(info.provider, null);
|
||||
|
||||
159
tests/unit/responses-ws-proxy.test.mjs
Normal file
159
tests/unit/responses-ws-proxy.test.mjs
Normal file
@@ -0,0 +1,159 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import http from "node:http";
|
||||
|
||||
const { createResponsesWsProxy } = await import("../../scripts/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" },
|
||||
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));
|
||||
setTimeout(() => {
|
||||
fakeUpstream.onmessage?.({
|
||||
data: JSON.stringify({
|
||||
type: "response.completed",
|
||||
response: { id: "resp_1", status: "completed" },
|
||||
}),
|
||||
});
|
||||
}, 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"));
|
||||
|
||||
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);
|
||||
|
||||
ws.close();
|
||||
await close(server);
|
||||
});
|
||||
Reference in New Issue
Block a user