feat(codex): support GPT-5.5 responses websocket (#1573)

Integrated into release/v3.7.0
This commit is contained in:
be0hhh
2026-04-24 18:44:59 +00:00
committed by GitHub
parent 31acf52d0a
commit c9aef0e595
18 changed files with 1362 additions and 9 deletions

View File

@@ -25,8 +25,9 @@ export default function CodexToolCard({
const [message, setMessage] = useState(null);
const [showInstallGuide, setShowInstallGuide] = useState(false);
const [selectedApiKey, setSelectedApiKey] = useState("");
const [selectedModel, setSelectedModel] = useState("");
const [selectedModel, setSelectedModel] = useState("gpt-5.5");
const CODEX_DEFAULT_MODELS = [
"gpt-5.5",
"gpt-5.3-codex",
"gpt-5.4",
"gpt-5.2-codex",
@@ -35,7 +36,7 @@ export default function CodexToolCard({
"gpt-5.1-codex-mini",
];
const [modelMappings, setModelMappings] = useState<Record<string, string>>({});
const [reasoningEffort, setReasoningEffort] = useState("medium");
const [reasoningEffort, setReasoningEffort] = useState("xhigh");
const [wireApi, setWireApi] = useState("chat");
const [modalOpen, setModalOpen] = useState(false);
const [modalTarget, setModalTarget] = useState<string | null>(null); // null = default model, string = mapping key
@@ -584,7 +585,7 @@ openai_base_url = "${getEffectiveBaseUrl()}"
type="text"
value={selectedModel}
onChange={(e) => setSelectedModel(e.target.value)}
placeholder="gpt-5.4"
placeholder="gpt-5.5"
className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50"
/>
{selectedModel && (
@@ -615,6 +616,7 @@ openai_base_url = "${getEffectiveBaseUrl()}"
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="xhigh">XHigh</option>
</select>
</div>

View File

@@ -0,0 +1,185 @@
import { NextResponse } from "next/server";
import { CodexExecutor } from "@omniroute/open-sse/executors/codex.ts";
import { getApiKeyMetadata } from "@/lib/db/apiKeys";
import { authorizeWebSocketHandshake, extractWsTokenFromRequest } from "@/lib/ws/handshake";
import { getModelInfo } from "@/sse/services/model";
import { getProviderCredentialsWithQuotaPreflight } from "@/sse/services/auth";
import { checkAndRefreshToken } from "@/sse/services/tokenRefresh";
const CODEX_RESPONSES_WS_URL = "wss://chatgpt.com/backend-api/codex/responses";
const executor = new CodexExecutor();
type JsonRecord = Record<string, unknown>;
function isRecord(value: unknown): value is JsonRecord {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function getBridgeSecret(): string {
return process.env.OMNIROUTE_WS_BRIDGE_SECRET || "";
}
function getAuthRequest(body: JsonRecord): Request {
const requestUrl = typeof body.requestUrl === "string" ? body.requestUrl : "/api/v1/responses";
const headers = isRecord(body.headers) ? body.headers : {};
const url = new URL(requestUrl, "http://omniroute.local");
const requestHeaders = new Headers();
for (const [key, value] of Object.entries(headers)) {
if (typeof value === "string") {
requestHeaders.set(key, value);
}
}
return new Request(url, { headers: requestHeaders });
}
function jsonError(status: number, code: string, message: string) {
return NextResponse.json(
{
error: {
code,
message,
},
},
{ status }
);
}
function normalizeUpstreamHeaders(headers: Record<string, string>): Record<string, string> {
const result: Record<string, string> = {};
for (const [key, value] of Object.entries(headers)) {
const lower = key.toLowerCase();
if (
lower === "host" ||
lower === "connection" ||
lower === "upgrade" ||
lower === "sec-websocket-key" ||
lower === "sec-websocket-version" ||
lower === "sec-websocket-extensions"
) {
continue;
}
result[key] = value;
}
result.Origin = "https://chatgpt.com";
return result;
}
async function authenticate(body: JsonRecord) {
const authRequest = getAuthRequest(body);
const auth = await authorizeWebSocketHandshake(authRequest);
if (!auth.authorized) {
return jsonError(
auth.hasCredential ? 403 : 401,
auth.hasCredential ? "ws_auth_invalid" : "ws_auth_required",
auth.hasCredential ? "Invalid WebSocket credential" : "WebSocket auth required"
);
}
return NextResponse.json({
ok: true,
authenticated: auth.authenticated,
authType: auth.authType,
wsAuth: auth.wsAuth,
});
}
async function prepare(body: JsonRecord) {
const authResponse = await authenticate(body);
if (!authResponse.ok) return authResponse;
const authRequest = getAuthRequest(body);
const apiKey = extractWsTokenFromRequest(authRequest);
const metadata = apiKey ? await getApiKeyMetadata(apiKey).catch(() => null) : null;
const allowedConnections =
metadata && Array.isArray(metadata.allowedConnections) && metadata.allowedConnections.length > 0
? metadata.allowedConnections
: null;
const responseBody = isRecord(body.response) ? body.response : {};
const requestedModel =
typeof responseBody.model === "string" && responseBody.model.trim()
? responseBody.model.trim()
: "gpt-5.5";
const modelInfo = await getModelInfo(requestedModel);
const provider = modelInfo.provider;
const model = modelInfo.model || requestedModel;
if (provider !== "codex") {
return jsonError(
400,
"codex_ws_provider_required",
`Responses WebSocket bridge only supports Codex models, got ${provider || "unknown"}`
);
}
const credentials = await getProviderCredentialsWithQuotaPreflight(
provider,
null,
allowedConnections,
model
);
if (!credentials || "allRateLimited" in credentials) {
return jsonError(
503,
"codex_credentials_unavailable",
"No available Codex OAuth connection for Responses WebSocket"
);
}
const refreshedCredentials = await checkAndRefreshToken(provider, credentials);
if (!refreshedCredentials?.accessToken) {
return jsonError(401, "codex_oauth_token_missing", "Codex OAuth access token is missing");
}
const transformed = (await executor.transformRequest(
model,
responseBody,
true,
refreshedCredentials
)) as JsonRecord;
transformed.model = model;
delete transformed.stream;
delete transformed.stream_options;
const headers = normalizeUpstreamHeaders(executor.buildHeaders(refreshedCredentials, true));
return NextResponse.json({
ok: true,
upstreamUrl: CODEX_RESPONSES_WS_URL,
browser: "chrome_142",
os: "windows",
connectionId: refreshedCredentials.connectionId,
model,
headers,
response: transformed,
});
}
export async function POST(request: Request) {
const expectedSecret = getBridgeSecret();
const receivedSecret = request.headers.get("x-omniroute-ws-bridge-secret") || "";
if (!expectedSecret || receivedSecret !== expectedSecret) {
return jsonError(403, "internal_bridge_forbidden", "Forbidden");
}
let body: JsonRecord;
try {
const parsed = await request.json();
body = isRecord(parsed) ? parsed : {};
} catch {
return jsonError(400, "invalid_json", "Request body must be JSON");
}
const action = typeof body.action === "string" ? body.action : "";
if (action === "authenticate") {
return authenticate(body);
}
if (action === "prepare") {
return prepare(body);
}
return jsonError(400, "invalid_action", "Unsupported bridge action");
}

View File

@@ -1710,7 +1710,7 @@ export const cliModelConfigSchema = z.object({
baseUrl: z.string().trim().min(1, "baseUrl and model are required"),
apiKey: z.string().optional(),
model: z.string().trim().min(1, "baseUrl and model are required"),
reasoningEffort: z.enum(["none", "low", "medium", "high"]).optional(),
reasoningEffort: z.enum(["none", "low", "medium", "high", "xhigh"]).optional(),
wireApi: z.enum(["chat", "responses"]).optional(),
modelMappings: z.record(z.string().trim().min(1), z.string().trim().min(1)).optional(),
});