@opengeni/codex 0.2.13 → 0.2.16

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/src/fetch.ts CHANGED
@@ -39,6 +39,20 @@ export type FetchLike = (input: string | URL | Request, init?: RequestInit) => P
39
39
  * error that happened during the same Codex turn.
40
40
  */
41
41
  export const CODEX_TRANSPORT_ERROR_HEADER = "x-opengeni-codex-transport-error";
42
+ /** Internal transport handoff; always removed before network I/O. */
43
+ export const CODEX_REQUEST_BODY_NORMALIZED_HEADER = "x-opengeni-request-body-normalized";
44
+ const REPLAYABLE_REQUEST_BODY_FACTORY = Symbol.for("opengeni.replayable-request-body-factory");
45
+
46
+ type ReplayableRequestInit = RequestInit & {
47
+ [REPLAYABLE_REQUEST_BODY_FACTORY]?: () => ReadableStream<Uint8Array>;
48
+ };
49
+ /** Internal resolved-model handoff; always removed before network I/O. */
50
+ export const CODEX_REQUEST_MODEL_HEADER = "x-opengeni-request-model";
51
+ /** Internal durable request-identity handoff; always removed before network I/O. */
52
+ export const CODEX_REQUEST_ID_HEADER = "x-opengeni-request-id";
53
+ /** Internal original response-mode handoff; always removed before network I/O. */
54
+ export const CODEX_REQUEST_CALLER_STREAM_HEADER = "x-opengeni-request-caller-stream";
55
+ const MAX_CODEX_ERROR_BODY_BYTES = 64 * 1024;
42
56
 
43
57
  function headersCarryCodexTransportMarker(headers: unknown): boolean {
44
58
  if (!headers || typeof headers !== "object") return false;
@@ -186,25 +200,157 @@ type RequestAudit = {
186
200
  transportAttempt: number;
187
201
  model?: string;
188
202
  logicalStartedAt: number;
189
- attemptStartedAt: number;
203
+ attemptStartedAtMonotonic: number;
190
204
  policy: CodexResponseTimeoutPolicy;
205
+ terminalOutcome: RequestTerminalOutcome | null;
191
206
  };
192
207
 
193
- async function emitRequestEvent(
208
+ type RequestTerminalOutcome = "completed" | "failed" | "timed_out";
209
+
210
+ type SemanticTerminalState = {
211
+ phase: "completed" | "failed" | null;
212
+ /** Non-streaming callers must parse the complete SSE body before settling. */
213
+ deferTransportTerminal: boolean;
214
+ };
215
+
216
+ type CodexSseEvent = {
217
+ type?: string;
218
+ response?: Record<string, unknown>;
219
+ error?: unknown;
220
+ code?: unknown;
221
+ message?: unknown;
222
+ param?: unknown;
223
+ item?: unknown;
224
+ };
225
+
226
+ type CodexSseTerminalClassification =
227
+ | { phase: "completed" }
228
+ | {
229
+ phase: "failed";
230
+ rawError: unknown;
231
+ fallbackCode: string;
232
+ fallbackMessage: string;
233
+ }
234
+ | null;
235
+
236
+ function classifyCodexSseTerminal(ev: CodexSseEvent): CodexSseTerminalClassification {
237
+ if (ev.type === "response.failed") {
238
+ return {
239
+ phase: "failed",
240
+ rawError: ev.response?.error,
241
+ fallbackCode: "response_failed",
242
+ fallbackMessage: "The Codex response failed",
243
+ };
244
+ }
245
+ if (ev.type === "error" || ev.type === "response.error") {
246
+ return {
247
+ phase: "failed",
248
+ rawError: ev.error ?? ev.response?.error ?? ev,
249
+ fallbackCode: "response_error",
250
+ fallbackMessage: "The Codex response stream reported an error",
251
+ };
252
+ }
253
+ if (ev.type === "response.incomplete") {
254
+ const details = ev.response?.incomplete_details;
255
+ const reason =
256
+ details && typeof details === "object"
257
+ ? (details as Record<string, unknown>).reason
258
+ : undefined;
259
+ return {
260
+ phase: "failed",
261
+ rawError: {
262
+ code: "response_incomplete",
263
+ message:
264
+ typeof reason === "string" && reason.length > 0
265
+ ? `The Codex response was incomplete (${reason})`
266
+ : "The Codex response was incomplete",
267
+ },
268
+ fallbackCode: "response_incomplete",
269
+ fallbackMessage: "The Codex response was incomplete",
270
+ };
271
+ }
272
+ if (ev.type !== "response.completed" && ev.type !== "response.done") {
273
+ return null;
274
+ }
275
+ if (!ev.response) {
276
+ return null;
277
+ }
278
+
279
+ const responseStatus = ev.response.status;
280
+ if (
281
+ (responseStatus !== undefined && responseStatus !== "completed") ||
282
+ (ev.response.error !== null && ev.response.error !== undefined)
283
+ ) {
284
+ const incomplete = responseStatus === "incomplete";
285
+ return {
286
+ phase: "failed",
287
+ rawError: ev.response.error,
288
+ fallbackCode: incomplete ? "response_incomplete" : "response_failed",
289
+ fallbackMessage: incomplete
290
+ ? "The Codex response was incomplete"
291
+ : "The Codex response failed",
292
+ };
293
+ }
294
+ return { phase: "completed" };
295
+ }
296
+
297
+ function markSemanticTerminal(state: SemanticTerminalState, phase: "completed" | "failed"): void {
298
+ if (state.phase === null) {
299
+ state.phase = phase;
300
+ }
301
+ }
302
+
303
+ function terminalOutcomeForPhase(
304
+ phase: CodexModelRequestEvent["phase"],
305
+ ): RequestTerminalOutcome | null {
306
+ if (phase === "completed" || phase === "failed" || phase === "timed_out") {
307
+ return phase;
308
+ }
309
+ return null;
310
+ }
311
+
312
+ function requestEventFor(
194
313
  audit: RequestAudit,
195
314
  event: Omit<
196
315
  CodexModelRequestEvent,
197
316
  "requestId" | "transportAttempt" | "model" | "durationMs" | "timeoutPolicy"
198
317
  >,
199
- ): Promise<void> {
200
- await audit.ctx.onModelRequestEvent?.({
318
+ ): CodexModelRequestEvent {
319
+ return {
201
320
  requestId: audit.requestId,
202
321
  transportAttempt: audit.transportAttempt,
203
322
  ...(audit.model ? { model: audit.model } : {}),
204
- durationMs: Math.max(0, Date.now() - audit.attemptStartedAt),
323
+ durationMs: Math.max(0, performance.now() - audit.attemptStartedAtMonotonic),
205
324
  timeoutPolicy: audit.policy,
206
325
  ...event,
207
- });
326
+ };
327
+ }
328
+
329
+ async function emitRequestEvent(
330
+ audit: RequestAudit,
331
+ event: Omit<
332
+ CodexModelRequestEvent,
333
+ "requestId" | "transportAttempt" | "model" | "durationMs" | "timeoutPolicy"
334
+ >,
335
+ ): Promise<boolean> {
336
+ const terminalOutcome = terminalOutcomeForPhase(event.phase);
337
+ if (terminalOutcome !== null) {
338
+ if (audit.terminalOutcome !== null) {
339
+ return false;
340
+ }
341
+ // Fence before invoking either observer. The durable observer may reject,
342
+ // but a later transport callback must never turn that one terminal into a
343
+ // contradictory second terminal.
344
+ audit.terminalOutcome = terminalOutcome;
345
+ }
346
+ const observed = requestEventFor(audit, event);
347
+ try {
348
+ audit.ctx.onModelRequestDiagnostic?.(observed);
349
+ } catch {
350
+ // Diagnostic observers are strictly non-blocking and cannot affect transport.
351
+ }
352
+ await audit.ctx.onModelRequestEvent?.(observed);
353
+ return true;
208
354
  }
209
355
 
210
356
  function providerRequestId(headers: Headers): string | undefined {
@@ -261,11 +407,13 @@ async function observedResponse(
261
407
  res: Response,
262
408
  audit: RequestAudit,
263
409
  externalSignal: AbortSignal | null | undefined,
410
+ semanticTerminal?: SemanticTerminalState,
264
411
  ): Promise<Response> {
265
412
  const requestId = providerRequestId(res.headers);
266
413
  if (!res.body) {
414
+ if (semanticTerminal) markSemanticTerminal(semanticTerminal, "failed");
267
415
  await emitRequestEvent(audit, {
268
- phase: res.ok ? "completed" : "failed",
416
+ phase: semanticTerminal?.phase ?? (res.ok ? "completed" : "failed"),
269
417
  responseObserved: true,
270
418
  status: res.status,
271
419
  ...(requestId ? { providerRequestId: requestId } : {}),
@@ -293,17 +441,19 @@ async function observedResponse(
293
441
  if (terminal) return;
294
442
  terminal = true;
295
443
  clearTimers();
444
+ const semanticPhase = semanticTerminal?.phase;
445
+ const phase = semanticPhase ?? "timed_out";
296
446
  const error = new CodexResponseTimeoutError(klass, audit.requestId, true);
297
447
  void reader.cancel(error).catch(() => undefined);
298
448
  void emitRequestEvent(audit, {
299
- phase: "timed_out",
449
+ phase,
300
450
  responseObserved: true,
301
- timeoutClass: klass,
451
+ ...(phase === "timed_out" ? { timeoutClass: klass } : {}),
302
452
  status: res.status,
303
453
  ...(requestId ? { providerRequestId: requestId } : {}),
304
454
  }).then(
305
- () => controller.error(error),
306
- () => controller.error(error),
455
+ () => (phase === "completed" ? controller.close() : controller.error(error)),
456
+ () => (phase === "completed" ? controller.close() : controller.error(error)),
307
457
  );
308
458
  };
309
459
  armIdle = () => {
@@ -323,13 +473,15 @@ async function observedResponse(
323
473
  const reason = externalSignal?.reason ?? new DOMException("Aborted", "AbortError");
324
474
  void reader.cancel(reason).catch(() => undefined);
325
475
  void emitRequestEvent(audit, {
326
- phase: "failed",
476
+ phase: semanticTerminal?.phase ?? "failed",
327
477
  responseObserved: true,
328
478
  status: res.status,
329
479
  ...(requestId ? { providerRequestId: requestId } : {}),
330
480
  }).then(
331
- () => controller.error(reason),
332
- () => controller.error(reason),
481
+ () =>
482
+ semanticTerminal?.phase === "completed" ? controller.close() : controller.error(reason),
483
+ () =>
484
+ semanticTerminal?.phase === "completed" ? controller.close() : controller.error(reason),
333
485
  );
334
486
  };
335
487
  if (externalSignal?.aborted) {
@@ -348,45 +500,69 @@ async function observedResponse(
348
500
  if (chunk.done) {
349
501
  terminal = true;
350
502
  clearTimers();
351
- await emitRequestEvent(audit, {
352
- phase: res.ok ? "completed" : "failed",
353
- responseObserved: true,
354
- status: res.status,
355
- ...(requestId ? { providerRequestId: requestId } : {}),
356
- });
503
+ if (semanticTerminal && semanticTerminal.phase === null) {
504
+ if (!semanticTerminal.deferTransportTerminal) {
505
+ markSemanticTerminal(semanticTerminal, "failed");
506
+ }
507
+ }
508
+ if (!semanticTerminal?.deferTransportTerminal || semanticTerminal.phase !== null) {
509
+ await emitRequestEvent(audit, {
510
+ phase: semanticTerminal?.phase ?? (res.ok ? "completed" : "failed"),
511
+ responseObserved: true,
512
+ status: res.status,
513
+ ...(requestId ? { providerRequestId: requestId } : {}),
514
+ });
515
+ }
357
516
  controller.close();
358
517
  return;
359
518
  }
360
- armIdle();
361
519
  if (!firstByte) {
362
520
  firstByte = true;
521
+ // Deliver the provider byte before durable audit I/O. Audit latency
522
+ // is not provider silence and must not manufacture an idle timeout.
523
+ if (idleTimer) clearTimeout(idleTimer);
524
+ controller.enqueue(chunk.value);
363
525
  await emitRequestEvent(audit, {
364
526
  phase: "first_byte",
365
527
  responseObserved: true,
366
528
  status: res.status,
367
529
  ...(requestId ? { providerRequestId: requestId } : {}),
368
530
  });
531
+ if (!terminal) armIdle();
532
+ return;
369
533
  }
534
+ armIdle();
370
535
  controller.enqueue(chunk.value);
371
536
  } catch (error) {
372
537
  if (terminal) return;
373
538
  terminal = true;
374
539
  clearTimers();
540
+ const semanticPhase = semanticTerminal?.phase;
541
+ if (semanticTerminal && semanticPhase === null) {
542
+ markSemanticTerminal(semanticTerminal, "failed");
543
+ }
375
544
  await emitRequestEvent(audit, {
376
- phase: "failed",
545
+ phase: semanticPhase ?? "failed",
377
546
  responseObserved: true,
378
547
  status: res.status,
379
548
  ...(requestId ? { providerRequestId: requestId } : {}),
380
549
  });
381
- controller.error(error);
550
+ if (semanticPhase === "completed") {
551
+ controller.close();
552
+ } else {
553
+ controller.error(error);
554
+ }
382
555
  }
383
556
  },
384
557
  async cancel(reason) {
385
558
  if (!terminal) {
386
559
  terminal = true;
387
560
  clearTimers();
561
+ if (semanticTerminal && semanticTerminal.phase === null) {
562
+ markSemanticTerminal(semanticTerminal, "failed");
563
+ }
388
564
  await emitRequestEvent(audit, {
389
- phase: "failed",
565
+ phase: semanticTerminal?.phase ?? "failed",
390
566
  responseObserved: true,
391
567
  status: res.status,
392
568
  ...(requestId ? { providerRequestId: requestId } : {}),
@@ -446,7 +622,8 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
446
622
  const rewritten = rawUrl.replace(/(?<!\/codex)\/responses(\b|$)/, "/codex/responses$1");
447
623
 
448
624
  const policy = resolveCodexResponseTimeoutPolicy(ctx.responseTimeoutPolicy);
449
- const requestId = ctx.nextRequestId?.() ?? randomUUID();
625
+ const handedRequestId = new Headers(init?.headers).get(CODEX_REQUEST_ID_HEADER);
626
+ const requestId = handedRequestId ?? ctx.nextRequestId?.() ?? randomUUID();
450
627
  const logicalStartedAt = Date.now();
451
628
  let transportAttempt = 0;
452
629
 
@@ -455,6 +632,13 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
455
632
  authenticationAttempt: number,
456
633
  ): Promise<Response> => {
457
634
  const headers = new Headers(init?.headers);
635
+ const bodyAlreadyNormalized = headers.get(CODEX_REQUEST_BODY_NORMALIZED_HEADER) === "1";
636
+ const normalizedModel = headers.get(CODEX_REQUEST_MODEL_HEADER) ?? undefined;
637
+ const normalizedCallerStream = headers.get(CODEX_REQUEST_CALLER_STREAM_HEADER);
638
+ headers.delete(CODEX_REQUEST_BODY_NORMALIZED_HEADER);
639
+ headers.delete(CODEX_REQUEST_MODEL_HEADER);
640
+ headers.delete(CODEX_REQUEST_ID_HEADER);
641
+ headers.delete(CODEX_REQUEST_CALLER_STREAM_HEADER);
458
642
  headers.set("Authorization", `Bearer ${auth.accessToken}`);
459
643
  if (auth.chatgptAccountId) {
460
644
  headers.set("ChatGPT-Account-ID", auth.chatgptAccountId);
@@ -486,13 +670,20 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
486
670
  }
487
671
 
488
672
  // The backend is streaming-only; force stream=true on the wire but remember
489
- // the caller's intent so a non-streaming caller (e.g. the compaction
490
- // summarizer) still gets a single JSON Response back.
491
- let callerWantsStream = true;
492
- let model: string | undefined;
673
+ // the caller's intent for legacy/unowned non-streaming consumers. The owned
674
+ // compaction path consumes the same streaming model boundary as normal turns.
675
+ let callerWantsStream = bodyAlreadyNormalized ? normalizedCallerStream !== "0" : true;
676
+ let model: string | undefined = normalizedModel;
493
677
  let requestOpaqueArtifacts: string[] = [];
494
- const nextInit: RequestInit = { ...init, headers };
495
- if (typeof init?.body === "string") {
678
+ const replayableBodyFactory = (init as ReplayableRequestInit | undefined)?.[
679
+ REPLAYABLE_REQUEST_BODY_FACTORY
680
+ ];
681
+ const nextInit: RequestInit = {
682
+ ...init,
683
+ headers,
684
+ ...(replayableBodyFactory ? { body: replayableBodyFactory() } : {}),
685
+ };
686
+ if (!bodyAlreadyNormalized && typeof init?.body === "string") {
496
687
  try {
497
688
  const parsed = JSON.parse(init.body) as Record<string, unknown>;
498
689
  callerWantsStream = parsed.stream === true;
@@ -501,10 +692,16 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
501
692
  nextInit.body = JSON.stringify(normalized);
502
693
  requestOpaqueArtifacts = opaqueProviderArtifactFingerprints(normalized.input);
503
694
  } catch {
504
- /* leave unparseable bodies untouched (already copied from init) */
695
+ // This is the final request-policy boundary for the strict Responses
696
+ // endpoint. Never let malformed bytes bypass the reviewed policy.
697
+ throw new Error("Model request could not be prepared");
505
698
  }
699
+ } else if (!bodyAlreadyNormalized) {
700
+ throw new Error("Model request could not be prepared");
701
+ }
702
+ if (!bodyAlreadyNormalized) {
703
+ ctx.onRequestOpaqueArtifacts?.({ requestId, fingerprints: requestOpaqueArtifacts });
506
704
  }
507
- ctx.onRequestOpaqueArtifacts?.({ requestId, fingerprints: requestOpaqueArtifacts });
508
705
  headers.set(
509
706
  "Idempotency-Key",
510
707
  authenticationAttempt === 0 ? requestId : `${requestId}:auth-${authenticationAttempt}`,
@@ -525,13 +722,18 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
525
722
  transportAttempt,
526
723
  ...(model ? { model } : {}),
527
724
  logicalStartedAt,
528
- attemptStartedAt: Date.now(),
725
+ attemptStartedAtMonotonic: performance.now(),
529
726
  policy,
727
+ terminalOutcome: null,
530
728
  };
531
729
  await emitRequestEvent(audit, {
532
730
  phase: "started",
533
731
  responseObserved: false,
534
732
  });
733
+ const semanticTerminal: SemanticTerminalState = {
734
+ phase: null,
735
+ deferTransportTerminal: !callerWantsStream,
736
+ };
535
737
  try {
536
738
  res = await fetchBeforeHeaders(base, rewritten, nextInit, audit);
537
739
  const upstreamRequestId = providerRequestId(res.headers);
@@ -541,8 +743,7 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
541
743
  status: res.status,
542
744
  ...(upstreamRequestId ? { providerRequestId: upstreamRequestId } : {}),
543
745
  });
544
- const observed = await observedResponse(res, audit, nextInit.signal);
545
- res = observed;
746
+ res = await observedResponse(res, audit, nextInit.signal, semanticTerminal);
546
747
  } catch (error) {
547
748
  if (nextInit.signal?.aborted) {
548
749
  await emitRequestEvent(audit, {
@@ -590,11 +791,10 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
590
791
  status: res.status,
591
792
  });
592
793
  }
593
- // The codex backend leaves the terminal event's response.output empty and
594
- // delivers the assistant items via output_item.done events instead. The
595
- // @openai/agents parser (streaming AND non-streaming) reads response.output,
596
- // so we must reconstruct it: collapse to one JSON Response for a non-streaming
597
- // caller, or repair the live stream's terminal event for a streaming caller.
794
+ // The backend leaves terminal response.output empty and delivers assistant
795
+ // items through output_item.done. The typed model reducer reconstructs normal
796
+ // streaming calls; only the legacy non-streaming transport fallback collapses
797
+ // SSE into one JSON response here.
598
798
  if (!res.ok) {
599
799
  // Buffer the error body once and re-emit it as a concrete JSON Response.
600
800
  // A streaming responses request whose error body is left as the raw
@@ -605,9 +805,25 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
605
805
  // Response lets the SDK reconstruct error.error for EVERY codex error
606
806
  // (401/400/5xx too). For a hard usage cap we also pin x-should-retry:false
607
807
  // so the SDK does not burn its retry budget on a limit that won't lift.
608
- return await bufferCodexErrorResponse(res);
808
+ const buffered = await bufferCodexErrorResponse(res);
809
+ const upstreamRequestId = providerRequestId(res.headers);
810
+ markSemanticTerminal(semanticTerminal, "failed");
811
+ await emitRequestEvent(audit, {
812
+ phase: "failed",
813
+ responseObserved: true,
814
+ status: res.status,
815
+ ...(upstreamRequestId ? { providerRequestId: upstreamRequestId } : {}),
816
+ }).catch(() => undefined);
817
+ return buffered;
609
818
  }
610
- return callerWantsStream ? repairCodexStream(res) : await sseToJsonResponse(res);
819
+ if (callerWantsStream) {
820
+ res = validateCodexStream(res, (phase) => {
821
+ markSemanticTerminal(semanticTerminal, phase);
822
+ });
823
+ } else {
824
+ res = await sseToJsonResponse(res, audit, semanticTerminal);
825
+ }
826
+ return res;
611
827
  };
612
828
 
613
829
  try {
@@ -681,35 +897,99 @@ export function classifyCodexUsageLimitError(error: unknown): CodexUsageLimitInf
681
897
  * Reading the body here also drains the socket of a discarded 401 (no leak).
682
898
  */
683
899
  async function bufferCodexErrorResponse(res: Response): Promise<Response> {
684
- const bodyText = await res.text().catch(() => "");
900
+ const { text: bodyText, truncated } = await readBoundedResponseText(
901
+ res,
902
+ MAX_CODEX_ERROR_BODY_BYTES,
903
+ );
685
904
  const headers = new Headers(res.headers);
686
905
  headers.set("content-type", "application/json");
687
906
  headers.set(CODEX_TRANSPORT_ERROR_HEADER, "1");
688
907
  headers.delete("content-length"); // body re-serialized
689
908
  headers.delete("content-encoding"); // text() already decoded any gzip
690
909
  let errorType: string | undefined;
910
+ let responseBody = bodyText;
691
911
  try {
692
912
  const parsed = JSON.parse(bodyText) as { error?: { type?: unknown } };
693
913
  errorType = typeof parsed.error?.type === "string" ? parsed.error.type : undefined;
694
914
  } catch {
695
915
  /* non-JSON error body — leave as-is, no retry-header override */
696
916
  }
917
+ if (truncated) {
918
+ responseBody = JSON.stringify({
919
+ error: {
920
+ type: "provider_error_body_too_large",
921
+ code: "provider_error_body_too_large",
922
+ message: `The provider returned an error body larger than ${MAX_CODEX_ERROR_BODY_BYTES} bytes`,
923
+ },
924
+ });
925
+ headers.set("x-opengeni-provider-error-truncated", "1");
926
+ }
697
927
  if (errorType === CODEX_USAGE_LIMIT_ERROR_TYPE) {
698
928
  headers.set("x-should-retry", "false");
699
929
  }
700
- return new Response(bodyText, {
930
+ return new Response(responseBody, {
701
931
  status: res.status,
702
932
  statusText: res.statusText,
703
933
  headers,
704
934
  });
705
935
  }
706
936
 
937
+ async function readBoundedResponseText(
938
+ response: Response,
939
+ maxBytes: number,
940
+ ): Promise<{ text: string; truncated: boolean }> {
941
+ if (!response.body) return { text: "", truncated: false };
942
+ const reader = response.body.getReader();
943
+ const decoder = new TextDecoder();
944
+ const parts: string[] = [];
945
+ let bytes = 0;
946
+ let truncated = false;
947
+ try {
948
+ while (bytes < maxBytes) {
949
+ const next = await reader.read();
950
+ if (next.done) {
951
+ parts.push(decoder.decode());
952
+ return { text: parts.join(""), truncated };
953
+ }
954
+ const remaining = maxBytes - bytes;
955
+ const accepted =
956
+ next.value.byteLength > remaining ? next.value.subarray(0, remaining) : next.value;
957
+ bytes += accepted.byteLength;
958
+ parts.push(decoder.decode(accepted, { stream: true }));
959
+ if (accepted.byteLength !== next.value.byteLength) {
960
+ truncated = true;
961
+ break;
962
+ }
963
+ if (bytes >= maxBytes) {
964
+ // Reaching the hard cap is sufficient to classify the body as
965
+ // oversized. Probing for one more chunk can wait forever when an
966
+ // upstream producer stops emitting without closing its stream.
967
+ truncated = true;
968
+ break;
969
+ }
970
+ }
971
+ } catch {
972
+ truncated = true;
973
+ } finally {
974
+ // Cancellation is advisory cleanup. Some Fetch/Streams implementations do
975
+ // not settle cancel() until the producer exits; never let an oversized
976
+ // provider error hold the request open behind that implementation detail.
977
+ if (truncated) void reader.cancel().catch(() => undefined);
978
+ }
979
+ return { text: parts.join(""), truncated };
980
+ }
981
+
707
982
  /**
708
983
  * Collapse a Responses SSE stream into the single JSON Response object a
709
984
  * non-streaming `responses.create` caller expects: the terminal response.*
710
985
  * event carries the full `response` payload.
711
986
  */
712
- async function sseToJsonResponse(res: Response): Promise<Response> {
987
+ async function sseToJsonResponse(
988
+ res: Response,
989
+ audit: RequestAudit,
990
+ semanticTerminal: SemanticTerminalState,
991
+ ): Promise<Response> {
992
+ const upstreamRequestId = providerRequestId(res.headers);
713
993
  const text = await res.text();
714
994
  let final: Record<string, unknown> | null = null;
715
995
  let terminalError: Response | null = null;
@@ -720,75 +1000,49 @@ async function sseToJsonResponse(res: Response): Promise<Response> {
720
1000
  continue;
721
1001
  }
722
1002
  try {
723
- const ev = JSON.parse(data) as {
724
- type?: string;
725
- response?: Record<string, unknown>;
726
- error?: unknown;
727
- code?: unknown;
728
- message?: unknown;
729
- param?: unknown;
730
- item?: unknown;
731
- };
1003
+ const ev = JSON.parse(data) as CodexSseEvent;
732
1004
  if (ev.type === "response.output_item.done" && ev.item !== undefined) {
733
1005
  items.push(ev.item);
734
- } else if (ev.type === "response.failed") {
735
- terminalError = codexSseFailureResponse(
736
- res,
737
- ev.response?.error,
738
- "response_failed",
739
- "The Codex response failed",
740
- {
741
- eventType: ev.type,
742
- responseId: ev.response?.id,
743
- responseStatus: ev.response?.status,
744
- },
745
- );
746
- } else if (ev.type === "error" || ev.type === "response.error") {
747
- terminalError = codexSseFailureResponse(
748
- res,
749
- ev.error ?? ev.response?.error ?? ev,
750
- "response_error",
751
- "The Codex response stream reported an error",
752
- {
753
- eventType: ev.type,
754
- responseId: ev.response?.id,
755
- responseStatus: ev.response?.status,
756
- },
757
- );
758
- } else if (ev.type === "response.incomplete") {
759
- const details = ev.response?.incomplete_details;
760
- const reason =
761
- details && typeof details === "object"
762
- ? (details as Record<string, unknown>).reason
763
- : undefined;
764
- terminalError = codexSseFailureResponse(
765
- res,
766
- {
767
- code: "response_incomplete",
768
- message:
769
- typeof reason === "string" && reason.length > 0
770
- ? `The Codex response was incomplete (${reason})`
771
- : "The Codex response was incomplete",
772
- },
773
- "response_incomplete",
774
- "The Codex response was incomplete",
775
- {
776
- eventType: ev.type,
777
- responseId: ev.response?.id,
778
- responseStatus: ev.response?.status,
779
- },
780
- );
781
- } else if (ev.type === "response.completed" || ev.type === "response.done") {
782
- final = ev.response ?? null;
1006
+ } else {
1007
+ const terminal = classifyCodexSseTerminal(ev);
1008
+ if (terminal?.phase === "failed") {
1009
+ terminalError = codexSseFailureResponse(
1010
+ res,
1011
+ terminal.rawError,
1012
+ terminal.fallbackCode,
1013
+ terminal.fallbackMessage,
1014
+ {
1015
+ eventType: ev.type,
1016
+ responseId: ev.response?.id,
1017
+ responseStatus: ev.response?.status,
1018
+ },
1019
+ );
1020
+ } else if (terminal?.phase === "completed") {
1021
+ final = ev.response ?? null;
1022
+ }
783
1023
  }
784
1024
  } catch {
785
1025
  /* ignore non-JSON keepalive lines */
786
1026
  }
787
1027
  }
788
1028
  if (terminalError) {
1029
+ markSemanticTerminal(semanticTerminal, "failed");
1030
+ await emitRequestEvent(audit, {
1031
+ phase: "failed",
1032
+ responseObserved: true,
1033
+ status: res.status,
1034
+ ...(upstreamRequestId ? { providerRequestId: upstreamRequestId } : {}),
1035
+ });
789
1036
  return terminalError;
790
1037
  }
791
1038
  if (!final) {
1039
+ markSemanticTerminal(semanticTerminal, "failed");
1040
+ await emitRequestEvent(audit, {
1041
+ phase: "failed",
1042
+ responseObserved: true,
1043
+ status: res.status,
1044
+ ...(upstreamRequestId ? { providerRequestId: upstreamRequestId } : {}),
1045
+ });
792
1046
  return codexSseFailureResponse(
793
1047
  res,
794
1048
  null,
@@ -804,6 +1058,13 @@ async function sseToJsonResponse(res: Response): Promise<Response> {
804
1058
  `[codex-debug] sse->json items=${items.length} outputLen=${Array.isArray(final?.output) ? (final.output as unknown[]).length : "?"}`,
805
1059
  );
806
1060
  }
1061
+ markSemanticTerminal(semanticTerminal, "completed");
1062
+ await emitRequestEvent(audit, {
1063
+ phase: "completed",
1064
+ responseObserved: true,
1065
+ status: res.status,
1066
+ ...(upstreamRequestId ? { providerRequestId: upstreamRequestId } : {}),
1067
+ });
807
1068
  const headers = new Headers(res.headers);
808
1069
  headers.set("content-type", "application/json");
809
1070
  headers.delete("content-length");
@@ -1074,12 +1335,17 @@ function codexSseFailureError(
1074
1335
  }
1075
1336
 
1076
1337
  /**
1077
- * Repair a live Responses SSE stream for the @openai/agents streaming parser: pass
1078
- * every event through unchanged, collect the output_item.done items, and inject
1079
- * them into the terminal event's empty `output` so the parser sees the message.
1338
+ * Preserve a live Responses SSE stream byte-for-byte while translating only
1339
+ * provider-specific terminal failures into typed transport errors. Successful
1340
+ * output reconstruction belongs to the model reducer, so this layer retains no
1341
+ * duplicate output-item graph.
1080
1342
  */
1081
- function repairCodexStream(res: Response): Response {
1343
+ function validateCodexStream(
1344
+ res: Response,
1345
+ onSemanticTerminal?: (phase: "completed" | "failed") => void,
1346
+ ): Response {
1082
1347
  if (!res.body) {
1348
+ onSemanticTerminal?.("failed");
1083
1349
  const error = codexSseFailureError(
1084
1350
  res,
1085
1351
  null,
@@ -1099,7 +1365,6 @@ function repairCodexStream(res: Response): Response {
1099
1365
  headers,
1100
1366
  });
1101
1367
  }
1102
- const items: unknown[] = [];
1103
1368
  const decoder = new TextDecoder();
1104
1369
  const encoder = new TextEncoder();
1105
1370
  let buffer = "";
@@ -1113,9 +1378,8 @@ function repairCodexStream(res: Response): Response {
1113
1378
  const block = buffer.slice(0, boundary.start);
1114
1379
  const separator = buffer.slice(boundary.start, boundary.end);
1115
1380
  buffer = buffer.slice(boundary.end);
1116
- const patched = patchSseBlock(block, items, res);
1117
- successfulTerminalSeen ||= patched.successfulTerminal;
1118
- controller.enqueue(encoder.encode(`${patched.block}${separator}`));
1381
+ successfulTerminalSeen ||= inspectCodexSseBlock(block, res, onSemanticTerminal);
1382
+ controller.enqueue(encoder.encode(`${block}${separator}`));
1119
1383
  boundary = findSseBlockBoundary(buffer, final);
1120
1384
  }
1121
1385
  };
@@ -1128,9 +1392,8 @@ function repairCodexStream(res: Response): Response {
1128
1392
  buffer += decoder.decode();
1129
1393
  emitCompleteBlocks(controller, true);
1130
1394
  if (buffer.length > 0) {
1131
- const patched = patchSseBlock(buffer, items, res);
1132
- successfulTerminalSeen ||= patched.successfulTerminal;
1133
- controller.enqueue(encoder.encode(patched.block));
1395
+ successfulTerminalSeen ||= inspectCodexSseBlock(buffer, res, onSemanticTerminal);
1396
+ controller.enqueue(encoder.encode(buffer));
1134
1397
  buffer = "";
1135
1398
  }
1136
1399
  if (!successfulTerminalSeen) {
@@ -1182,83 +1445,50 @@ function sseLineEndingEnd(value: string, index: number, final: boolean): number
1182
1445
  return final ? index + 1 : null;
1183
1446
  }
1184
1447
 
1185
- type PatchedSseBlock = { block: string; successfulTerminal: boolean };
1448
+ const CODEX_TERMINAL_TYPE_HINTS = [
1449
+ '"response.completed"',
1450
+ '"response.done"',
1451
+ '"response.failed"',
1452
+ '"response.incomplete"',
1453
+ '"response.error"',
1454
+ '"error"',
1455
+ ] as const;
1186
1456
 
1187
1457
  /**
1188
- * Collect output_item.done items and rewrite only a successful terminal event.
1189
- * Failed/error/incomplete terminals throw before their provider message can be
1190
- * exposed to Agents as an ordinary response_done event.
1458
+ * Parse only blocks that can be terminal. Ordinary deltas and output items pass
1459
+ * without object allocation; failed/error/incomplete terminals throw before the
1460
+ * model can mistake them for an ordinary response_done event.
1191
1461
  */
1192
- function patchSseBlock(block: string, items: unknown[], source: Response): PatchedSseBlock {
1462
+ function inspectCodexSseBlock(
1463
+ block: string,
1464
+ source: Response,
1465
+ onSemanticTerminal?: (phase: "completed" | "failed") => void,
1466
+ ): boolean {
1193
1467
  const lines = block.split(/\r\n|\r|\n/);
1194
1468
  const dataStr = lines
1195
1469
  .filter((l) => l.startsWith("data:"))
1196
1470
  .map((l) => l.slice(5).trim())
1197
1471
  .join("\n");
1198
1472
  if (!dataStr || dataStr === "[DONE]") {
1199
- return { block, successfulTerminal: false };
1473
+ return false;
1200
1474
  }
1201
- let ev: {
1202
- type?: string;
1203
- item?: unknown;
1204
- response?: Record<string, unknown>;
1205
- error?: unknown;
1206
- code?: unknown;
1207
- message?: unknown;
1208
- param?: unknown;
1209
- };
1475
+ if (!CODEX_TERMINAL_TYPE_HINTS.some((terminalType) => dataStr.includes(terminalType))) {
1476
+ return false;
1477
+ }
1478
+ let ev: CodexSseEvent;
1210
1479
  try {
1211
1480
  ev = JSON.parse(dataStr);
1212
1481
  } catch {
1213
- return { block, successfulTerminal: false };
1214
- }
1215
- if (ev.type === "response.output_item.done" && ev.item !== undefined) {
1216
- items.push(ev.item);
1217
- return { block, successfulTerminal: false };
1218
- }
1219
- if (ev.type === "response.failed") {
1220
- throw codexSseFailureError(
1221
- source,
1222
- ev.response?.error,
1223
- "response_failed",
1224
- "The Codex response failed",
1225
- {
1226
- eventType: ev.type,
1227
- responseId: ev.response?.id,
1228
- responseStatus: ev.response?.status,
1229
- },
1230
- );
1231
- }
1232
- if (ev.type === "error" || ev.type === "response.error") {
1233
- throw codexSseFailureError(
1234
- source,
1235
- ev.error ?? ev.response?.error ?? ev,
1236
- "response_error",
1237
- "The Codex response stream reported an error",
1238
- {
1239
- eventType: ev.type,
1240
- responseId: ev.response?.id,
1241
- responseStatus: ev.response?.status,
1242
- },
1243
- );
1482
+ return false;
1244
1483
  }
1245
- if (ev.type === "response.incomplete") {
1246
- const details = ev.response?.incomplete_details;
1247
- const reason =
1248
- details && typeof details === "object"
1249
- ? (details as Record<string, unknown>).reason
1250
- : undefined;
1484
+ const terminal = classifyCodexSseTerminal(ev);
1485
+ if (terminal?.phase === "failed") {
1486
+ onSemanticTerminal?.("failed");
1251
1487
  throw codexSseFailureError(
1252
1488
  source,
1253
- {
1254
- code: "response_incomplete",
1255
- message:
1256
- typeof reason === "string" && reason.length > 0
1257
- ? `The Codex response was incomplete (${reason})`
1258
- : "The Codex response was incomplete",
1259
- },
1260
- "response_incomplete",
1261
- "The Codex response was incomplete",
1489
+ terminal.rawError,
1490
+ terminal.fallbackCode,
1491
+ terminal.fallbackMessage,
1262
1492
  {
1263
1493
  eventType: ev.type,
1264
1494
  responseId: ev.response?.id,
@@ -1266,37 +1496,9 @@ function patchSseBlock(block: string, items: unknown[], source: Response): Patch
1266
1496
  },
1267
1497
  );
1268
1498
  }
1269
- if ((ev.type === "response.completed" || ev.type === "response.done") && ev.response) {
1270
- if (
1271
- ev.response.status === "failed" ||
1272
- ev.response.status === "incomplete" ||
1273
- (ev.response.error !== null && ev.response.error !== undefined)
1274
- ) {
1275
- throw codexSseFailureError(
1276
- source,
1277
- ev.response.error,
1278
- ev.response.status === "incomplete" ? "response_incomplete" : "response_failed",
1279
- ev.response.status === "incomplete"
1280
- ? "The Codex response was incomplete"
1281
- : "The Codex response failed",
1282
- {
1283
- eventType: ev.type,
1284
- responseId: ev.response.id,
1285
- responseStatus: ev.response.status,
1286
- },
1287
- );
1288
- }
1289
- const out = ev.response.output;
1290
- if ((!Array.isArray(out) || out.length === 0) && items.length > 0) {
1291
- ev.response = { ...ev.response, output: items };
1292
- const nonData = lines.filter((l) => !l.startsWith("data:"));
1293
- const lineEnding = block.match(/\r\n|\r|\n/)?.[0] ?? "\n";
1294
- return {
1295
- block: [...nonData, `data: ${JSON.stringify(ev)}`].join(lineEnding),
1296
- successfulTerminal: true,
1297
- };
1298
- }
1299
- return { block, successfulTerminal: true };
1499
+ if (terminal?.phase === "completed") {
1500
+ onSemanticTerminal?.("completed");
1501
+ return true;
1300
1502
  }
1301
- return { block, successfulTerminal: false };
1503
+ return false;
1302
1504
  }