@opengeni/codex 0.2.3 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-K47573CJ.js → chunk-YGKQDUY7.js} +14 -2
- package/dist/{chunk-K47573CJ.js.map → chunk-YGKQDUY7.js.map} +1 -1
- package/dist/constants.d.ts +8 -2
- package/dist/constants.js +13 -1
- package/dist/index.d.ts +231 -5
- package/dist/index.js +1402 -91
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/api-client.ts +131 -22
- package/src/bounded-operation.ts +43 -0
- package/src/constants.ts +20 -2
- package/src/fetch.ts +866 -49
- package/src/index.ts +3 -0
- package/src/model-output-truncation.ts +590 -0
- package/src/oversized-image-card.ts +18 -0
- package/src/refresh.ts +21 -10
- package/src/request-context.ts +38 -0
- package/src/reset-credits.ts +157 -0
- package/src/response-timeout.ts +159 -0
- package/src/usage-normalize.ts +20 -2
package/src/fetch.ts
CHANGED
|
@@ -10,13 +10,24 @@
|
|
|
10
10
|
// - retries once on 401 after a forced token refresh (spec §1.9)
|
|
11
11
|
// Stream parsing is delegated to the SDK (SSE passthrough; spec §0(d)).
|
|
12
12
|
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
13
14
|
import { CODEX_ORIGINATOR } from "./constants";
|
|
14
15
|
import { normalizeCodexRequestBody } from "./normalize";
|
|
15
16
|
import {
|
|
16
17
|
codexRequestStorage,
|
|
18
|
+
type CodexModelRequestEvent,
|
|
19
|
+
type CodexRequestContext,
|
|
20
|
+
type CodexResponseTimeoutPolicy,
|
|
17
21
|
type CodexTokenSnapshot,
|
|
18
22
|
type CodexUsageHeaderSnapshot,
|
|
19
23
|
} from "./request-context";
|
|
24
|
+
import {
|
|
25
|
+
CODEX_RESPONSE_TIMEOUT_ERROR_TYPE,
|
|
26
|
+
CodexResponseTimeoutError,
|
|
27
|
+
classifyCodexResponseTimeoutError,
|
|
28
|
+
isPreHeadersTimeoutError,
|
|
29
|
+
resolveCodexResponseTimeoutPolicy,
|
|
30
|
+
} from "./response-timeout";
|
|
20
31
|
|
|
21
32
|
export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
22
33
|
|
|
@@ -118,6 +129,252 @@ export function parseCodexUsageHeaders(headers: Headers): CodexUsageHeaderSnapsh
|
|
|
118
129
|
};
|
|
119
130
|
}
|
|
120
131
|
|
|
132
|
+
type RequestAudit = {
|
|
133
|
+
ctx: CodexRequestContext;
|
|
134
|
+
requestId: string;
|
|
135
|
+
transportAttempt: number;
|
|
136
|
+
model?: string;
|
|
137
|
+
logicalStartedAt: number;
|
|
138
|
+
attemptStartedAt: number;
|
|
139
|
+
policy: CodexResponseTimeoutPolicy;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
async function emitRequestEvent(
|
|
143
|
+
audit: RequestAudit,
|
|
144
|
+
event: Omit<
|
|
145
|
+
CodexModelRequestEvent,
|
|
146
|
+
"requestId" | "transportAttempt" | "model" | "durationMs" | "timeoutPolicy"
|
|
147
|
+
>,
|
|
148
|
+
): Promise<void> {
|
|
149
|
+
await audit.ctx.onModelRequestEvent?.({
|
|
150
|
+
requestId: audit.requestId,
|
|
151
|
+
transportAttempt: audit.transportAttempt,
|
|
152
|
+
...(audit.model ? { model: audit.model } : {}),
|
|
153
|
+
durationMs: Math.max(0, Date.now() - audit.attemptStartedAt),
|
|
154
|
+
timeoutPolicy: audit.policy,
|
|
155
|
+
...event,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function providerRequestId(headers: Headers): string | undefined {
|
|
160
|
+
return headers.get("x-request-id") ?? headers.get("request-id") ?? undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function fetchBeforeHeaders(
|
|
164
|
+
base: FetchLike,
|
|
165
|
+
input: string,
|
|
166
|
+
init: RequestInit,
|
|
167
|
+
audit: RequestAudit,
|
|
168
|
+
): Promise<Response> {
|
|
169
|
+
const elapsed = Date.now() - audit.logicalStartedAt;
|
|
170
|
+
const wholeRemainingMs = audit.policy.wholeRequestTimeoutMs - elapsed;
|
|
171
|
+
const timeoutClass =
|
|
172
|
+
wholeRemainingMs <= audit.policy.headersTimeoutMs ? "whole_request" : "headers";
|
|
173
|
+
const deadlineMs = Math.max(1, Math.min(audit.policy.headersTimeoutMs, wholeRemainingMs));
|
|
174
|
+
if (wholeRemainingMs <= 0) {
|
|
175
|
+
throw new CodexResponseTimeoutError("whole_request", audit.requestId, false);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const externalSignal = init.signal;
|
|
179
|
+
if (externalSignal?.aborted) throw externalSignal.reason;
|
|
180
|
+
const controller = new AbortController();
|
|
181
|
+
const forwardAbort = () => controller.abort(externalSignal?.reason);
|
|
182
|
+
externalSignal?.addEventListener("abort", forwardAbort, { once: true });
|
|
183
|
+
const basePromise = base(input, { ...init, signal: controller.signal });
|
|
184
|
+
let deadlineError: CodexResponseTimeoutError | null = null;
|
|
185
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
186
|
+
const deadline = new Promise<never>((_resolve, reject) => {
|
|
187
|
+
timer = setTimeout(() => {
|
|
188
|
+
deadlineError = new CodexResponseTimeoutError(timeoutClass, audit.requestId, false);
|
|
189
|
+
reject(deadlineError);
|
|
190
|
+
}, deadlineMs);
|
|
191
|
+
});
|
|
192
|
+
try {
|
|
193
|
+
return await Promise.race([basePromise, deadline]);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
if (deadlineError) {
|
|
196
|
+
controller.abort(deadlineError);
|
|
197
|
+
void basePromise
|
|
198
|
+
.then((late) => late.body?.cancel(deadlineError ?? undefined))
|
|
199
|
+
.catch(() => undefined);
|
|
200
|
+
throw deadlineError;
|
|
201
|
+
}
|
|
202
|
+
throw error;
|
|
203
|
+
} finally {
|
|
204
|
+
if (timer) clearTimeout(timer);
|
|
205
|
+
externalSignal?.removeEventListener("abort", forwardAbort);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function observedResponse(
|
|
210
|
+
res: Response,
|
|
211
|
+
audit: RequestAudit,
|
|
212
|
+
externalSignal: AbortSignal | null | undefined,
|
|
213
|
+
): Promise<Response> {
|
|
214
|
+
const requestId = providerRequestId(res.headers);
|
|
215
|
+
if (!res.body) {
|
|
216
|
+
await emitRequestEvent(audit, {
|
|
217
|
+
phase: res.ok ? "completed" : "failed",
|
|
218
|
+
responseObserved: true,
|
|
219
|
+
status: res.status,
|
|
220
|
+
...(requestId ? { providerRequestId: requestId } : {}),
|
|
221
|
+
});
|
|
222
|
+
return res;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const reader = res.body.getReader();
|
|
226
|
+
let terminal = false;
|
|
227
|
+
let firstByte = false;
|
|
228
|
+
let idleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
229
|
+
let wholeTimer: ReturnType<typeof setTimeout> | undefined;
|
|
230
|
+
let armIdle: () => void = () => undefined;
|
|
231
|
+
let abortFromOutside: (() => void) | undefined;
|
|
232
|
+
|
|
233
|
+
const clearTimers = () => {
|
|
234
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
235
|
+
if (wholeTimer) clearTimeout(wholeTimer);
|
|
236
|
+
if (abortFromOutside) externalSignal?.removeEventListener("abort", abortFromOutside);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const body = new ReadableStream<Uint8Array>({
|
|
240
|
+
start(controller) {
|
|
241
|
+
const timeOut = (klass: "idle_stream" | "whole_request") => {
|
|
242
|
+
if (terminal) return;
|
|
243
|
+
terminal = true;
|
|
244
|
+
clearTimers();
|
|
245
|
+
const error = new CodexResponseTimeoutError(klass, audit.requestId, true);
|
|
246
|
+
void reader.cancel(error).catch(() => undefined);
|
|
247
|
+
void emitRequestEvent(audit, {
|
|
248
|
+
phase: "timed_out",
|
|
249
|
+
responseObserved: true,
|
|
250
|
+
timeoutClass: klass,
|
|
251
|
+
status: res.status,
|
|
252
|
+
...(requestId ? { providerRequestId: requestId } : {}),
|
|
253
|
+
}).then(
|
|
254
|
+
() => controller.error(error),
|
|
255
|
+
() => controller.error(error),
|
|
256
|
+
);
|
|
257
|
+
};
|
|
258
|
+
armIdle = () => {
|
|
259
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
260
|
+
idleTimer = setTimeout(() => timeOut("idle_stream"), audit.policy.streamIdleTimeoutMs);
|
|
261
|
+
};
|
|
262
|
+
armIdle();
|
|
263
|
+
const wholeRemaining = Math.max(
|
|
264
|
+
1,
|
|
265
|
+
audit.policy.wholeRequestTimeoutMs - (Date.now() - audit.logicalStartedAt),
|
|
266
|
+
);
|
|
267
|
+
wholeTimer = setTimeout(() => timeOut("whole_request"), wholeRemaining);
|
|
268
|
+
abortFromOutside = () => {
|
|
269
|
+
if (terminal) return;
|
|
270
|
+
terminal = true;
|
|
271
|
+
clearTimers();
|
|
272
|
+
const reason = externalSignal?.reason ?? new DOMException("Aborted", "AbortError");
|
|
273
|
+
void reader.cancel(reason).catch(() => undefined);
|
|
274
|
+
void emitRequestEvent(audit, {
|
|
275
|
+
phase: "failed",
|
|
276
|
+
responseObserved: true,
|
|
277
|
+
status: res.status,
|
|
278
|
+
...(requestId ? { providerRequestId: requestId } : {}),
|
|
279
|
+
}).then(
|
|
280
|
+
() => controller.error(reason),
|
|
281
|
+
() => controller.error(reason),
|
|
282
|
+
);
|
|
283
|
+
};
|
|
284
|
+
if (externalSignal?.aborted) {
|
|
285
|
+
abortFromOutside();
|
|
286
|
+
} else {
|
|
287
|
+
externalSignal?.addEventListener("abort", abortFromOutside, { once: true });
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
async pull(controller) {
|
|
291
|
+
if (terminal) return;
|
|
292
|
+
try {
|
|
293
|
+
const chunk = await reader.read();
|
|
294
|
+
if (terminal) return;
|
|
295
|
+
if (chunk.done) {
|
|
296
|
+
terminal = true;
|
|
297
|
+
clearTimers();
|
|
298
|
+
await emitRequestEvent(audit, {
|
|
299
|
+
phase: res.ok ? "completed" : "failed",
|
|
300
|
+
responseObserved: true,
|
|
301
|
+
status: res.status,
|
|
302
|
+
...(requestId ? { providerRequestId: requestId } : {}),
|
|
303
|
+
});
|
|
304
|
+
controller.close();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
armIdle();
|
|
308
|
+
if (!firstByte) {
|
|
309
|
+
firstByte = true;
|
|
310
|
+
await emitRequestEvent(audit, {
|
|
311
|
+
phase: "first_byte",
|
|
312
|
+
responseObserved: true,
|
|
313
|
+
status: res.status,
|
|
314
|
+
...(requestId ? { providerRequestId: requestId } : {}),
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
controller.enqueue(chunk.value);
|
|
318
|
+
} catch (error) {
|
|
319
|
+
if (terminal) return;
|
|
320
|
+
terminal = true;
|
|
321
|
+
clearTimers();
|
|
322
|
+
await emitRequestEvent(audit, {
|
|
323
|
+
phase: "failed",
|
|
324
|
+
responseObserved: true,
|
|
325
|
+
status: res.status,
|
|
326
|
+
...(requestId ? { providerRequestId: requestId } : {}),
|
|
327
|
+
});
|
|
328
|
+
controller.error(error);
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
async cancel(reason) {
|
|
332
|
+
if (!terminal) {
|
|
333
|
+
terminal = true;
|
|
334
|
+
clearTimers();
|
|
335
|
+
await emitRequestEvent(audit, {
|
|
336
|
+
phase: "failed",
|
|
337
|
+
responseObserved: true,
|
|
338
|
+
status: res.status,
|
|
339
|
+
...(requestId ? { providerRequestId: requestId } : {}),
|
|
340
|
+
}).catch(() => undefined);
|
|
341
|
+
}
|
|
342
|
+
await reader.cancel(reason).catch(() => undefined);
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
const headers = new Headers(res.headers);
|
|
346
|
+
headers.delete("content-length");
|
|
347
|
+
return new Response(body, { status: res.status, statusText: res.statusText, headers });
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function timeoutErrorResponse(info: {
|
|
351
|
+
timeoutClass: "connect" | "headers" | "idle_stream" | "whole_request";
|
|
352
|
+
requestId: string;
|
|
353
|
+
responseObserved: boolean;
|
|
354
|
+
message: string;
|
|
355
|
+
}): Response {
|
|
356
|
+
return new Response(
|
|
357
|
+
JSON.stringify({
|
|
358
|
+
error: {
|
|
359
|
+
type: CODEX_RESPONSE_TIMEOUT_ERROR_TYPE,
|
|
360
|
+
code: CODEX_RESPONSE_TIMEOUT_ERROR_TYPE,
|
|
361
|
+
message: info.message,
|
|
362
|
+
timeout_class: info.timeoutClass,
|
|
363
|
+
response_observed: info.responseObserved,
|
|
364
|
+
request_id: info.requestId,
|
|
365
|
+
},
|
|
366
|
+
}),
|
|
367
|
+
{
|
|
368
|
+
status: 504,
|
|
369
|
+
headers: {
|
|
370
|
+
"content-type": "application/json",
|
|
371
|
+
"x-should-retry": "false",
|
|
372
|
+
[CODEX_TRANSPORT_ERROR_HEADER]: "1",
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
121
378
|
export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): FetchLike {
|
|
122
379
|
return async (input, init) => {
|
|
123
380
|
const ctx = codexRequestStorage.getStore();
|
|
@@ -131,7 +388,15 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
|
|
|
131
388
|
// URLs whose base already includes /codex (avoids /codex/codex/responses).
|
|
132
389
|
const rewritten = rawUrl.replace(/(?<!\/codex)\/responses(\b|$)/, "/codex/responses$1");
|
|
133
390
|
|
|
134
|
-
const
|
|
391
|
+
const policy = resolveCodexResponseTimeoutPolicy(ctx.responseTimeoutPolicy);
|
|
392
|
+
const requestId = ctx.nextRequestId?.() ?? randomUUID();
|
|
393
|
+
const logicalStartedAt = Date.now();
|
|
394
|
+
let transportAttempt = 0;
|
|
395
|
+
|
|
396
|
+
const attempt = async (
|
|
397
|
+
auth: CodexTokenSnapshot,
|
|
398
|
+
authenticationAttempt: number,
|
|
399
|
+
): Promise<Response> => {
|
|
135
400
|
const headers = new Headers(init?.headers);
|
|
136
401
|
headers.set("Authorization", `Bearer ${auth.accessToken}`);
|
|
137
402
|
if (auth.chatgptAccountId) {
|
|
@@ -158,26 +423,90 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
|
|
|
158
423
|
// the caller's intent so a non-streaming caller (e.g. the compaction
|
|
159
424
|
// summarizer) still gets a single JSON Response back.
|
|
160
425
|
let callerWantsStream = true;
|
|
426
|
+
let model: string | undefined;
|
|
161
427
|
const nextInit: RequestInit = { ...init, headers };
|
|
162
428
|
if (typeof init?.body === "string") {
|
|
163
429
|
try {
|
|
164
430
|
const parsed = JSON.parse(init.body) as Record<string, unknown>;
|
|
165
431
|
callerWantsStream = parsed.stream === true;
|
|
166
|
-
|
|
432
|
+
const normalized = normalizeCodexRequestBody(parsed, ctx.resolveModel);
|
|
433
|
+
model = typeof normalized.model === "string" ? normalized.model : undefined;
|
|
434
|
+
nextInit.body = JSON.stringify(normalized);
|
|
167
435
|
} catch {
|
|
168
436
|
/* leave unparseable bodies untouched (already copied from init) */
|
|
169
437
|
}
|
|
170
438
|
}
|
|
439
|
+
headers.set(
|
|
440
|
+
"Idempotency-Key",
|
|
441
|
+
authenticationAttempt === 0 ? requestId : `${requestId}:auth-${authenticationAttempt}`,
|
|
442
|
+
);
|
|
171
443
|
if (process.env.CODEX_DEBUG) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
444
|
+
let keys: string[] = [];
|
|
445
|
+
if (typeof nextInit.body === "string") {
|
|
446
|
+
try {
|
|
447
|
+
keys = Object.keys(JSON.parse(nextInit.body) as Record<string, unknown>);
|
|
448
|
+
} catch {
|
|
449
|
+
/* an unparseable body is already passed through unchanged above */
|
|
450
|
+
}
|
|
451
|
+
}
|
|
176
452
|
console.error(
|
|
177
453
|
`[codex-debug] POST ${rewritten} stream=${callerWantsStream} bodyKeys=[${keys.join(",")}]`,
|
|
178
454
|
);
|
|
179
455
|
}
|
|
180
|
-
|
|
456
|
+
let res: Response;
|
|
457
|
+
transportAttempt += 1;
|
|
458
|
+
const audit: RequestAudit = {
|
|
459
|
+
ctx,
|
|
460
|
+
requestId,
|
|
461
|
+
transportAttempt,
|
|
462
|
+
...(model ? { model } : {}),
|
|
463
|
+
logicalStartedAt,
|
|
464
|
+
attemptStartedAt: Date.now(),
|
|
465
|
+
policy,
|
|
466
|
+
};
|
|
467
|
+
await emitRequestEvent(audit, {
|
|
468
|
+
phase: "started",
|
|
469
|
+
responseObserved: false,
|
|
470
|
+
});
|
|
471
|
+
try {
|
|
472
|
+
res = await fetchBeforeHeaders(base, rewritten, nextInit, audit);
|
|
473
|
+
const upstreamRequestId = providerRequestId(res.headers);
|
|
474
|
+
await emitRequestEvent(audit, {
|
|
475
|
+
phase: "headers",
|
|
476
|
+
responseObserved: true,
|
|
477
|
+
status: res.status,
|
|
478
|
+
...(upstreamRequestId ? { providerRequestId: upstreamRequestId } : {}),
|
|
479
|
+
});
|
|
480
|
+
const observed = await observedResponse(res, audit, nextInit.signal);
|
|
481
|
+
res = observed;
|
|
482
|
+
} catch (error) {
|
|
483
|
+
if (nextInit.signal?.aborted) {
|
|
484
|
+
await emitRequestEvent(audit, {
|
|
485
|
+
phase: "failed",
|
|
486
|
+
responseObserved: false,
|
|
487
|
+
}).catch(() => undefined);
|
|
488
|
+
throw error;
|
|
489
|
+
}
|
|
490
|
+
const klass = isPreHeadersTimeoutError(error);
|
|
491
|
+
if (!klass) {
|
|
492
|
+
await emitRequestEvent(audit, {
|
|
493
|
+
phase: "failed",
|
|
494
|
+
responseObserved: false,
|
|
495
|
+
});
|
|
496
|
+
throw error;
|
|
497
|
+
}
|
|
498
|
+
// An absent response does not prove that the provider never accepted
|
|
499
|
+
// this operation. Until a provider-specific receipt can prove
|
|
500
|
+
// non-acceptance or resume the same operation, never replay it.
|
|
501
|
+
// Audit persistence must not replace the typed transport timeout.
|
|
502
|
+
await emitRequestEvent(audit, {
|
|
503
|
+
phase: "timed_out",
|
|
504
|
+
responseObserved: false,
|
|
505
|
+
timeoutClass: klass,
|
|
506
|
+
willRetry: false,
|
|
507
|
+
}).catch(() => undefined);
|
|
508
|
+
throw new CodexResponseTimeoutError(klass, requestId, false);
|
|
509
|
+
}
|
|
181
510
|
// Multi-account P4 (Part A): scrape the usage headers ONCE, before the
|
|
182
511
|
// OK/!res.ok branch, so the same fire-and-forget read also covers the 429
|
|
183
512
|
// hard-cap path (an exhausted serving account stamps its own fresh
|
|
@@ -216,11 +545,22 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
|
|
|
216
545
|
return callerWantsStream ? repairCodexStream(res) : await sseToJsonResponse(res);
|
|
217
546
|
};
|
|
218
547
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
548
|
+
try {
|
|
549
|
+
let res = await attempt(await ctx.getToken(), 0);
|
|
550
|
+
if (res.status === 401) {
|
|
551
|
+
res = await attempt(await ctx.refresh(), 1); // single refresh-on-401 retry (spec §1.9)
|
|
552
|
+
}
|
|
553
|
+
return res;
|
|
554
|
+
} catch (error) {
|
|
555
|
+
const timeout = classifyCodexResponseTimeoutError(error);
|
|
556
|
+
if (!timeout) throw error;
|
|
557
|
+
return timeoutErrorResponse({
|
|
558
|
+
timeoutClass: timeout.timeoutClass,
|
|
559
|
+
requestId: timeout.requestId ?? requestId,
|
|
560
|
+
responseObserved: timeout.responseObserved,
|
|
561
|
+
message: timeout.message,
|
|
562
|
+
});
|
|
222
563
|
}
|
|
223
|
-
return res;
|
|
224
564
|
};
|
|
225
565
|
}
|
|
226
566
|
|
|
@@ -292,7 +632,11 @@ async function bufferCodexErrorResponse(res: Response): Promise<Response> {
|
|
|
292
632
|
if (errorType === CODEX_USAGE_LIMIT_ERROR_TYPE) {
|
|
293
633
|
headers.set("x-should-retry", "false");
|
|
294
634
|
}
|
|
295
|
-
return new Response(bodyText, {
|
|
635
|
+
return new Response(bodyText, {
|
|
636
|
+
status: res.status,
|
|
637
|
+
statusText: res.statusText,
|
|
638
|
+
headers,
|
|
639
|
+
});
|
|
296
640
|
}
|
|
297
641
|
|
|
298
642
|
/**
|
|
@@ -303,14 +647,10 @@ async function bufferCodexErrorResponse(res: Response): Promise<Response> {
|
|
|
303
647
|
async function sseToJsonResponse(res: Response): Promise<Response> {
|
|
304
648
|
const text = await res.text();
|
|
305
649
|
let final: Record<string, unknown> | null = null;
|
|
650
|
+
let terminalError: Response | null = null;
|
|
306
651
|
const items: unknown[] = []; // assembled from output_item.done (the codex backend
|
|
307
652
|
// leaves response.completed.response.output empty and emits the items separately).
|
|
308
|
-
for (const
|
|
309
|
-
const data = block
|
|
310
|
-
.split("\n")
|
|
311
|
-
.filter((l) => l.startsWith("data:"))
|
|
312
|
-
.map((l) => l.slice(5).trim())
|
|
313
|
-
.join("\n");
|
|
653
|
+
for (const data of sseDataPayloads(text)) {
|
|
314
654
|
if (!data || data === "[DONE]") {
|
|
315
655
|
continue;
|
|
316
656
|
}
|
|
@@ -318,21 +658,79 @@ async function sseToJsonResponse(res: Response): Promise<Response> {
|
|
|
318
658
|
const ev = JSON.parse(data) as {
|
|
319
659
|
type?: string;
|
|
320
660
|
response?: Record<string, unknown>;
|
|
661
|
+
error?: unknown;
|
|
662
|
+
code?: unknown;
|
|
663
|
+
message?: unknown;
|
|
664
|
+
param?: unknown;
|
|
321
665
|
item?: unknown;
|
|
322
666
|
};
|
|
323
667
|
if (ev.type === "response.output_item.done" && ev.item !== undefined) {
|
|
324
668
|
items.push(ev.item);
|
|
325
|
-
} else if (
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
669
|
+
} else if (ev.type === "response.failed") {
|
|
670
|
+
terminalError = codexSseFailureResponse(
|
|
671
|
+
res,
|
|
672
|
+
ev.response?.error,
|
|
673
|
+
"response_failed",
|
|
674
|
+
"The Codex response failed",
|
|
675
|
+
{
|
|
676
|
+
eventType: ev.type,
|
|
677
|
+
responseId: ev.response?.id,
|
|
678
|
+
responseStatus: ev.response?.status,
|
|
679
|
+
},
|
|
680
|
+
);
|
|
681
|
+
} else if (ev.type === "error" || ev.type === "response.error") {
|
|
682
|
+
terminalError = codexSseFailureResponse(
|
|
683
|
+
res,
|
|
684
|
+
ev.error ?? ev.response?.error ?? ev,
|
|
685
|
+
"response_error",
|
|
686
|
+
"The Codex response stream reported an error",
|
|
687
|
+
{
|
|
688
|
+
eventType: ev.type,
|
|
689
|
+
responseId: ev.response?.id,
|
|
690
|
+
responseStatus: ev.response?.status,
|
|
691
|
+
},
|
|
692
|
+
);
|
|
693
|
+
} else if (ev.type === "response.incomplete") {
|
|
694
|
+
const details = ev.response?.incomplete_details;
|
|
695
|
+
const reason =
|
|
696
|
+
details && typeof details === "object"
|
|
697
|
+
? (details as Record<string, unknown>).reason
|
|
698
|
+
: undefined;
|
|
699
|
+
terminalError = codexSseFailureResponse(
|
|
700
|
+
res,
|
|
701
|
+
{
|
|
702
|
+
code: "response_incomplete",
|
|
703
|
+
message:
|
|
704
|
+
typeof reason === "string" && reason.length > 0
|
|
705
|
+
? `The Codex response was incomplete (${reason})`
|
|
706
|
+
: "The Codex response was incomplete",
|
|
707
|
+
},
|
|
708
|
+
"response_incomplete",
|
|
709
|
+
"The Codex response was incomplete",
|
|
710
|
+
{
|
|
711
|
+
eventType: ev.type,
|
|
712
|
+
responseId: ev.response?.id,
|
|
713
|
+
responseStatus: ev.response?.status,
|
|
714
|
+
},
|
|
715
|
+
);
|
|
716
|
+
} else if (ev.type === "response.completed" || ev.type === "response.done") {
|
|
330
717
|
final = ev.response ?? null;
|
|
331
718
|
}
|
|
332
719
|
} catch {
|
|
333
720
|
/* ignore non-JSON keepalive lines */
|
|
334
721
|
}
|
|
335
722
|
}
|
|
723
|
+
if (terminalError) {
|
|
724
|
+
return terminalError;
|
|
725
|
+
}
|
|
726
|
+
if (!final) {
|
|
727
|
+
return codexSseFailureResponse(
|
|
728
|
+
res,
|
|
729
|
+
null,
|
|
730
|
+
"invalid_sse_terminal",
|
|
731
|
+
"The Codex response stream ended without a terminal response",
|
|
732
|
+
);
|
|
733
|
+
}
|
|
336
734
|
if (final && items.length > 0) {
|
|
337
735
|
final = { ...final, output: items }; // prefer the assembled items over an empty output array
|
|
338
736
|
}
|
|
@@ -344,7 +742,269 @@ async function sseToJsonResponse(res: Response): Promise<Response> {
|
|
|
344
742
|
const headers = new Headers(res.headers);
|
|
345
743
|
headers.set("content-type", "application/json");
|
|
346
744
|
headers.delete("content-length");
|
|
347
|
-
return new Response(JSON.stringify(final
|
|
745
|
+
return new Response(JSON.stringify(final), { status: 200, headers });
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const NON_RETRYABLE_SSE_ERROR_CODES = new Set([
|
|
749
|
+
"bio_policy",
|
|
750
|
+
"context_length_exceeded",
|
|
751
|
+
"cyber_policy",
|
|
752
|
+
"insufficient_quota",
|
|
753
|
+
"invalid_prompt",
|
|
754
|
+
"usage_limit_reached",
|
|
755
|
+
]);
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Project the data payloads from a complete SSE body. EventSource accepts LF,
|
|
759
|
+
* CRLF, and bare CR line endings; splitting only on `\n\n` can therefore merge
|
|
760
|
+
* a standards-valid terminal failure into the preceding event and silently
|
|
761
|
+
* turn it into `{}`. Preserve the SSE rule that multiple data lines are joined
|
|
762
|
+
* with `\n`, and tolerate a final event without a trailing blank line as the
|
|
763
|
+
* previous transport parser did.
|
|
764
|
+
*/
|
|
765
|
+
function sseDataPayloads(text: string): string[] {
|
|
766
|
+
const payloads: string[] = [];
|
|
767
|
+
let dataLines: string[] = [];
|
|
768
|
+
const dispatch = () => {
|
|
769
|
+
if (dataLines.length > 0) payloads.push(dataLines.join("\n"));
|
|
770
|
+
dataLines = [];
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
for (const line of text.split(/\r\n|\r|\n/)) {
|
|
774
|
+
if (line === "") {
|
|
775
|
+
dispatch();
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
if (line === "data") {
|
|
779
|
+
dataLines.push("");
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
if (!line.startsWith("data:")) continue;
|
|
783
|
+
const value = line.slice(5);
|
|
784
|
+
dataLines.push(value.startsWith(" ") ? value.slice(1) : value);
|
|
785
|
+
}
|
|
786
|
+
dispatch();
|
|
787
|
+
return payloads;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const CODEX_TERMINAL_ERROR_FIELD_MAX_BYTES = 256;
|
|
791
|
+
const CODEX_TERMINAL_ERROR_MESSAGE_MAX_BYTES = 4 * 1024;
|
|
792
|
+
const CODEX_TERMINAL_ERROR_TRUNCATION_MARKER = "… [truncated]";
|
|
793
|
+
|
|
794
|
+
function boundedTerminalErrorField(
|
|
795
|
+
value: unknown,
|
|
796
|
+
maxBytes: number,
|
|
797
|
+
): { value?: string; truncated: boolean } {
|
|
798
|
+
if (typeof value !== "string") return { truncated: false };
|
|
799
|
+
const encoder = new TextEncoder();
|
|
800
|
+
const encoded = encoder.encode(value);
|
|
801
|
+
if (encoded.byteLength <= maxBytes) return { value, truncated: false };
|
|
802
|
+
|
|
803
|
+
const markerBytes = encoder.encode(CODEX_TERMINAL_ERROR_TRUNCATION_MARKER).byteLength;
|
|
804
|
+
let prefixEnd = Math.max(0, maxBytes - markerBytes);
|
|
805
|
+
while (prefixEnd > 0 && (encoded[prefixEnd]! & 0xc0) === 0x80) {
|
|
806
|
+
prefixEnd -= 1;
|
|
807
|
+
}
|
|
808
|
+
return {
|
|
809
|
+
value: `${new TextDecoder().decode(encoded.subarray(0, prefixEnd))}${CODEX_TERMINAL_ERROR_TRUNCATION_MARKER}`,
|
|
810
|
+
truncated: true,
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Convert a terminal error carried inside an HTTP-200 SSE stream into the
|
|
816
|
+
* ordinary non-2xx JSON error contract expected by the OpenAI SDK. Codex CLI
|
|
817
|
+
* treats the same events as provider failures; returning a successful `{}`
|
|
818
|
+
* loses the actual cause and makes compaction look semantically empty.
|
|
819
|
+
*/
|
|
820
|
+
function codexSseFailureResponse(
|
|
821
|
+
source: Response,
|
|
822
|
+
rawError: unknown,
|
|
823
|
+
fallbackCode: string,
|
|
824
|
+
fallbackMessage: string,
|
|
825
|
+
metadata: {
|
|
826
|
+
eventType?: unknown;
|
|
827
|
+
responseId?: unknown;
|
|
828
|
+
responseStatus?: unknown;
|
|
829
|
+
} = {},
|
|
830
|
+
): Response {
|
|
831
|
+
const projection = codexSseFailureProjection(
|
|
832
|
+
source,
|
|
833
|
+
rawError,
|
|
834
|
+
fallbackCode,
|
|
835
|
+
fallbackMessage,
|
|
836
|
+
metadata,
|
|
837
|
+
);
|
|
838
|
+
return new Response(JSON.stringify({ error: projection.error }), {
|
|
839
|
+
status: projection.status,
|
|
840
|
+
headers: projection.headers,
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
export type CodexSseFailureProjection = {
|
|
845
|
+
status: number;
|
|
846
|
+
error: {
|
|
847
|
+
type: string;
|
|
848
|
+
code: string;
|
|
849
|
+
message: string;
|
|
850
|
+
param?: string;
|
|
851
|
+
event_type?: string;
|
|
852
|
+
response_id?: string;
|
|
853
|
+
response_status?: string;
|
|
854
|
+
diagnostic_truncated?: true;
|
|
855
|
+
};
|
|
856
|
+
headers: Headers;
|
|
857
|
+
};
|
|
858
|
+
|
|
859
|
+
function codexSseFailureProjection(
|
|
860
|
+
source: Response,
|
|
861
|
+
rawError: unknown,
|
|
862
|
+
fallbackCode: string,
|
|
863
|
+
fallbackMessage: string,
|
|
864
|
+
metadata: {
|
|
865
|
+
eventType?: unknown;
|
|
866
|
+
responseId?: unknown;
|
|
867
|
+
responseStatus?: unknown;
|
|
868
|
+
} = {},
|
|
869
|
+
): CodexSseFailureProjection {
|
|
870
|
+
const record =
|
|
871
|
+
rawError && typeof rawError === "object" && !Array.isArray(rawError)
|
|
872
|
+
? (rawError as Record<string, unknown>)
|
|
873
|
+
: {};
|
|
874
|
+
const typeField = boundedTerminalErrorField(record.type, CODEX_TERMINAL_ERROR_FIELD_MAX_BYTES);
|
|
875
|
+
const codeField = boundedTerminalErrorField(record.code, CODEX_TERMINAL_ERROR_FIELD_MAX_BYTES);
|
|
876
|
+
const messageField = boundedTerminalErrorField(
|
|
877
|
+
record.message ?? (typeof rawError === "string" ? rawError : undefined),
|
|
878
|
+
CODEX_TERMINAL_ERROR_MESSAGE_MAX_BYTES,
|
|
879
|
+
);
|
|
880
|
+
const paramField = boundedTerminalErrorField(record.param, CODEX_TERMINAL_ERROR_FIELD_MAX_BYTES);
|
|
881
|
+
const eventTypeField = boundedTerminalErrorField(
|
|
882
|
+
metadata.eventType,
|
|
883
|
+
CODEX_TERMINAL_ERROR_FIELD_MAX_BYTES,
|
|
884
|
+
);
|
|
885
|
+
const responseIdField = boundedTerminalErrorField(
|
|
886
|
+
metadata.responseId,
|
|
887
|
+
CODEX_TERMINAL_ERROR_FIELD_MAX_BYTES,
|
|
888
|
+
);
|
|
889
|
+
const responseStatusField = boundedTerminalErrorField(
|
|
890
|
+
metadata.responseStatus,
|
|
891
|
+
CODEX_TERMINAL_ERROR_FIELD_MAX_BYTES,
|
|
892
|
+
);
|
|
893
|
+
const providerType =
|
|
894
|
+
typeField.value === "error" ||
|
|
895
|
+
typeField.value === "response.error" ||
|
|
896
|
+
typeField.value === "response.failed"
|
|
897
|
+
? undefined
|
|
898
|
+
: typeField.value;
|
|
899
|
+
const code =
|
|
900
|
+
(codeField.value?.length ? codeField.value : undefined) ??
|
|
901
|
+
(providerType?.length ? providerType : undefined) ??
|
|
902
|
+
fallbackCode;
|
|
903
|
+
const diagnosticTruncated =
|
|
904
|
+
typeField.truncated ||
|
|
905
|
+
codeField.truncated ||
|
|
906
|
+
messageField.truncated ||
|
|
907
|
+
paramField.truncated ||
|
|
908
|
+
eventTypeField.truncated ||
|
|
909
|
+
responseIdField.truncated ||
|
|
910
|
+
responseStatusField.truncated ||
|
|
911
|
+
Object.keys(record).some((key) => !["type", "code", "message", "param"].includes(key)) ||
|
|
912
|
+
(rawError !== null &&
|
|
913
|
+
rawError !== undefined &&
|
|
914
|
+
typeof rawError !== "string" &&
|
|
915
|
+
(typeof rawError !== "object" || Array.isArray(rawError)));
|
|
916
|
+
const error: CodexSseFailureProjection["error"] = {
|
|
917
|
+
type: providerType?.length ? providerType : code,
|
|
918
|
+
code,
|
|
919
|
+
message: messageField.value?.length ? messageField.value : fallbackMessage,
|
|
920
|
+
...(paramField.value?.length ? { param: paramField.value } : {}),
|
|
921
|
+
...(eventTypeField.value?.length ? { event_type: eventTypeField.value } : {}),
|
|
922
|
+
...(responseIdField.value?.length ? { response_id: responseIdField.value } : {}),
|
|
923
|
+
...(responseStatusField.value?.length ? { response_status: responseStatusField.value } : {}),
|
|
924
|
+
...(diagnosticTruncated ? { diagnostic_truncated: true } : {}),
|
|
925
|
+
};
|
|
926
|
+
const status =
|
|
927
|
+
code === "rate_limit_exceeded" ||
|
|
928
|
+
code === "usage_limit_reached" ||
|
|
929
|
+
code === "insufficient_quota"
|
|
930
|
+
? 429
|
|
931
|
+
: NON_RETRYABLE_SSE_ERROR_CODES.has(code)
|
|
932
|
+
? 400
|
|
933
|
+
: 502;
|
|
934
|
+
const headers = new Headers(source.headers);
|
|
935
|
+
headers.set("content-type", "application/json");
|
|
936
|
+
headers.set(CODEX_TRANSPORT_ERROR_HEADER, "1");
|
|
937
|
+
// A terminal event means the provider already accepted and completed this
|
|
938
|
+
// request. Never let the OpenAI SDK replay it merely because we synthesized
|
|
939
|
+
// a non-2xx response to preserve the terminal failure.
|
|
940
|
+
headers.set("x-should-retry", "false");
|
|
941
|
+
headers.delete("content-length");
|
|
942
|
+
headers.delete("content-encoding");
|
|
943
|
+
return { status, error, headers };
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* A provider terminal carried inside an accepted HTTP-200 stream. The OpenAI
|
|
948
|
+
* SDK cannot turn that late terminal into a non-2xx APIError because headers
|
|
949
|
+
* have already been accepted, so the body transform throws this equivalent
|
|
950
|
+
* bounded shape. Provider-supplied message/param text is intentionally absent:
|
|
951
|
+
* the worker may persist Error.message, while identifiers/classifications are
|
|
952
|
+
* sufficient for retry, compaction, and incident diagnostics.
|
|
953
|
+
*/
|
|
954
|
+
export class CodexStreamingTerminalError extends Error {
|
|
955
|
+
readonly status: number;
|
|
956
|
+
readonly code: string;
|
|
957
|
+
readonly type: string;
|
|
958
|
+
readonly eventType?: string;
|
|
959
|
+
readonly responseId?: string;
|
|
960
|
+
readonly responseStatus?: string;
|
|
961
|
+
readonly headers: Headers;
|
|
962
|
+
readonly error: Record<string, unknown>;
|
|
963
|
+
|
|
964
|
+
constructor(projection: CodexSseFailureProjection, publicMessage: string) {
|
|
965
|
+
super(publicMessage);
|
|
966
|
+
this.name = "CodexStreamingTerminalError";
|
|
967
|
+
this.status = projection.status;
|
|
968
|
+
this.code = projection.error.code;
|
|
969
|
+
this.type = projection.error.type;
|
|
970
|
+
if (projection.error.event_type !== undefined) {
|
|
971
|
+
this.eventType = projection.error.event_type;
|
|
972
|
+
}
|
|
973
|
+
if (projection.error.response_id !== undefined) {
|
|
974
|
+
this.responseId = projection.error.response_id;
|
|
975
|
+
}
|
|
976
|
+
if (projection.error.response_status !== undefined) {
|
|
977
|
+
this.responseStatus = projection.error.response_status;
|
|
978
|
+
}
|
|
979
|
+
this.headers = projection.headers;
|
|
980
|
+
this.error = {
|
|
981
|
+
type: projection.error.type,
|
|
982
|
+
code: projection.error.code,
|
|
983
|
+
...(projection.error.event_type ? { event_type: projection.error.event_type } : {}),
|
|
984
|
+
...(projection.error.response_id ? { response_id: projection.error.response_id } : {}),
|
|
985
|
+
...(projection.error.response_status
|
|
986
|
+
? { response_status: projection.error.response_status }
|
|
987
|
+
: {}),
|
|
988
|
+
...(projection.error.diagnostic_truncated ? { diagnostic_truncated: true } : {}),
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function codexSseFailureError(
|
|
994
|
+
source: Response,
|
|
995
|
+
rawError: unknown,
|
|
996
|
+
fallbackCode: string,
|
|
997
|
+
publicMessage: string,
|
|
998
|
+
metadata: {
|
|
999
|
+
eventType?: unknown;
|
|
1000
|
+
responseId?: unknown;
|
|
1001
|
+
responseStatus?: unknown;
|
|
1002
|
+
} = {},
|
|
1003
|
+
): CodexStreamingTerminalError {
|
|
1004
|
+
return new CodexStreamingTerminalError(
|
|
1005
|
+
codexSseFailureProjection(source, rawError, fallbackCode, publicMessage, metadata),
|
|
1006
|
+
publicMessage,
|
|
1007
|
+
);
|
|
348
1008
|
}
|
|
349
1009
|
|
|
350
1010
|
/**
|
|
@@ -354,66 +1014,223 @@ async function sseToJsonResponse(res: Response): Promise<Response> {
|
|
|
354
1014
|
*/
|
|
355
1015
|
function repairCodexStream(res: Response): Response {
|
|
356
1016
|
if (!res.body) {
|
|
357
|
-
|
|
1017
|
+
const error = codexSseFailureError(
|
|
1018
|
+
res,
|
|
1019
|
+
null,
|
|
1020
|
+
"invalid_sse_terminal",
|
|
1021
|
+
"The Codex response stream ended without a terminal response",
|
|
1022
|
+
);
|
|
1023
|
+
const body = new ReadableStream<Uint8Array>({
|
|
1024
|
+
start(controller) {
|
|
1025
|
+
controller.error(error);
|
|
1026
|
+
},
|
|
1027
|
+
});
|
|
1028
|
+
const headers = new Headers(res.headers);
|
|
1029
|
+
headers.delete("content-length");
|
|
1030
|
+
return new Response(body, {
|
|
1031
|
+
status: res.status,
|
|
1032
|
+
statusText: res.statusText,
|
|
1033
|
+
headers,
|
|
1034
|
+
});
|
|
358
1035
|
}
|
|
359
1036
|
const items: unknown[] = [];
|
|
360
1037
|
const decoder = new TextDecoder();
|
|
361
1038
|
const encoder = new TextEncoder();
|
|
362
1039
|
let buffer = "";
|
|
1040
|
+
let successfulTerminalSeen = false;
|
|
1041
|
+
const emitCompleteBlocks = (
|
|
1042
|
+
controller: TransformStreamDefaultController<Uint8Array>,
|
|
1043
|
+
final: boolean,
|
|
1044
|
+
) => {
|
|
1045
|
+
let boundary = findSseBlockBoundary(buffer, final);
|
|
1046
|
+
while (boundary) {
|
|
1047
|
+
const block = buffer.slice(0, boundary.start);
|
|
1048
|
+
const separator = buffer.slice(boundary.start, boundary.end);
|
|
1049
|
+
buffer = buffer.slice(boundary.end);
|
|
1050
|
+
const patched = patchSseBlock(block, items, res);
|
|
1051
|
+
successfulTerminalSeen ||= patched.successfulTerminal;
|
|
1052
|
+
controller.enqueue(encoder.encode(`${patched.block}${separator}`));
|
|
1053
|
+
boundary = findSseBlockBoundary(buffer, final);
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
363
1056
|
const transform = new TransformStream<Uint8Array, Uint8Array>({
|
|
364
1057
|
transform(chunk, controller) {
|
|
365
1058
|
buffer += decoder.decode(chunk, { stream: true });
|
|
366
|
-
|
|
367
|
-
while (idx !== -1) {
|
|
368
|
-
const block = buffer.slice(0, idx);
|
|
369
|
-
buffer = buffer.slice(idx + 2);
|
|
370
|
-
controller.enqueue(encoder.encode(`${patchSseBlock(block, items)}\n\n`));
|
|
371
|
-
idx = buffer.indexOf("\n\n");
|
|
372
|
-
}
|
|
1059
|
+
emitCompleteBlocks(controller, false);
|
|
373
1060
|
},
|
|
374
1061
|
flush(controller) {
|
|
1062
|
+
buffer += decoder.decode();
|
|
1063
|
+
emitCompleteBlocks(controller, true);
|
|
375
1064
|
if (buffer.length > 0) {
|
|
376
|
-
|
|
1065
|
+
const patched = patchSseBlock(buffer, items, res);
|
|
1066
|
+
successfulTerminalSeen ||= patched.successfulTerminal;
|
|
1067
|
+
controller.enqueue(encoder.encode(patched.block));
|
|
1068
|
+
buffer = "";
|
|
1069
|
+
}
|
|
1070
|
+
if (!successfulTerminalSeen) {
|
|
1071
|
+
throw codexSseFailureError(
|
|
1072
|
+
res,
|
|
1073
|
+
null,
|
|
1074
|
+
"invalid_sse_terminal",
|
|
1075
|
+
"The Codex response stream ended without a terminal response",
|
|
1076
|
+
);
|
|
377
1077
|
}
|
|
378
1078
|
},
|
|
379
1079
|
});
|
|
380
1080
|
const headers = new Headers(res.headers);
|
|
381
1081
|
headers.delete("content-length");
|
|
382
|
-
return new Response(res.body.pipeThrough(transform), {
|
|
1082
|
+
return new Response(res.body.pipeThrough(transform), {
|
|
1083
|
+
status: res.status,
|
|
1084
|
+
headers,
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
type SseBlockBoundary = { start: number; end: number };
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Find two consecutive SSE line endings without misreading one CRLF as a bare
|
|
1092
|
+
* CR followed by a bare LF. A trailing CR is intentionally held until the next
|
|
1093
|
+
* chunk (or final flush), because only then can it be distinguished from the
|
|
1094
|
+
* first byte of CRLF.
|
|
1095
|
+
*/
|
|
1096
|
+
function findSseBlockBoundary(value: string, final: boolean): SseBlockBoundary | null {
|
|
1097
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1098
|
+
const firstEnd = sseLineEndingEnd(value, index, final);
|
|
1099
|
+
if (firstEnd === null) continue;
|
|
1100
|
+
const secondEnd = sseLineEndingEnd(value, firstEnd, final);
|
|
1101
|
+
if (secondEnd !== null) {
|
|
1102
|
+
return { start: index, end: secondEnd };
|
|
1103
|
+
}
|
|
1104
|
+
index = firstEnd - 1;
|
|
1105
|
+
}
|
|
1106
|
+
return null;
|
|
383
1107
|
}
|
|
384
1108
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
1109
|
+
function sseLineEndingEnd(value: string, index: number, final: boolean): number | null {
|
|
1110
|
+
const current = value[index];
|
|
1111
|
+
if (current === "\n") return index + 1;
|
|
1112
|
+
if (current !== "\r") return null;
|
|
1113
|
+
if (index + 1 < value.length) {
|
|
1114
|
+
return value[index + 1] === "\n" ? index + 2 : index + 1;
|
|
1115
|
+
}
|
|
1116
|
+
return final ? index + 1 : null;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
type PatchedSseBlock = { block: string; successfulTerminal: boolean };
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* Collect output_item.done items and rewrite only a successful terminal event.
|
|
1123
|
+
* Failed/error/incomplete terminals throw before their provider message can be
|
|
1124
|
+
* exposed to Agents as an ordinary response_done event.
|
|
1125
|
+
*/
|
|
1126
|
+
function patchSseBlock(block: string, items: unknown[], source: Response): PatchedSseBlock {
|
|
1127
|
+
const lines = block.split(/\r\n|\r|\n/);
|
|
388
1128
|
const dataStr = lines
|
|
389
1129
|
.filter((l) => l.startsWith("data:"))
|
|
390
1130
|
.map((l) => l.slice(5).trim())
|
|
391
1131
|
.join("\n");
|
|
392
1132
|
if (!dataStr || dataStr === "[DONE]") {
|
|
393
|
-
return block;
|
|
1133
|
+
return { block, successfulTerminal: false };
|
|
394
1134
|
}
|
|
395
|
-
let ev: {
|
|
1135
|
+
let ev: {
|
|
1136
|
+
type?: string;
|
|
1137
|
+
item?: unknown;
|
|
1138
|
+
response?: Record<string, unknown>;
|
|
1139
|
+
error?: unknown;
|
|
1140
|
+
code?: unknown;
|
|
1141
|
+
message?: unknown;
|
|
1142
|
+
param?: unknown;
|
|
1143
|
+
};
|
|
396
1144
|
try {
|
|
397
1145
|
ev = JSON.parse(dataStr);
|
|
398
1146
|
} catch {
|
|
399
|
-
return block;
|
|
1147
|
+
return { block, successfulTerminal: false };
|
|
400
1148
|
}
|
|
401
1149
|
if (ev.type === "response.output_item.done" && ev.item !== undefined) {
|
|
402
1150
|
items.push(ev.item);
|
|
403
|
-
return block;
|
|
1151
|
+
return { block, successfulTerminal: false };
|
|
1152
|
+
}
|
|
1153
|
+
if (ev.type === "response.failed") {
|
|
1154
|
+
throw codexSseFailureError(
|
|
1155
|
+
source,
|
|
1156
|
+
ev.response?.error,
|
|
1157
|
+
"response_failed",
|
|
1158
|
+
"The Codex response failed",
|
|
1159
|
+
{
|
|
1160
|
+
eventType: ev.type,
|
|
1161
|
+
responseId: ev.response?.id,
|
|
1162
|
+
responseStatus: ev.response?.status,
|
|
1163
|
+
},
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
if (ev.type === "error" || ev.type === "response.error") {
|
|
1167
|
+
throw codexSseFailureError(
|
|
1168
|
+
source,
|
|
1169
|
+
ev.error ?? ev.response?.error ?? ev,
|
|
1170
|
+
"response_error",
|
|
1171
|
+
"The Codex response stream reported an error",
|
|
1172
|
+
{
|
|
1173
|
+
eventType: ev.type,
|
|
1174
|
+
responseId: ev.response?.id,
|
|
1175
|
+
responseStatus: ev.response?.status,
|
|
1176
|
+
},
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
if (ev.type === "response.incomplete") {
|
|
1180
|
+
const details = ev.response?.incomplete_details;
|
|
1181
|
+
const reason =
|
|
1182
|
+
details && typeof details === "object"
|
|
1183
|
+
? (details as Record<string, unknown>).reason
|
|
1184
|
+
: undefined;
|
|
1185
|
+
throw codexSseFailureError(
|
|
1186
|
+
source,
|
|
1187
|
+
{
|
|
1188
|
+
code: "response_incomplete",
|
|
1189
|
+
message:
|
|
1190
|
+
typeof reason === "string" && reason.length > 0
|
|
1191
|
+
? `The Codex response was incomplete (${reason})`
|
|
1192
|
+
: "The Codex response was incomplete",
|
|
1193
|
+
},
|
|
1194
|
+
"response_incomplete",
|
|
1195
|
+
"The Codex response was incomplete",
|
|
1196
|
+
{
|
|
1197
|
+
eventType: ev.type,
|
|
1198
|
+
responseId: ev.response?.id,
|
|
1199
|
+
responseStatus: ev.response?.status,
|
|
1200
|
+
},
|
|
1201
|
+
);
|
|
404
1202
|
}
|
|
405
|
-
if (
|
|
406
|
-
(
|
|
407
|
-
ev.
|
|
408
|
-
ev.
|
|
409
|
-
|
|
410
|
-
|
|
1203
|
+
if ((ev.type === "response.completed" || ev.type === "response.done") && ev.response) {
|
|
1204
|
+
if (
|
|
1205
|
+
ev.response.status === "failed" ||
|
|
1206
|
+
ev.response.status === "incomplete" ||
|
|
1207
|
+
(ev.response.error !== null && ev.response.error !== undefined)
|
|
1208
|
+
) {
|
|
1209
|
+
throw codexSseFailureError(
|
|
1210
|
+
source,
|
|
1211
|
+
ev.response.error,
|
|
1212
|
+
ev.response.status === "incomplete" ? "response_incomplete" : "response_failed",
|
|
1213
|
+
ev.response.status === "incomplete"
|
|
1214
|
+
? "The Codex response was incomplete"
|
|
1215
|
+
: "The Codex response failed",
|
|
1216
|
+
{
|
|
1217
|
+
eventType: ev.type,
|
|
1218
|
+
responseId: ev.response.id,
|
|
1219
|
+
responseStatus: ev.response.status,
|
|
1220
|
+
},
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
411
1223
|
const out = ev.response.output;
|
|
412
1224
|
if ((!Array.isArray(out) || out.length === 0) && items.length > 0) {
|
|
413
1225
|
ev.response = { ...ev.response, output: items };
|
|
414
1226
|
const nonData = lines.filter((l) => !l.startsWith("data:"));
|
|
415
|
-
|
|
1227
|
+
const lineEnding = block.match(/\r\n|\r|\n/)?.[0] ?? "\n";
|
|
1228
|
+
return {
|
|
1229
|
+
block: [...nonData, `data: ${JSON.stringify(ev)}`].join(lineEnding),
|
|
1230
|
+
successfulTerminal: true,
|
|
1231
|
+
};
|
|
416
1232
|
}
|
|
1233
|
+
return { block, successfulTerminal: true };
|
|
417
1234
|
}
|
|
418
|
-
return block;
|
|
1235
|
+
return { block, successfulTerminal: false };
|
|
419
1236
|
}
|