@opengeni/codex 0.2.11 → 0.2.15

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.
@@ -1,4 +1,4 @@
1
- import type { FetchLike } from "./fetch";
1
+ import type { FetchLike } from "./fetch.js";
2
2
  /**
3
3
  * Maps connector tool names to a Responses-API-legal charset and back. One
4
4
  * instance per codex_apps transport (i.e. per turn): tools/list populates it,
@@ -18,7 +18,7 @@
18
18
  * runtime request seam and the database history boundary call the same pure
19
19
  * function, so replayed conversation truth is identical to live model input.
20
20
  */
21
- export { MODEL_TOOL_OUTPUT_OVERSIZED_IMAGE_CARD_DATA_URL } from "./oversized-image-card";
21
+ export { MODEL_TOOL_OUTPUT_OVERSIZED_IMAGE_CARD_DATA_URL } from "./oversized-image-card.js";
22
22
  export type ModelHistoryItem = Record<string, unknown>;
23
23
  export declare const CODEX_MODEL_TOOL_OUTPUT_TRUNCATION_TOKENS = 10000;
24
24
  export declare const CODEX_TOOL_OUTPUT_SERIALIZATION_ALLOWANCE = 1.2;
@@ -1,5 +1,11 @@
1
1
  /** Mutates a parsed Responses request body in place and returns it. Pure + synchronous + unit-testable. */
2
2
  export declare function normalizeCodexRequestBody(body: Record<string, unknown>, resolveModel: (slug: string) => string): Record<string, unknown>;
3
+ /**
4
+ * Copy-on-write form for model clients that may retain converted input items.
5
+ * Only records the mutable normalizer can touch are copied; large content,
6
+ * tools, and unchanged protocol items remain shared immutable values.
7
+ */
8
+ export declare function normalizedCodexRequestBody(body: Readonly<Record<string, unknown>>, resolveModel: (slug: string) => string): Record<string, unknown>;
3
9
  /**
4
10
  * Build a longest-prefix model resolver. Catalog slugs come from GET /models
5
11
  * (api-client.ts). One leading `namespace/` segment is stripped first; an
@@ -1,7 +1,7 @@
1
- import { type CodexAuthHeaders } from "./api-client";
2
- import { CODEX_REALTIME_MODEL, CODEX_REALTIME_VERSION } from "./constants";
3
- import type { CodexFetch } from "./device-code";
4
- import { type CodexRealtimeInitialItem } from "./realtime-v3";
1
+ import { type CodexAuthHeaders } from "./api-client.js";
2
+ import { CODEX_REALTIME_MODEL, CODEX_REALTIME_VERSION } from "./constants.js";
3
+ import type { CodexFetch } from "./device-code.js";
4
+ import { type CodexRealtimeInitialItem } from "./realtime-v3.js";
5
5
  export declare const CODEX_REALTIME_VOICES: readonly ["juniper", "maple", "spruce", "ember", "vale", "breeze", "arbor", "sol", "cove"];
6
6
  export type CodexRealtimeVoice = (typeof CODEX_REALTIME_VOICES)[number];
7
7
  export type CodexRealtimeCallInput = {
package/dist/refresh.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { CodexFetch } from "./device-code";
1
+ import type { CodexFetch } from "./device-code.js";
2
2
  /** Permanent — the workspace must reconnect (status => needs_relogin). */
3
3
  export declare class CodexReloginRequired extends Error {
4
4
  constructor(message: string);
@@ -1,4 +1,4 @@
1
- import type { CodexResponseTimeoutClass, CodexResponseTimeoutPolicy } from "./request-context";
1
+ import type { CodexResponseTimeoutClass, CodexResponseTimeoutPolicy } from "./request-context.js";
2
2
  export declare const CODEX_RESPONSE_TIMEOUT_ERROR_TYPE = "opengeni_codex_response_timeout";
3
3
  export declare const DEFAULT_CODEX_RESPONSE_TIMEOUT_POLICY: CodexResponseTimeoutPolicy;
4
4
  export declare function resolveCodexResponseTimeoutPolicy(override: Partial<CodexResponseTimeoutPolicy> | undefined): CodexResponseTimeoutPolicy;
@@ -1,4 +1,4 @@
1
- import { type CodexRateLimitResetCreditsSummary } from "./reset-credits";
1
+ import { type CodexRateLimitResetCreditsSummary } from "./reset-credits.js";
2
2
  /** The 5-hour (primary) window's `limit_window_seconds`. */
3
3
  export declare const CODEX_FIVE_HOUR_WINDOW_SECONDS = 18000;
4
4
  /** The weekly (secondary) window's `limit_window_seconds`. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/codex",
3
- "version": "0.2.11",
3
+ "version": "0.2.15",
4
4
  "description": "ChatGPT/Codex subscription auth + transport: device-code login, token refresh, and the Responses-backend fetch. Pure HTTP + transforms; no database dependency.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -41,6 +41,7 @@
41
41
  "prepublishOnly": "bash ../../scripts/prepublish-guard"
42
42
  },
43
43
  "dependencies": {
44
+ "@opengeni/network": "^0.2.2",
44
45
  "zod": "^4.2.1"
45
46
  },
46
47
  "engines": {
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;
@@ -357,16 +371,22 @@ async function observedResponse(
357
371
  controller.close();
358
372
  return;
359
373
  }
360
- armIdle();
361
374
  if (!firstByte) {
362
375
  firstByte = true;
376
+ // Deliver the provider byte before durable audit I/O. Audit latency
377
+ // is not provider silence and must not manufacture an idle timeout.
378
+ if (idleTimer) clearTimeout(idleTimer);
379
+ controller.enqueue(chunk.value);
363
380
  await emitRequestEvent(audit, {
364
381
  phase: "first_byte",
365
382
  responseObserved: true,
366
383
  status: res.status,
367
384
  ...(requestId ? { providerRequestId: requestId } : {}),
368
385
  });
386
+ if (!terminal) armIdle();
387
+ return;
369
388
  }
389
+ armIdle();
370
390
  controller.enqueue(chunk.value);
371
391
  } catch (error) {
372
392
  if (terminal) return;
@@ -446,7 +466,8 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
446
466
  const rewritten = rawUrl.replace(/(?<!\/codex)\/responses(\b|$)/, "/codex/responses$1");
447
467
 
448
468
  const policy = resolveCodexResponseTimeoutPolicy(ctx.responseTimeoutPolicy);
449
- const requestId = ctx.nextRequestId?.() ?? randomUUID();
469
+ const handedRequestId = new Headers(init?.headers).get(CODEX_REQUEST_ID_HEADER);
470
+ const requestId = handedRequestId ?? ctx.nextRequestId?.() ?? randomUUID();
450
471
  const logicalStartedAt = Date.now();
451
472
  let transportAttempt = 0;
452
473
 
@@ -455,6 +476,13 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
455
476
  authenticationAttempt: number,
456
477
  ): Promise<Response> => {
457
478
  const headers = new Headers(init?.headers);
479
+ const bodyAlreadyNormalized = headers.get(CODEX_REQUEST_BODY_NORMALIZED_HEADER) === "1";
480
+ const normalizedModel = headers.get(CODEX_REQUEST_MODEL_HEADER) ?? undefined;
481
+ const normalizedCallerStream = headers.get(CODEX_REQUEST_CALLER_STREAM_HEADER);
482
+ headers.delete(CODEX_REQUEST_BODY_NORMALIZED_HEADER);
483
+ headers.delete(CODEX_REQUEST_MODEL_HEADER);
484
+ headers.delete(CODEX_REQUEST_ID_HEADER);
485
+ headers.delete(CODEX_REQUEST_CALLER_STREAM_HEADER);
458
486
  headers.set("Authorization", `Bearer ${auth.accessToken}`);
459
487
  if (auth.chatgptAccountId) {
460
488
  headers.set("ChatGPT-Account-ID", auth.chatgptAccountId);
@@ -486,13 +514,20 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
486
514
  }
487
515
 
488
516
  // 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;
517
+ // the caller's intent for legacy/unowned non-streaming consumers. The owned
518
+ // compaction path consumes the same streaming model boundary as normal turns.
519
+ let callerWantsStream = bodyAlreadyNormalized ? normalizedCallerStream !== "0" : true;
520
+ let model: string | undefined = normalizedModel;
493
521
  let requestOpaqueArtifacts: string[] = [];
494
- const nextInit: RequestInit = { ...init, headers };
495
- if (typeof init?.body === "string") {
522
+ const replayableBodyFactory = (init as ReplayableRequestInit | undefined)?.[
523
+ REPLAYABLE_REQUEST_BODY_FACTORY
524
+ ];
525
+ const nextInit: RequestInit = {
526
+ ...init,
527
+ headers,
528
+ ...(replayableBodyFactory ? { body: replayableBodyFactory() } : {}),
529
+ };
530
+ if (!bodyAlreadyNormalized && typeof init?.body === "string") {
496
531
  try {
497
532
  const parsed = JSON.parse(init.body) as Record<string, unknown>;
498
533
  callerWantsStream = parsed.stream === true;
@@ -501,26 +536,27 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
501
536
  nextInit.body = JSON.stringify(normalized);
502
537
  requestOpaqueArtifacts = opaqueProviderArtifactFingerprints(normalized.input);
503
538
  } catch {
504
- /* leave unparseable bodies untouched (already copied from init) */
539
+ // This is the final request-policy boundary for the strict Responses
540
+ // endpoint. Never let malformed bytes bypass the reviewed policy.
541
+ throw new Error("Model request could not be prepared");
505
542
  }
543
+ } else if (!bodyAlreadyNormalized) {
544
+ throw new Error("Model request could not be prepared");
545
+ }
546
+ if (!bodyAlreadyNormalized) {
547
+ ctx.onRequestOpaqueArtifacts?.({ requestId, fingerprints: requestOpaqueArtifacts });
506
548
  }
507
- ctx.onRequestOpaqueArtifacts?.({ requestId, fingerprints: requestOpaqueArtifacts });
508
549
  headers.set(
509
550
  "Idempotency-Key",
510
551
  authenticationAttempt === 0 ? requestId : `${requestId}:auth-${authenticationAttempt}`,
511
552
  );
512
553
  if (process.env.CODEX_DEBUG) {
513
- let keys: string[] = [];
514
- if (typeof nextInit.body === "string") {
515
- try {
516
- keys = Object.keys(JSON.parse(nextInit.body) as Record<string, unknown>);
517
- } catch {
518
- /* an unparseable body is already passed through unchanged above */
519
- }
520
- }
521
- console.error(
522
- `[codex-debug] POST ${rewritten} stream=${callerWantsStream} bodyKeys=[${keys.join(",")}]`,
523
- );
554
+ console.error("[codex-debug] request dispatched", {
555
+ method: "POST",
556
+ origin: "codex-subscription",
557
+ route: "codex_responses",
558
+ stream: callerWantsStream,
559
+ });
524
560
  }
525
561
  let res: Response;
526
562
  transportAttempt += 1;
@@ -587,18 +623,18 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
587
623
  ctx.onUsageHeaders?.(usage);
588
624
  }
589
625
  if (process.env.CODEX_DEBUG && !res.ok) {
590
- // Never log provider bodies: they can contain request-derived content or
591
- // account details. Status + request id is sufficient to correlate with
592
- // the structured worker failure telemetry.
593
- console.error(
594
- `[codex-debug] <- ${res.status} requestId=${res.headers.get("x-request-id") ?? "unknown"}`,
595
- );
626
+ // Never log provider bodies, identifiers, or headers: they can contain
627
+ // request-derived or account content. A bounded status is sufficient.
628
+ console.error("[codex-debug] request failed", {
629
+ origin: "codex-subscription",
630
+ route: "codex_responses",
631
+ status: res.status,
632
+ });
596
633
  }
597
- // The codex backend leaves the terminal event's response.output empty and
598
- // delivers the assistant items via output_item.done events instead. The
599
- // @openai/agents parser (streaming AND non-streaming) reads response.output,
600
- // so we must reconstruct it: collapse to one JSON Response for a non-streaming
601
- // caller, or repair the live stream's terminal event for a streaming caller.
634
+ // The backend leaves terminal response.output empty and delivers assistant
635
+ // items through output_item.done. The typed model reducer reconstructs normal
636
+ // streaming calls; only the legacy non-streaming transport fallback collapses
637
+ // SSE into one JSON response here.
602
638
  if (!res.ok) {
603
639
  // Buffer the error body once and re-emit it as a concrete JSON Response.
604
640
  // A streaming responses request whose error body is left as the raw
@@ -611,7 +647,7 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
611
647
  // so the SDK does not burn its retry budget on a limit that won't lift.
612
648
  return await bufferCodexErrorResponse(res);
613
649
  }
614
- return callerWantsStream ? repairCodexStream(res) : await sseToJsonResponse(res);
650
+ return callerWantsStream ? validateCodexStream(res) : await sseToJsonResponse(res);
615
651
  };
616
652
 
617
653
  try {
@@ -685,29 +721,88 @@ export function classifyCodexUsageLimitError(error: unknown): CodexUsageLimitInf
685
721
  * Reading the body here also drains the socket of a discarded 401 (no leak).
686
722
  */
687
723
  async function bufferCodexErrorResponse(res: Response): Promise<Response> {
688
- const bodyText = await res.text().catch(() => "");
724
+ const { text: bodyText, truncated } = await readBoundedResponseText(
725
+ res,
726
+ MAX_CODEX_ERROR_BODY_BYTES,
727
+ );
689
728
  const headers = new Headers(res.headers);
690
729
  headers.set("content-type", "application/json");
691
730
  headers.set(CODEX_TRANSPORT_ERROR_HEADER, "1");
692
731
  headers.delete("content-length"); // body re-serialized
693
732
  headers.delete("content-encoding"); // text() already decoded any gzip
694
733
  let errorType: string | undefined;
734
+ let responseBody = bodyText;
695
735
  try {
696
736
  const parsed = JSON.parse(bodyText) as { error?: { type?: unknown } };
697
737
  errorType = typeof parsed.error?.type === "string" ? parsed.error.type : undefined;
698
738
  } catch {
699
739
  /* non-JSON error body — leave as-is, no retry-header override */
700
740
  }
741
+ if (truncated) {
742
+ responseBody = JSON.stringify({
743
+ error: {
744
+ type: "provider_error_body_too_large",
745
+ code: "provider_error_body_too_large",
746
+ message: `The provider returned an error body larger than ${MAX_CODEX_ERROR_BODY_BYTES} bytes`,
747
+ },
748
+ });
749
+ headers.set("x-opengeni-provider-error-truncated", "1");
750
+ }
701
751
  if (errorType === CODEX_USAGE_LIMIT_ERROR_TYPE) {
702
752
  headers.set("x-should-retry", "false");
703
753
  }
704
- return new Response(bodyText, {
754
+ return new Response(responseBody, {
705
755
  status: res.status,
706
756
  statusText: res.statusText,
707
757
  headers,
708
758
  });
709
759
  }
710
760
 
761
+ async function readBoundedResponseText(
762
+ response: Response,
763
+ maxBytes: number,
764
+ ): Promise<{ text: string; truncated: boolean }> {
765
+ if (!response.body) return { text: "", truncated: false };
766
+ const reader = response.body.getReader();
767
+ const decoder = new TextDecoder();
768
+ const parts: string[] = [];
769
+ let bytes = 0;
770
+ let truncated = false;
771
+ try {
772
+ while (bytes < maxBytes) {
773
+ const next = await reader.read();
774
+ if (next.done) {
775
+ parts.push(decoder.decode());
776
+ return { text: parts.join(""), truncated };
777
+ }
778
+ const remaining = maxBytes - bytes;
779
+ const accepted =
780
+ next.value.byteLength > remaining ? next.value.subarray(0, remaining) : next.value;
781
+ bytes += accepted.byteLength;
782
+ parts.push(decoder.decode(accepted, { stream: true }));
783
+ if (accepted.byteLength !== next.value.byteLength) {
784
+ truncated = true;
785
+ break;
786
+ }
787
+ if (bytes >= maxBytes) {
788
+ // Reaching the hard cap is sufficient to classify the body as
789
+ // oversized. Probing for one more chunk can wait forever when an
790
+ // upstream producer stops emitting without closing its stream.
791
+ truncated = true;
792
+ break;
793
+ }
794
+ }
795
+ } catch {
796
+ truncated = true;
797
+ } finally {
798
+ // Cancellation is advisory cleanup. Some Fetch/Streams implementations do
799
+ // not settle cancel() until the producer exits; never let an oversized
800
+ // provider error hold the request open behind that implementation detail.
801
+ if (truncated) void reader.cancel().catch(() => undefined);
802
+ }
803
+ return { text: parts.join(""), truncated };
804
+ }
805
+
711
806
  /**
712
807
  * Collapse a Responses SSE stream into the single JSON Response object a
713
808
  * non-streaming `responses.create` caller expects: the terminal response.*
@@ -1016,9 +1111,9 @@ function codexSseFailureProjection(
1016
1111
  * A provider terminal carried inside an accepted HTTP-200 stream. The OpenAI
1017
1112
  * SDK cannot turn that late terminal into a non-2xx APIError because headers
1018
1113
  * have already been accepted, so the body transform throws this equivalent
1019
- * bounded shape. Provider-supplied message/param text is intentionally absent:
1020
- * the worker may persist Error.message, while identifiers/classifications are
1021
- * sufficient for retry, compaction, and incident diagnostics.
1114
+ * bounded shape. Provider-supplied message/param text remains exact within the
1115
+ * explicit terminal-field byte contract; retry classification is additive and
1116
+ * never substitutes for the source diagnostic.
1022
1117
  */
1023
1118
  export class CodexStreamingTerminalError extends Error {
1024
1119
  readonly status: number;
@@ -1030,8 +1125,8 @@ export class CodexStreamingTerminalError extends Error {
1030
1125
  readonly headers: Headers;
1031
1126
  readonly error: Record<string, unknown>;
1032
1127
 
1033
- constructor(projection: CodexSseFailureProjection, publicMessage: string) {
1034
- super(publicMessage);
1128
+ constructor(projection: CodexSseFailureProjection) {
1129
+ super(projection.error.message);
1035
1130
  this.name = "CodexStreamingTerminalError";
1036
1131
  this.status = projection.status;
1037
1132
  this.code = projection.error.code;
@@ -1049,6 +1144,8 @@ export class CodexStreamingTerminalError extends Error {
1049
1144
  this.error = {
1050
1145
  type: projection.error.type,
1051
1146
  code: projection.error.code,
1147
+ message: projection.error.message,
1148
+ ...(projection.error.param ? { param: projection.error.param } : {}),
1052
1149
  ...(projection.error.event_type ? { event_type: projection.error.event_type } : {}),
1053
1150
  ...(projection.error.response_id ? { response_id: projection.error.response_id } : {}),
1054
1151
  ...(projection.error.response_status
@@ -1072,16 +1169,16 @@ function codexSseFailureError(
1072
1169
  ): CodexStreamingTerminalError {
1073
1170
  return new CodexStreamingTerminalError(
1074
1171
  codexSseFailureProjection(source, rawError, fallbackCode, publicMessage, metadata),
1075
- publicMessage,
1076
1172
  );
1077
1173
  }
1078
1174
 
1079
1175
  /**
1080
- * Repair a live Responses SSE stream for the @openai/agents streaming parser: pass
1081
- * every event through unchanged, collect the output_item.done items, and inject
1082
- * them into the terminal event's empty `output` so the parser sees the message.
1176
+ * Preserve a live Responses SSE stream byte-for-byte while translating only
1177
+ * provider-specific terminal failures into typed transport errors. Successful
1178
+ * output reconstruction belongs to the model reducer, so this layer retains no
1179
+ * duplicate output-item graph.
1083
1180
  */
1084
- function repairCodexStream(res: Response): Response {
1181
+ function validateCodexStream(res: Response): Response {
1085
1182
  if (!res.body) {
1086
1183
  const error = codexSseFailureError(
1087
1184
  res,
@@ -1102,7 +1199,6 @@ function repairCodexStream(res: Response): Response {
1102
1199
  headers,
1103
1200
  });
1104
1201
  }
1105
- const items: unknown[] = [];
1106
1202
  const decoder = new TextDecoder();
1107
1203
  const encoder = new TextEncoder();
1108
1204
  let buffer = "";
@@ -1116,9 +1212,8 @@ function repairCodexStream(res: Response): Response {
1116
1212
  const block = buffer.slice(0, boundary.start);
1117
1213
  const separator = buffer.slice(boundary.start, boundary.end);
1118
1214
  buffer = buffer.slice(boundary.end);
1119
- const patched = patchSseBlock(block, items, res);
1120
- successfulTerminalSeen ||= patched.successfulTerminal;
1121
- controller.enqueue(encoder.encode(`${patched.block}${separator}`));
1215
+ successfulTerminalSeen ||= inspectCodexSseBlock(block, res);
1216
+ controller.enqueue(encoder.encode(`${block}${separator}`));
1122
1217
  boundary = findSseBlockBoundary(buffer, final);
1123
1218
  }
1124
1219
  };
@@ -1131,9 +1226,8 @@ function repairCodexStream(res: Response): Response {
1131
1226
  buffer += decoder.decode();
1132
1227
  emitCompleteBlocks(controller, true);
1133
1228
  if (buffer.length > 0) {
1134
- const patched = patchSseBlock(buffer, items, res);
1135
- successfulTerminalSeen ||= patched.successfulTerminal;
1136
- controller.enqueue(encoder.encode(patched.block));
1229
+ successfulTerminalSeen ||= inspectCodexSseBlock(buffer, res);
1230
+ controller.enqueue(encoder.encode(buffer));
1137
1231
  buffer = "";
1138
1232
  }
1139
1233
  if (!successfulTerminalSeen) {
@@ -1185,21 +1279,31 @@ function sseLineEndingEnd(value: string, index: number, final: boolean): number
1185
1279
  return final ? index + 1 : null;
1186
1280
  }
1187
1281
 
1188
- type PatchedSseBlock = { block: string; successfulTerminal: boolean };
1282
+ const CODEX_TERMINAL_TYPE_HINTS = [
1283
+ '"response.completed"',
1284
+ '"response.done"',
1285
+ '"response.failed"',
1286
+ '"response.incomplete"',
1287
+ '"response.error"',
1288
+ '"error"',
1289
+ ] as const;
1189
1290
 
1190
1291
  /**
1191
- * Collect output_item.done items and rewrite only a successful terminal event.
1192
- * Failed/error/incomplete terminals throw before their provider message can be
1193
- * exposed to Agents as an ordinary response_done event.
1292
+ * Parse only blocks that can be terminal. Ordinary deltas and output items pass
1293
+ * without object allocation; failed/error/incomplete terminals throw before the
1294
+ * model can mistake them for an ordinary response_done event.
1194
1295
  */
1195
- function patchSseBlock(block: string, items: unknown[], source: Response): PatchedSseBlock {
1296
+ function inspectCodexSseBlock(block: string, source: Response): boolean {
1196
1297
  const lines = block.split(/\r\n|\r|\n/);
1197
1298
  const dataStr = lines
1198
1299
  .filter((l) => l.startsWith("data:"))
1199
1300
  .map((l) => l.slice(5).trim())
1200
1301
  .join("\n");
1201
1302
  if (!dataStr || dataStr === "[DONE]") {
1202
- return { block, successfulTerminal: false };
1303
+ return false;
1304
+ }
1305
+ if (!CODEX_TERMINAL_TYPE_HINTS.some((terminalType) => dataStr.includes(terminalType))) {
1306
+ return false;
1203
1307
  }
1204
1308
  let ev: {
1205
1309
  type?: string;
@@ -1213,11 +1317,7 @@ function patchSseBlock(block: string, items: unknown[], source: Response): Patch
1213
1317
  try {
1214
1318
  ev = JSON.parse(dataStr);
1215
1319
  } catch {
1216
- return { block, successfulTerminal: false };
1217
- }
1218
- if (ev.type === "response.output_item.done" && ev.item !== undefined) {
1219
- items.push(ev.item);
1220
- return { block, successfulTerminal: false };
1320
+ return false;
1221
1321
  }
1222
1322
  if (ev.type === "response.failed") {
1223
1323
  throw codexSseFailureError(
@@ -1271,8 +1371,7 @@ function patchSseBlock(block: string, items: unknown[], source: Response): Patch
1271
1371
  }
1272
1372
  if ((ev.type === "response.completed" || ev.type === "response.done") && ev.response) {
1273
1373
  if (
1274
- ev.response.status === "failed" ||
1275
- ev.response.status === "incomplete" ||
1374
+ (ev.response.status !== undefined && ev.response.status !== "completed") ||
1276
1375
  (ev.response.error !== null && ev.response.error !== undefined)
1277
1376
  ) {
1278
1377
  throw codexSseFailureError(
@@ -1289,17 +1388,7 @@ function patchSseBlock(block: string, items: unknown[], source: Response): Patch
1289
1388
  },
1290
1389
  );
1291
1390
  }
1292
- const out = ev.response.output;
1293
- if ((!Array.isArray(out) || out.length === 0) && items.length > 0) {
1294
- ev.response = { ...ev.response, output: items };
1295
- const nonData = lines.filter((l) => !l.startsWith("data:"));
1296
- const lineEnding = block.match(/\r\n|\r|\n/)?.[0] ?? "\n";
1297
- return {
1298
- block: [...nonData, `data: ${JSON.stringify(ev)}`].join(lineEnding),
1299
- successfulTerminal: true,
1300
- };
1301
- }
1302
- return { block, successfulTerminal: true };
1391
+ return true;
1303
1392
  }
1304
- return { block, successfulTerminal: false };
1393
+ return false;
1305
1394
  }