[codex] HTTP-first Codex upstream websocket settings (#280)
* http-first codex upstream websocket settings * address websocket runtime review comments --------- Co-authored-by: apple <apple@appledeMacBook-Pro.local>
This commit is contained in:
@@ -139,7 +139,7 @@ describe('codexWebsocketRuntime', () => {
|
||||
await runtime.closeSession('exec-session-close');
|
||||
});
|
||||
|
||||
it('treats response.incomplete as a terminal websocket failure instead of waiting for socket close', async () => {
|
||||
it('returns response.incomplete as a terminal websocket event without rejecting the session turn', async () => {
|
||||
upstreamMessageHandler = (socket, parsed) => {
|
||||
socket.send(JSON.stringify({
|
||||
type: 'response.incomplete',
|
||||
@@ -154,37 +154,224 @@ describe('codexWebsocketRuntime', () => {
|
||||
}));
|
||||
};
|
||||
|
||||
const { createCodexWebsocketRuntime, CodexWebsocketRuntimeError } = await import('./codexWebsocketRuntime.js');
|
||||
const { createCodexWebsocketRuntime } = await import('./codexWebsocketRuntime.js');
|
||||
const runtime = createCodexWebsocketRuntime();
|
||||
|
||||
let error: unknown;
|
||||
try {
|
||||
await runtime.sendRequest({
|
||||
sessionId: 'exec-session-incomplete',
|
||||
requestUrl: upstreamWsUrl,
|
||||
headers: {
|
||||
Authorization: 'Bearer oauth-access-token',
|
||||
'OpenAI-Beta': 'responses_websockets=2026-02-06',
|
||||
},
|
||||
body: {
|
||||
model: 'gpt-5.4',
|
||||
input: [],
|
||||
},
|
||||
});
|
||||
} catch (caught) {
|
||||
error = caught;
|
||||
}
|
||||
|
||||
expect(error).toBeInstanceOf(CodexWebsocketRuntimeError);
|
||||
expect(error).toMatchObject({
|
||||
message: 'max_output_tokens',
|
||||
status: 502,
|
||||
const result = await runtime.sendRequest({
|
||||
sessionId: 'exec-session-incomplete',
|
||||
requestUrl: upstreamWsUrl,
|
||||
headers: {
|
||||
Authorization: 'Bearer oauth-access-token',
|
||||
'OpenAI-Beta': 'responses_websockets=2026-02-06',
|
||||
},
|
||||
body: {
|
||||
model: 'gpt-5.4',
|
||||
input: [],
|
||||
},
|
||||
});
|
||||
expect((error as CodexWebsocketRuntimeError).events).toEqual([
|
||||
|
||||
expect(result.events).toEqual([
|
||||
expect.objectContaining({
|
||||
type: 'response.incomplete',
|
||||
}),
|
||||
]);
|
||||
expect(result.reusedSession).toBe(false);
|
||||
|
||||
await runtime.closeSession('exec-session-incomplete');
|
||||
});
|
||||
|
||||
it('keeps the upstream websocket session alive across response.failed terminal turns', async () => {
|
||||
upstreamMessageHandler = (socket, parsed, requestIndex) => {
|
||||
if (requestIndex === 1) {
|
||||
socket.send(JSON.stringify({
|
||||
type: 'response.failed',
|
||||
response: {
|
||||
id: 'resp-failed',
|
||||
model: parsed.model || 'gpt-5.4',
|
||||
status: 'failed',
|
||||
error: {
|
||||
message: 'tool execution failed',
|
||||
type: 'server_error',
|
||||
},
|
||||
},
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
socket.send(JSON.stringify({
|
||||
type: 'response.completed',
|
||||
response: {
|
||||
id: `resp-${requestIndex}`,
|
||||
object: 'response',
|
||||
model: parsed.model || 'gpt-5.4',
|
||||
status: 'completed',
|
||||
output: [],
|
||||
usage: {
|
||||
input_tokens: 1,
|
||||
output_tokens: 1,
|
||||
total_tokens: 2,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const { createCodexWebsocketRuntime } = await import('./codexWebsocketRuntime.js');
|
||||
const runtime = createCodexWebsocketRuntime();
|
||||
|
||||
const first = await runtime.sendRequest({
|
||||
sessionId: 'exec-session-failed-turn',
|
||||
requestUrl: upstreamWsUrl,
|
||||
headers: {
|
||||
Authorization: 'Bearer oauth-access-token',
|
||||
'OpenAI-Beta': 'responses_websockets=2026-02-06',
|
||||
},
|
||||
body: {
|
||||
model: 'gpt-5.4',
|
||||
input: [],
|
||||
},
|
||||
});
|
||||
|
||||
const second = await runtime.sendRequest({
|
||||
sessionId: 'exec-session-failed-turn',
|
||||
requestUrl: upstreamWsUrl,
|
||||
headers: {
|
||||
Authorization: 'Bearer oauth-access-token',
|
||||
'OpenAI-Beta': 'responses_websockets=2026-02-06',
|
||||
},
|
||||
body: {
|
||||
model: 'gpt-5.4',
|
||||
previous_response_id: 'resp-failed',
|
||||
input: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(first.events).toEqual([
|
||||
expect.objectContaining({
|
||||
type: 'response.failed',
|
||||
response: expect.objectContaining({
|
||||
id: 'resp-failed',
|
||||
error: expect.objectContaining({
|
||||
message: 'tool execution failed',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
expect(first.reusedSession).toBe(false);
|
||||
expect(second.events[0]).toMatchObject({
|
||||
type: 'response.completed',
|
||||
response: { id: 'resp-2' },
|
||||
});
|
||||
expect(second.reusedSession).toBe(true);
|
||||
expect(upstreamConnectionCount).toBe(1);
|
||||
expect(upstreamRequests).toHaveLength(2);
|
||||
|
||||
await runtime.closeSession('exec-session-failed-turn');
|
||||
});
|
||||
|
||||
it('fails the current turn and opens a fresh websocket on the next turn when a reused session closes before yielding any events', async () => {
|
||||
upstreamMessageHandler = (socket, parsed, requestIndex) => {
|
||||
if (requestIndex === 1) {
|
||||
socket.send(JSON.stringify({
|
||||
type: 'response.completed',
|
||||
response: {
|
||||
id: 'resp-1',
|
||||
object: 'response',
|
||||
model: parsed.model || 'gpt-5.4',
|
||||
status: 'completed',
|
||||
output: [],
|
||||
usage: {
|
||||
input_tokens: 1,
|
||||
output_tokens: 1,
|
||||
total_tokens: 2,
|
||||
},
|
||||
},
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (requestIndex === 2) {
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
socket.send(JSON.stringify({
|
||||
type: 'response.completed',
|
||||
response: {
|
||||
id: 'resp-3',
|
||||
object: 'response',
|
||||
model: parsed.model || 'gpt-5.4',
|
||||
status: 'completed',
|
||||
output: [],
|
||||
usage: {
|
||||
input_tokens: 1,
|
||||
output_tokens: 1,
|
||||
total_tokens: 2,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const { createCodexWebsocketRuntime } = await import('./codexWebsocketRuntime.js');
|
||||
const runtime = createCodexWebsocketRuntime();
|
||||
|
||||
await runtime.sendRequest({
|
||||
sessionId: 'exec-session-retry-stale',
|
||||
requestUrl: upstreamWsUrl,
|
||||
headers: {
|
||||
Authorization: 'Bearer oauth-access-token',
|
||||
'OpenAI-Beta': 'responses_websockets=2026-02-06',
|
||||
},
|
||||
body: {
|
||||
model: 'gpt-5.4',
|
||||
input: [],
|
||||
},
|
||||
});
|
||||
|
||||
await expect(runtime.sendRequest({
|
||||
sessionId: 'exec-session-retry-stale',
|
||||
requestUrl: upstreamWsUrl,
|
||||
headers: {
|
||||
Authorization: 'Bearer oauth-access-token',
|
||||
'OpenAI-Beta': 'responses_websockets=2026-02-06',
|
||||
},
|
||||
body: {
|
||||
model: 'gpt-5.4',
|
||||
previous_response_id: 'resp-1',
|
||||
input: [],
|
||||
},
|
||||
})).rejects.toThrow('stream closed before response.completed');
|
||||
|
||||
const recovered = await runtime.sendRequest({
|
||||
sessionId: 'exec-session-retry-stale',
|
||||
requestUrl: upstreamWsUrl,
|
||||
headers: {
|
||||
Authorization: 'Bearer oauth-access-token',
|
||||
'OpenAI-Beta': 'responses_websockets=2026-02-06',
|
||||
},
|
||||
body: {
|
||||
model: 'gpt-5.4',
|
||||
previous_response_id: 'resp-1',
|
||||
input: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(recovered.events[0]).toMatchObject({
|
||||
type: 'response.completed',
|
||||
response: { id: 'resp-3' },
|
||||
});
|
||||
expect(recovered.reusedSession).toBe(false);
|
||||
expect(upstreamConnectionCount).toBe(2);
|
||||
expect(upstreamRequests).toHaveLength(3);
|
||||
expect(upstreamRequests[1]).toMatchObject({
|
||||
type: 'response.create',
|
||||
previous_response_id: 'resp-1',
|
||||
});
|
||||
expect(upstreamRequests[2]).toMatchObject({
|
||||
type: 'response.create',
|
||||
previous_response_id: 'resp-1',
|
||||
});
|
||||
|
||||
await runtime.closeSession('exec-session-retry-stale');
|
||||
});
|
||||
|
||||
it('treats top-level error frames as terminal websocket failures', async () => {
|
||||
|
||||
@@ -29,9 +29,9 @@ function isTerminalEvent(payload: Record<string, unknown>): boolean {
|
||||
|| type === 'error';
|
||||
}
|
||||
|
||||
function isFailureTerminalEvent(payload: Record<string, unknown>): boolean {
|
||||
function isRuntimeErrorEvent(payload: Record<string, unknown>): boolean {
|
||||
const type = asTrimmedString(payload.type);
|
||||
return type === 'response.failed' || type === 'response.incomplete' || type === 'error';
|
||||
return type === 'error';
|
||||
}
|
||||
|
||||
function asFiniteNumber(value: unknown): number | undefined {
|
||||
@@ -262,7 +262,7 @@ async function sendSessionRequest(
|
||||
events.push(parsed);
|
||||
if (!isTerminalEvent(parsed)) return;
|
||||
if (settled) return;
|
||||
if (isFailureTerminalEvent(parsed)) {
|
||||
if (isRuntimeErrorEvent(parsed)) {
|
||||
settled = true;
|
||||
cleanup();
|
||||
clearSessionSocket(session, socket);
|
||||
|
||||
@@ -264,6 +264,7 @@ export async function handleChatSurfaceRequest(
|
||||
};
|
||||
let startTime = Date.now();
|
||||
const leaseResult = await acquireSurfaceChannelLease({
|
||||
stickySessionKey,
|
||||
selected,
|
||||
});
|
||||
if (leaseResult.status === 'timeout') {
|
||||
@@ -789,6 +790,7 @@ export async function handleClaudeCountTokensSurfaceRequest(
|
||||
const oauth = getOauthInfoFromAccount(selected.account);
|
||||
const startTime = Date.now();
|
||||
const leaseResult = await acquireSurfaceChannelLease({
|
||||
stickySessionKey,
|
||||
selected,
|
||||
});
|
||||
if (leaseResult.status === 'timeout') {
|
||||
|
||||
@@ -380,6 +380,7 @@ export async function handleOpenAiResponsesSurfaceRequest(
|
||||
|
||||
const startTime = Date.now();
|
||||
const leaseResult = await acquireSurfaceChannelLease({
|
||||
stickySessionKey,
|
||||
selected,
|
||||
});
|
||||
if (leaseResult.status === 'timeout') {
|
||||
|
||||
@@ -187,13 +187,17 @@ export function clearSurfaceStickyChannel(input: {
|
||||
}
|
||||
|
||||
export async function acquireSurfaceChannelLease(input: {
|
||||
stickySessionKey?: string | null;
|
||||
selected: {
|
||||
channel: { id: number };
|
||||
account?: { extraConfig?: string | null } | null;
|
||||
};
|
||||
}) {
|
||||
return await proxyChannelCoordinator.acquireChannelLease({
|
||||
channelId: input.selected.channel.id,
|
||||
// Only session-addressable requests should consume the guarded per-channel
|
||||
// lease pool. Requests without a stable downstream session key should keep
|
||||
// the pre-sticky-session parallel behavior instead of contending globally.
|
||||
channelId: input.stickySessionKey ? input.selected.channel.id : 0,
|
||||
accountExtraConfig: input.selected.account?.extraConfig,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user