Skip to content

Commit 3b766d9

Browse files
junjun
authored andcommitted
fix(server): join GPT-Live sideband on the API host, not backend-api
openai/codex builds the sideband WebSocket from the ApiKey provider default (api.openai.com/v1) even for ChatGPT-auth calls created via backend-api; chatgpt.com/backend-api rejects sideband upgrades before 101. Route backend-shape sideband joins to wss://api.openai.com/v1 (live/{id}, realtime/calls/{id}, realtime?call_id=) with the same relayed pool auth. Verified live: session.started received end-to-end.
1 parent 75344b0 commit 3b766d9

2 files changed

Lines changed: 36 additions & 13 deletions

File tree

src/server/live.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ const LIVE_RELAY_HEADERS = ["content-type", "location"] as const;
4545
/** AVAS WebRTC call-create query (openai/codex `configure_realtime_call_request`). */
4646
export const LIVE_AVAS_QUERY = "intent=quicksilver&architecture=avas";
4747

48+
/**
49+
* Sideband WebSocket API root. openai/codex joins the sideband via the API provider default
50+
* (`to_api_provider(AuthMode::ApiKey)` → https://api.openai.com/v1) even for ChatGPT-auth calls
51+
* created through backend-api; chatgpt.com/backend-api rejects sideband upgrades pre-101
52+
* (verified live 2026-07-24). The call-create bearer works on the API host unchanged.
53+
*/
54+
export const LIVE_SIDEBAND_API_ROOT = "https://api.openai.com/v1";
55+
4856
/**
4957
* Client protocol headers relayed verbatim to the upstream on call-create and sideband upgrade.
5058
* `openai-alpha: quicksilver=v2` carries the Frameless protocol negotiation — without it the
@@ -142,26 +150,29 @@ export function buildLiveSidebandUpstreamWsUrl(
142150
target: LiveSidebandTarget,
143151
): string {
144152
const root = providerBaseUrl.replace(/\/$/, "");
153+
if (usesBackendShape) {
154+
// ChatGPT backend-api call-create, but the sideband join lives on the public API host
155+
// (matches openai/codex, which builds the sideband from the ApiKey provider default).
156+
if (target.style === "frameless-path") {
157+
return httpsToWss(`${LIVE_SIDEBAND_API_ROOT}/live/${target.callId}`);
158+
}
159+
if (target.style === "realtime-calls-path") {
160+
return httpsToWss(`${LIVE_SIDEBAND_API_ROOT}/realtime/calls/${target.callId}`);
161+
}
162+
return httpsToWss(
163+
`${LIVE_SIDEBAND_API_ROOT}/realtime?intent=quicksilver&call_id=${encodeURIComponent(target.callId)}`,
164+
);
165+
}
145166
if (target.style === "frameless-path") {
146167
// Frameless: normalize to .../live then append /{callId}.
147-
// Backend bases like .../backend-api/codex keep their path and append /{callId}.
148-
if (usesBackendShape) {
149-
return httpsToWss(`${root}/${target.callId}`);
150-
}
151168
const apiRoot = root.replace(/\/v1\/?$/, "");
152169
return httpsToWss(`${apiRoot}/v1/live/${target.callId}`);
153170
}
154171
if (target.style === "realtime-calls-path") {
155-
if (usesBackendShape) {
156-
return httpsToWss(`${root}/realtime/calls/${target.callId}`);
157-
}
158172
const apiRoot = root.replace(/\/v1\/?$/, "");
159173
return httpsToWss(`${apiRoot}/v1/realtime/calls/${target.callId}`);
160174
}
161175
// Realtime v1/v2: /v1/realtime?intent=quicksilver&call_id=
162-
if (usesBackendShape) {
163-
return httpsToWss(`${root}/realtime?intent=quicksilver&call_id=${encodeURIComponent(target.callId)}`);
164-
}
165176
const apiRoot = root.replace(/\/v1\/?$/, "");
166177
return httpsToWss(
167178
`${apiRoot}/v1/realtime?intent=quicksilver&call_id=${encodeURIComponent(target.callId)}`,

tests/server-live.test.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,7 @@ test("sideband GET /v1/live/{callId} upgrades and relays bidirectionally to Chat
498498
constructor(url: string | URL, protocols?: string | string[] | Record<string, unknown>) {
499499
const parsed = new URL(String(url));
500500
const target =
501-
parsed.hostname === "chatgpt.com" && parsed.pathname.startsWith("/backend-api/codex/")
501+
parsed.hostname === "api.openai.com" && parsed.pathname.startsWith("/v1/live/")
502502
? `ws://127.0.0.1:${upstreamPort}${parsed.pathname}${parsed.search}`
503503
: String(url);
504504
super(target, protocols as string[]);
@@ -526,7 +526,7 @@ test("sideband GET /v1/live/{callId} upgrades and relays bidirectionally to Chat
526526
client.addEventListener("message", (event) => {
527527
try {
528528
expect(String(event.data)).toBe("echo:ping-sideband");
529-
expect(seenPaths).toContain("/backend-api/codex/rtc_sideband");
529+
expect(seenPaths).toContain("/v1/live/rtc_sideband");
530530
expect(seenUpgradeHeaders).toHaveLength(1);
531531
expect(seenUpgradeHeaders[0].get("openai-alpha")).toBe("quicksilver=v2");
532532
expect(seenUpgradeHeaders[0].get("x-session-id")).toBe("rts_side");
@@ -576,7 +576,19 @@ test("buildLiveSidebandUpstreamWsUrl maps Frameless and Realtime join shapes", a
576576
style: "frameless-path",
577577
callId: "rtc_1",
578578
}),
579-
).toBe("wss://chatgpt.com/backend-api/codex/rtc_1");
579+
).toBe("wss://api.openai.com/v1/live/rtc_1");
580+
expect(
581+
buildLiveSidebandUpstreamWsUrl("https://chatgpt.com/backend-api/codex", true, {
582+
style: "realtime-calls-path",
583+
callId: "rtc_1",
584+
}),
585+
).toBe("wss://api.openai.com/v1/realtime/calls/rtc_1");
586+
expect(
587+
buildLiveSidebandUpstreamWsUrl("https://chatgpt.com/backend-api/codex", true, {
588+
style: "realtime-query",
589+
callId: "rtc_2",
590+
}),
591+
).toBe("wss://api.openai.com/v1/realtime?intent=quicksilver&call_id=rtc_2");
580592
expect(
581593
buildLiveSidebandUpstreamWsUrl("https://api.openai.com/v1", false, {
582594
style: "frameless-path",

0 commit comments

Comments
 (0)