
























@@ -133,6 +133,52 @@ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
133133});
134134}
135135136+function resolveRequestTimeoutMs(options?: OpenAICodexResponsesOptions): number | undefined {
137+const timeoutMs = options?.timeoutMs;
138+return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0
139+ ? Math.floor(timeoutMs)
140+ : undefined;
141+}
142+143+function buildRequestSignal(
144+baseSignal: AbortSignal | undefined,
145+timeoutMs: number | undefined,
146+): AbortSignal | undefined {
147+if (timeoutMs === undefined) {
148+return baseSignal;
149+}
150+const timeoutSignal = AbortSignal.timeout(timeoutMs);
151+if (!baseSignal) {
152+return timeoutSignal;
153+}
154+return AbortSignal.any([baseSignal, timeoutSignal]);
155+}
156+157+function isRequestTimeoutError(
158+error: unknown,
159+callerSignal: AbortSignal | undefined,
160+requestSignal: AbortSignal | undefined,
161+timeoutMs: number | undefined,
162+): boolean {
163+if (timeoutMs === undefined || callerSignal?.aborted || !requestSignal?.aborted) {
164+return false;
165+}
166+if (!(error instanceof Error)) {
167+return false;
168+}
169+return (
170+error.name === "AbortError" ||
171+error.name === "TimeoutError" ||
172+error.message === "Request was aborted"
173+);
174+}
175+176+function formatRequestTimeoutError(timeoutMs: number, cause: unknown): Error {
177+return new Error(`Request timed out after ${timeoutMs}ms`, {
178+cause: cause instanceof Error ? cause : undefined,
179+});
180+}
181+136182// ============================================================================
137183// Main Stream Function
138184// ============================================================================
@@ -148,6 +194,8 @@ export const streamOpenAICodexResponses: StreamFunction<
148194const stream = new AssistantMessageEventStream();
149195150196void (async () => {
197+let requestTimeoutMs: number | undefined;
198+let activeSignal: AbortSignal | undefined;
151199const output: AssistantMessage = {
152200role: "assistant",
153201content: [],
@@ -194,6 +242,10 @@ export const streamOpenAICodexResponses: StreamFunction<
194242websocketRequestId,
195243);
196244const bodyJson = JSON.stringify(body);
245+requestTimeoutMs = resolveRequestTimeoutMs(options);
246+activeSignal = buildRequestSignal(options?.signal, requestTimeoutMs);
247+const requestOptions =
248+activeSignal === options?.signal ? options : { ...options, signal: activeSignal };
197249const transport = options?.transport || "auto";
198250const websocketDisabledForSession =
199251transport === "auto" && isWebSocketSseFallbackActive(options?.sessionId);
@@ -214,10 +266,10 @@ export const streamOpenAICodexResponses: StreamFunction<
214266() => {
215267websocketStarted = true;
216268},
217-options,
269+requestOptions,
218270);
219271220-if (options?.signal?.aborted) {
272+if (activeSignal?.aborted) {
221273throw new Error("Request was aborted");
222274}
223275stream.push({
@@ -228,7 +280,7 @@ export const streamOpenAICodexResponses: StreamFunction<
228280stream.end();
229281return;
230282} catch (error) {
231-const aborted = options?.signal?.aborted;
283+const aborted = activeSignal?.aborted;
232284if (aborted || isCodexNonTransportError(error)) {
233285throw error;
234286}
@@ -259,7 +311,7 @@ export const streamOpenAICodexResponses: StreamFunction<
259311let lastError: Error | undefined;
260312261313for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
262-if (options?.signal?.aborted) {
314+if (activeSignal?.aborted) {
263315throw new Error("Request was aborted");
264316}
265317@@ -268,7 +320,7 @@ export const streamOpenAICodexResponses: StreamFunction<
268320method: "POST",
269321headers: sseHeaders,
270322body: bodyJson,
271-signal: options?.signal,
323+signal: activeSignal,
272324});
273325await options?.onResponse?.(
274326{ status: response.status, headers: headersToRecord(response.headers) },
@@ -304,7 +356,7 @@ export const streamOpenAICodexResponses: StreamFunction<
304356}
305357}
306358307-await sleep(delayMs, options?.signal);
359+await sleep(delayMs, activeSignal);
308360continue;
309361}
310362@@ -317,15 +369,24 @@ export const streamOpenAICodexResponses: StreamFunction<
317369throw new Error(info.friendlyMessage || info.message);
318370} catch (error) {
319371if (error instanceof Error) {
372+if (
373+isRequestTimeoutError(error, options?.signal, activeSignal, requestTimeoutMs) &&
374+requestTimeoutMs !== undefined
375+) {
376+throw formatRequestTimeoutError(requestTimeoutMs, error);
377+}
320378if (error.name === "AbortError" || error.message === "Request was aborted") {
321379throw new Error("Request was aborted", { cause: error });
322380}
381+if (error.name === "TimeoutError" && requestTimeoutMs !== undefined) {
382+throw new Error(`Request timed out after ${requestTimeoutMs}ms`, { cause: error });
383+}
323384}
324385lastError = error instanceof Error ? error : new Error(String(error));
325386// Network errors are retryable
326387if (attempt < MAX_RETRIES && !lastError.message.includes("usage limit")) {
327388const delayMs = BASE_DELAY_MS * 2 ** attempt;
328-await sleep(delayMs, options?.signal);
389+await sleep(delayMs, activeSignal);
329390continue;
330391}
331392throw lastError;
@@ -343,7 +404,7 @@ export const streamOpenAICodexResponses: StreamFunction<
343404stream.push({ type: "start", partial: output });
344405await processStream(response, output, stream, model, options);
345406346-if (options?.signal?.aborted) {
407+if (activeSignal?.aborted) {
347408throw new Error("Request was aborted");
348409}
349410@@ -354,12 +415,18 @@ export const streamOpenAICodexResponses: StreamFunction<
354415});
355416stream.end();
356417} catch (error) {
418+const normalizedError =
419+isRequestTimeoutError(error, options?.signal, activeSignal, requestTimeoutMs) &&
420+requestTimeoutMs !== undefined
421+ ? formatRequestTimeoutError(requestTimeoutMs, error)
422+ : error;
357423for (const block of output.content) {
358424// partialJson is only a streaming scratch buffer; never persist it.
359425delete (block as { partialJson?: string }).partialJson;
360426}
361427output.stopReason = options?.signal?.aborted ? "aborted" : "error";
362-output.errorMessage = error instanceof Error ? error.message : String(error);
428+output.errorMessage =
429+normalizedError instanceof Error ? normalizedError.message : String(normalizedError);
363430stream.push({ type: "error", reason: output.stopReason, error: output });
364431stream.end();
365432}
@@ -975,6 +1042,11 @@ async function connectWebSocket(
9751042signal?.removeEventListener("abort", onAbort);
9761043};
97710441045+if (signal?.aborted) {
1046+onAbort();
1047+return;
1048+}
1049+9781050socket.addEventListener("open", onOpen);
9791051socket.addEventListener("error", onError);
9801052socket.addEventListener("close", onClose);
@@ -1374,6 +1446,9 @@ async function processWebSocketStream(
13741446}
13751447}
13761448try {
1449+if (options?.signal?.aborted) {
1450+throw new Error("Request was aborted");
1451+}
13771452socket.send(JSON.stringify({ type: "response.create", ...requestBody }));
13781453await processResponsesStream(
13791454startWebSocketOutputOnFirstEvent(
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。