fix: address codex review findings
This commit is contained in:
@@ -147,6 +147,9 @@ describe('codexWebsocketRuntime', () => {
|
||||
id: 'resp-incomplete',
|
||||
model: parsed.model || 'gpt-5.4',
|
||||
status: 'incomplete',
|
||||
incomplete_details: {
|
||||
reason: 'max_output_tokens',
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
@@ -154,18 +157,34 @@ describe('codexWebsocketRuntime', () => {
|
||||
const { createCodexWebsocketRuntime, CodexWebsocketRuntimeError } = await import('./codexWebsocketRuntime.js');
|
||||
const runtime = createCodexWebsocketRuntime();
|
||||
|
||||
await expect(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: [],
|
||||
},
|
||||
})).rejects.toBeInstanceOf(CodexWebsocketRuntimeError);
|
||||
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,
|
||||
});
|
||||
expect((error as CodexWebsocketRuntimeError).events).toEqual([
|
||||
expect.objectContaining({
|
||||
type: 'response.incomplete',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('treats top-level error frames as terminal websocket failures', async () => {
|
||||
@@ -182,17 +201,36 @@ describe('codexWebsocketRuntime', () => {
|
||||
const { createCodexWebsocketRuntime, CodexWebsocketRuntimeError } = await import('./codexWebsocketRuntime.js');
|
||||
const runtime = createCodexWebsocketRuntime();
|
||||
|
||||
await expect(runtime.sendRequest({
|
||||
sessionId: 'exec-session-error',
|
||||
requestUrl: upstreamWsUrl,
|
||||
headers: {
|
||||
Authorization: 'Bearer oauth-access-token',
|
||||
'OpenAI-Beta': 'responses_websockets=2026-02-06',
|
||||
},
|
||||
body: {
|
||||
model: 'gpt-5.4',
|
||||
input: [],
|
||||
},
|
||||
})).rejects.toBeInstanceOf(CodexWebsocketRuntimeError);
|
||||
let error: unknown;
|
||||
try {
|
||||
await runtime.sendRequest({
|
||||
sessionId: 'exec-session-error',
|
||||
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: 'account mismatch',
|
||||
status: 502,
|
||||
});
|
||||
expect((error as CodexWebsocketRuntimeError).events).toEqual([
|
||||
expect.objectContaining({
|
||||
type: 'error',
|
||||
error: expect.objectContaining({
|
||||
message: 'account mismatch',
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,6 +34,32 @@ function isFailureTerminalEvent(payload: Record<string, unknown>): boolean {
|
||||
return type === 'response.failed' || type === 'response.incomplete' || type === 'error';
|
||||
}
|
||||
|
||||
function asFiniteNumber(value: unknown): number | undefined {
|
||||
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
||||
}
|
||||
|
||||
function extractFailureTerminalStatus(payload: Record<string, unknown>): number {
|
||||
const response = isRecord(payload.response) ? payload.response : null;
|
||||
const responseError = response && isRecord(response.error) ? response.error : null;
|
||||
const topLevelError = isRecord(payload.error) ? payload.error : null;
|
||||
const candidates = [
|
||||
payload.status,
|
||||
payload.statusCode,
|
||||
payload.code,
|
||||
topLevelError?.status,
|
||||
topLevelError?.statusCode,
|
||||
topLevelError?.code,
|
||||
responseError?.status,
|
||||
responseError?.statusCode,
|
||||
responseError?.code,
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
const status = asFiniteNumber(candidate);
|
||||
if (status !== undefined) return status;
|
||||
}
|
||||
return 502;
|
||||
}
|
||||
|
||||
function extractTerminalErrorMessage(payload: Record<string, unknown>): string {
|
||||
const type = asTrimmedString(payload.type);
|
||||
if (type === 'error' && isRecord(payload.error)) {
|
||||
@@ -243,7 +269,7 @@ async function sendSessionRequest(
|
||||
void closeSocket(socket);
|
||||
reject(new CodexWebsocketRuntimeError(extractTerminalErrorMessage(parsed), {
|
||||
events: [...events],
|
||||
status: typeof parsed.status === 'number' && Number.isFinite(parsed.status) ? parsed.status : undefined,
|
||||
status: extractFailureTerminalStatus(parsed),
|
||||
payload: parsed,
|
||||
}));
|
||||
return;
|
||||
|
||||
@@ -135,6 +135,19 @@ function carriesResponsesFileUrlInput(value: unknown): boolean {
|
||||
return Object.values(value).some((entry) => carriesResponsesFileUrlInput(entry));
|
||||
}
|
||||
|
||||
function shouldRefreshOauthResponsesRequest(input: {
|
||||
oauthProvider?: string;
|
||||
status: number;
|
||||
response: Response;
|
||||
rawErrText: string;
|
||||
}): boolean {
|
||||
if (input.status === 401) return true;
|
||||
if (input.status !== 403 || input.oauthProvider !== 'codex') return false;
|
||||
const authenticate = input.response.headers.get('www-authenticate') || '';
|
||||
const combined = `${authenticate}\n${input.rawErrText || ''}`;
|
||||
return /\b(invalid_token|expired_token|expired|invalid|unauthorized|account mismatch|authentication)\b/i.test(combined);
|
||||
}
|
||||
|
||||
type UsageSummary = ReturnType<typeof parseProxyUsage>;
|
||||
|
||||
export async function handleOpenAiResponsesSurfaceRequest(
|
||||
@@ -314,7 +327,12 @@ export async function handleOpenAiResponsesSurfaceRequest(
|
||||
dispatchRequest,
|
||||
});
|
||||
const tryRecover = async (ctx: Parameters<NonNullable<typeof endpointStrategy.tryRecover>>[0]) => {
|
||||
if ((ctx.response.status === 401 || ctx.response.status === 403) && oauth) {
|
||||
if (oauth && shouldRefreshOauthResponsesRequest({
|
||||
oauthProvider: oauth.provider,
|
||||
status: ctx.response.status,
|
||||
response: ctx.response,
|
||||
rawErrText: ctx.rawErrText || '',
|
||||
})) {
|
||||
try {
|
||||
const refreshed = await refreshOauthAccessTokenSingleflight(selected.account.id);
|
||||
selected.tokenValue = refreshed.accessToken;
|
||||
|
||||
Reference in New Issue
Block a user