@opengeni/codex 0.2.2 → 0.2.5

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/constants.ts CHANGED
@@ -31,7 +31,7 @@ export const CODEX_MODEL_ID_PREFIX = "codex/";
31
31
  export const CODEX_FALLBACK_MODEL_SLUGS = ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"] as const;
32
32
 
33
33
  // Live Codex model-catalog values for every exposed gpt-5.6 subscription slug.
34
- // Verified 2026-07-14 against Codex CLI 0.144.4's freshly fetched
34
+ // Verified 2026-07-18 against Codex CLI 0.144.6's freshly fetched
35
35
  // ~/.codex/models_cache.json and the matching openai/codex core derivations:
36
36
  // raw context window = 272,000
37
37
  // effective input window (95%) = 258,400
@@ -52,7 +52,7 @@ export const CODEX_MODEL_AUTO_COMPACT_TOKEN_LIMIT = Math.floor(
52
52
  // 2026-07-09: 0.142.4 filtered every GPT-5.6 slug out of GET /models, while the
53
53
  // official Codex 0.144.0+ releases return all three exact slugs above. Keep
54
54
  // this pinned to the latest stable Codex release we have verified end-to-end.
55
- export const CODEX_CLIENT_VERSION = "0.144.4";
55
+ export const CODEX_CLIENT_VERSION = "0.144.6";
56
56
 
57
57
  export const CODEX_REFRESH_WINDOW_MS = 5 * 60 * 1000; // proactive refresh when within 5 min of exp (spec §1.1)
58
58
  export const CODEX_REFRESH_FALLBACK_MS = 8 * 24 * 60 * 60 * 1000; // 8 days when exp is unparseable
package/src/fetch.ts CHANGED
@@ -292,7 +292,11 @@ async function bufferCodexErrorResponse(res: Response): Promise<Response> {
292
292
  if (errorType === CODEX_USAGE_LIMIT_ERROR_TYPE) {
293
293
  headers.set("x-should-retry", "false");
294
294
  }
295
- return new Response(bodyText, { status: res.status, statusText: res.statusText, headers });
295
+ return new Response(bodyText, {
296
+ status: res.status,
297
+ statusText: res.statusText,
298
+ headers,
299
+ });
296
300
  }
297
301
 
298
302
  /**
@@ -303,14 +307,10 @@ async function bufferCodexErrorResponse(res: Response): Promise<Response> {
303
307
  async function sseToJsonResponse(res: Response): Promise<Response> {
304
308
  const text = await res.text();
305
309
  let final: Record<string, unknown> | null = null;
310
+ let terminalError: Response | null = null;
306
311
  const items: unknown[] = []; // assembled from output_item.done (the codex backend
307
312
  // leaves response.completed.response.output empty and emits the items separately).
308
- for (const block of text.split("\n\n")) {
309
- const data = block
310
- .split("\n")
311
- .filter((l) => l.startsWith("data:"))
312
- .map((l) => l.slice(5).trim())
313
- .join("\n");
313
+ for (const data of sseDataPayloads(text)) {
314
314
  if (!data || data === "[DONE]") {
315
315
  continue;
316
316
  }
@@ -318,21 +318,79 @@ async function sseToJsonResponse(res: Response): Promise<Response> {
318
318
  const ev = JSON.parse(data) as {
319
319
  type?: string;
320
320
  response?: Record<string, unknown>;
321
+ error?: unknown;
322
+ code?: unknown;
323
+ message?: unknown;
324
+ param?: unknown;
321
325
  item?: unknown;
322
326
  };
323
327
  if (ev.type === "response.output_item.done" && ev.item !== undefined) {
324
328
  items.push(ev.item);
325
- } else if (
326
- ev.type === "response.completed" ||
327
- ev.type === "response.done" ||
328
- ev.type === "response.incomplete"
329
- ) {
329
+ } else if (ev.type === "response.failed") {
330
+ terminalError = codexSseFailureResponse(
331
+ res,
332
+ ev.response?.error,
333
+ "response_failed",
334
+ "The Codex response failed",
335
+ {
336
+ eventType: ev.type,
337
+ responseId: ev.response?.id,
338
+ responseStatus: ev.response?.status,
339
+ },
340
+ );
341
+ } else if (ev.type === "error" || ev.type === "response.error") {
342
+ terminalError = codexSseFailureResponse(
343
+ res,
344
+ ev.error ?? ev.response?.error ?? ev,
345
+ "response_error",
346
+ "The Codex response stream reported an error",
347
+ {
348
+ eventType: ev.type,
349
+ responseId: ev.response?.id,
350
+ responseStatus: ev.response?.status,
351
+ },
352
+ );
353
+ } else if (ev.type === "response.incomplete") {
354
+ const details = ev.response?.incomplete_details;
355
+ const reason =
356
+ details && typeof details === "object"
357
+ ? (details as Record<string, unknown>).reason
358
+ : undefined;
359
+ terminalError = codexSseFailureResponse(
360
+ res,
361
+ {
362
+ code: "response_incomplete",
363
+ message:
364
+ typeof reason === "string" && reason.length > 0
365
+ ? `The Codex response was incomplete (${reason})`
366
+ : "The Codex response was incomplete",
367
+ },
368
+ "response_incomplete",
369
+ "The Codex response was incomplete",
370
+ {
371
+ eventType: ev.type,
372
+ responseId: ev.response?.id,
373
+ responseStatus: ev.response?.status,
374
+ },
375
+ );
376
+ } else if (ev.type === "response.completed" || ev.type === "response.done") {
330
377
  final = ev.response ?? null;
331
378
  }
332
379
  } catch {
333
380
  /* ignore non-JSON keepalive lines */
334
381
  }
335
382
  }
383
+ if (terminalError) {
384
+ return terminalError;
385
+ }
386
+ if (!final) {
387
+ return codexSseFailureResponse(
388
+ res,
389
+ null,
390
+ "invalid_sse_terminal",
391
+ "The Codex response stream ended without a terminal response",
392
+ );
393
+ }
336
394
  if (final && items.length > 0) {
337
395
  final = { ...final, output: items }; // prefer the assembled items over an empty output array
338
396
  }
@@ -344,7 +402,269 @@ async function sseToJsonResponse(res: Response): Promise<Response> {
344
402
  const headers = new Headers(res.headers);
345
403
  headers.set("content-type", "application/json");
346
404
  headers.delete("content-length");
347
- return new Response(JSON.stringify(final ?? {}), { status: 200, headers });
405
+ return new Response(JSON.stringify(final), { status: 200, headers });
406
+ }
407
+
408
+ const NON_RETRYABLE_SSE_ERROR_CODES = new Set([
409
+ "bio_policy",
410
+ "context_length_exceeded",
411
+ "cyber_policy",
412
+ "insufficient_quota",
413
+ "invalid_prompt",
414
+ "usage_limit_reached",
415
+ ]);
416
+
417
+ /**
418
+ * Project the data payloads from a complete SSE body. EventSource accepts LF,
419
+ * CRLF, and bare CR line endings; splitting only on `\n\n` can therefore merge
420
+ * a standards-valid terminal failure into the preceding event and silently
421
+ * turn it into `{}`. Preserve the SSE rule that multiple data lines are joined
422
+ * with `\n`, and tolerate a final event without a trailing blank line as the
423
+ * previous transport parser did.
424
+ */
425
+ function sseDataPayloads(text: string): string[] {
426
+ const payloads: string[] = [];
427
+ let dataLines: string[] = [];
428
+ const dispatch = () => {
429
+ if (dataLines.length > 0) payloads.push(dataLines.join("\n"));
430
+ dataLines = [];
431
+ };
432
+
433
+ for (const line of text.split(/\r\n|\r|\n/)) {
434
+ if (line === "") {
435
+ dispatch();
436
+ continue;
437
+ }
438
+ if (line === "data") {
439
+ dataLines.push("");
440
+ continue;
441
+ }
442
+ if (!line.startsWith("data:")) continue;
443
+ const value = line.slice(5);
444
+ dataLines.push(value.startsWith(" ") ? value.slice(1) : value);
445
+ }
446
+ dispatch();
447
+ return payloads;
448
+ }
449
+
450
+ const CODEX_TERMINAL_ERROR_FIELD_MAX_BYTES = 256;
451
+ const CODEX_TERMINAL_ERROR_MESSAGE_MAX_BYTES = 4 * 1024;
452
+ const CODEX_TERMINAL_ERROR_TRUNCATION_MARKER = "… [truncated]";
453
+
454
+ function boundedTerminalErrorField(
455
+ value: unknown,
456
+ maxBytes: number,
457
+ ): { value?: string; truncated: boolean } {
458
+ if (typeof value !== "string") return { truncated: false };
459
+ const encoder = new TextEncoder();
460
+ const encoded = encoder.encode(value);
461
+ if (encoded.byteLength <= maxBytes) return { value, truncated: false };
462
+
463
+ const markerBytes = encoder.encode(CODEX_TERMINAL_ERROR_TRUNCATION_MARKER).byteLength;
464
+ let prefixEnd = Math.max(0, maxBytes - markerBytes);
465
+ while (prefixEnd > 0 && (encoded[prefixEnd]! & 0xc0) === 0x80) {
466
+ prefixEnd -= 1;
467
+ }
468
+ return {
469
+ value: `${new TextDecoder().decode(encoded.subarray(0, prefixEnd))}${CODEX_TERMINAL_ERROR_TRUNCATION_MARKER}`,
470
+ truncated: true,
471
+ };
472
+ }
473
+
474
+ /**
475
+ * Convert a terminal error carried inside an HTTP-200 SSE stream into the
476
+ * ordinary non-2xx JSON error contract expected by the OpenAI SDK. Codex CLI
477
+ * treats the same events as provider failures; returning a successful `{}`
478
+ * loses the actual cause and makes compaction look semantically empty.
479
+ */
480
+ function codexSseFailureResponse(
481
+ source: Response,
482
+ rawError: unknown,
483
+ fallbackCode: string,
484
+ fallbackMessage: string,
485
+ metadata: {
486
+ eventType?: unknown;
487
+ responseId?: unknown;
488
+ responseStatus?: unknown;
489
+ } = {},
490
+ ): Response {
491
+ const projection = codexSseFailureProjection(
492
+ source,
493
+ rawError,
494
+ fallbackCode,
495
+ fallbackMessage,
496
+ metadata,
497
+ );
498
+ return new Response(JSON.stringify({ error: projection.error }), {
499
+ status: projection.status,
500
+ headers: projection.headers,
501
+ });
502
+ }
503
+
504
+ export type CodexSseFailureProjection = {
505
+ status: number;
506
+ error: {
507
+ type: string;
508
+ code: string;
509
+ message: string;
510
+ param?: string;
511
+ event_type?: string;
512
+ response_id?: string;
513
+ response_status?: string;
514
+ diagnostic_truncated?: true;
515
+ };
516
+ headers: Headers;
517
+ };
518
+
519
+ function codexSseFailureProjection(
520
+ source: Response,
521
+ rawError: unknown,
522
+ fallbackCode: string,
523
+ fallbackMessage: string,
524
+ metadata: {
525
+ eventType?: unknown;
526
+ responseId?: unknown;
527
+ responseStatus?: unknown;
528
+ } = {},
529
+ ): CodexSseFailureProjection {
530
+ const record =
531
+ rawError && typeof rawError === "object" && !Array.isArray(rawError)
532
+ ? (rawError as Record<string, unknown>)
533
+ : {};
534
+ const typeField = boundedTerminalErrorField(record.type, CODEX_TERMINAL_ERROR_FIELD_MAX_BYTES);
535
+ const codeField = boundedTerminalErrorField(record.code, CODEX_TERMINAL_ERROR_FIELD_MAX_BYTES);
536
+ const messageField = boundedTerminalErrorField(
537
+ record.message ?? (typeof rawError === "string" ? rawError : undefined),
538
+ CODEX_TERMINAL_ERROR_MESSAGE_MAX_BYTES,
539
+ );
540
+ const paramField = boundedTerminalErrorField(record.param, CODEX_TERMINAL_ERROR_FIELD_MAX_BYTES);
541
+ const eventTypeField = boundedTerminalErrorField(
542
+ metadata.eventType,
543
+ CODEX_TERMINAL_ERROR_FIELD_MAX_BYTES,
544
+ );
545
+ const responseIdField = boundedTerminalErrorField(
546
+ metadata.responseId,
547
+ CODEX_TERMINAL_ERROR_FIELD_MAX_BYTES,
548
+ );
549
+ const responseStatusField = boundedTerminalErrorField(
550
+ metadata.responseStatus,
551
+ CODEX_TERMINAL_ERROR_FIELD_MAX_BYTES,
552
+ );
553
+ const providerType =
554
+ typeField.value === "error" ||
555
+ typeField.value === "response.error" ||
556
+ typeField.value === "response.failed"
557
+ ? undefined
558
+ : typeField.value;
559
+ const code =
560
+ (codeField.value?.length ? codeField.value : undefined) ??
561
+ (providerType?.length ? providerType : undefined) ??
562
+ fallbackCode;
563
+ const diagnosticTruncated =
564
+ typeField.truncated ||
565
+ codeField.truncated ||
566
+ messageField.truncated ||
567
+ paramField.truncated ||
568
+ eventTypeField.truncated ||
569
+ responseIdField.truncated ||
570
+ responseStatusField.truncated ||
571
+ Object.keys(record).some((key) => !["type", "code", "message", "param"].includes(key)) ||
572
+ (rawError !== null &&
573
+ rawError !== undefined &&
574
+ typeof rawError !== "string" &&
575
+ (typeof rawError !== "object" || Array.isArray(rawError)));
576
+ const error: CodexSseFailureProjection["error"] = {
577
+ type: providerType?.length ? providerType : code,
578
+ code,
579
+ message: messageField.value?.length ? messageField.value : fallbackMessage,
580
+ ...(paramField.value?.length ? { param: paramField.value } : {}),
581
+ ...(eventTypeField.value?.length ? { event_type: eventTypeField.value } : {}),
582
+ ...(responseIdField.value?.length ? { response_id: responseIdField.value } : {}),
583
+ ...(responseStatusField.value?.length ? { response_status: responseStatusField.value } : {}),
584
+ ...(diagnosticTruncated ? { diagnostic_truncated: true } : {}),
585
+ };
586
+ const status =
587
+ code === "rate_limit_exceeded" ||
588
+ code === "usage_limit_reached" ||
589
+ code === "insufficient_quota"
590
+ ? 429
591
+ : NON_RETRYABLE_SSE_ERROR_CODES.has(code)
592
+ ? 400
593
+ : 502;
594
+ const headers = new Headers(source.headers);
595
+ headers.set("content-type", "application/json");
596
+ headers.set(CODEX_TRANSPORT_ERROR_HEADER, "1");
597
+ // A terminal event means the provider already accepted and completed this
598
+ // request. Never let the OpenAI SDK replay it merely because we synthesized
599
+ // a non-2xx response to preserve the terminal failure.
600
+ headers.set("x-should-retry", "false");
601
+ headers.delete("content-length");
602
+ headers.delete("content-encoding");
603
+ return { status, error, headers };
604
+ }
605
+
606
+ /**
607
+ * A provider terminal carried inside an accepted HTTP-200 stream. The OpenAI
608
+ * SDK cannot turn that late terminal into a non-2xx APIError because headers
609
+ * have already been accepted, so the body transform throws this equivalent
610
+ * bounded shape. Provider-supplied message/param text is intentionally absent:
611
+ * the worker may persist Error.message, while identifiers/classifications are
612
+ * sufficient for retry, compaction, and incident diagnostics.
613
+ */
614
+ export class CodexStreamingTerminalError extends Error {
615
+ readonly status: number;
616
+ readonly code: string;
617
+ readonly type: string;
618
+ readonly eventType?: string;
619
+ readonly responseId?: string;
620
+ readonly responseStatus?: string;
621
+ readonly headers: Headers;
622
+ readonly error: Record<string, unknown>;
623
+
624
+ constructor(projection: CodexSseFailureProjection, publicMessage: string) {
625
+ super(publicMessage);
626
+ this.name = "CodexStreamingTerminalError";
627
+ this.status = projection.status;
628
+ this.code = projection.error.code;
629
+ this.type = projection.error.type;
630
+ if (projection.error.event_type !== undefined) {
631
+ this.eventType = projection.error.event_type;
632
+ }
633
+ if (projection.error.response_id !== undefined) {
634
+ this.responseId = projection.error.response_id;
635
+ }
636
+ if (projection.error.response_status !== undefined) {
637
+ this.responseStatus = projection.error.response_status;
638
+ }
639
+ this.headers = projection.headers;
640
+ this.error = {
641
+ type: projection.error.type,
642
+ code: projection.error.code,
643
+ ...(projection.error.event_type ? { event_type: projection.error.event_type } : {}),
644
+ ...(projection.error.response_id ? { response_id: projection.error.response_id } : {}),
645
+ ...(projection.error.response_status
646
+ ? { response_status: projection.error.response_status }
647
+ : {}),
648
+ ...(projection.error.diagnostic_truncated ? { diagnostic_truncated: true } : {}),
649
+ };
650
+ }
651
+ }
652
+
653
+ function codexSseFailureError(
654
+ source: Response,
655
+ rawError: unknown,
656
+ fallbackCode: string,
657
+ publicMessage: string,
658
+ metadata: {
659
+ eventType?: unknown;
660
+ responseId?: unknown;
661
+ responseStatus?: unknown;
662
+ } = {},
663
+ ): CodexStreamingTerminalError {
664
+ return new CodexStreamingTerminalError(
665
+ codexSseFailureProjection(source, rawError, fallbackCode, publicMessage, metadata),
666
+ publicMessage,
667
+ );
348
668
  }
349
669
 
350
670
  /**
@@ -354,66 +674,223 @@ async function sseToJsonResponse(res: Response): Promise<Response> {
354
674
  */
355
675
  function repairCodexStream(res: Response): Response {
356
676
  if (!res.body) {
357
- return res;
677
+ const error = codexSseFailureError(
678
+ res,
679
+ null,
680
+ "invalid_sse_terminal",
681
+ "The Codex response stream ended without a terminal response",
682
+ );
683
+ const body = new ReadableStream<Uint8Array>({
684
+ start(controller) {
685
+ controller.error(error);
686
+ },
687
+ });
688
+ const headers = new Headers(res.headers);
689
+ headers.delete("content-length");
690
+ return new Response(body, {
691
+ status: res.status,
692
+ statusText: res.statusText,
693
+ headers,
694
+ });
358
695
  }
359
696
  const items: unknown[] = [];
360
697
  const decoder = new TextDecoder();
361
698
  const encoder = new TextEncoder();
362
699
  let buffer = "";
700
+ let successfulTerminalSeen = false;
701
+ const emitCompleteBlocks = (
702
+ controller: TransformStreamDefaultController<Uint8Array>,
703
+ final: boolean,
704
+ ) => {
705
+ let boundary = findSseBlockBoundary(buffer, final);
706
+ while (boundary) {
707
+ const block = buffer.slice(0, boundary.start);
708
+ const separator = buffer.slice(boundary.start, boundary.end);
709
+ buffer = buffer.slice(boundary.end);
710
+ const patched = patchSseBlock(block, items, res);
711
+ successfulTerminalSeen ||= patched.successfulTerminal;
712
+ controller.enqueue(encoder.encode(`${patched.block}${separator}`));
713
+ boundary = findSseBlockBoundary(buffer, final);
714
+ }
715
+ };
363
716
  const transform = new TransformStream<Uint8Array, Uint8Array>({
364
717
  transform(chunk, controller) {
365
718
  buffer += decoder.decode(chunk, { stream: true });
366
- let idx = buffer.indexOf("\n\n");
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
- }
719
+ emitCompleteBlocks(controller, false);
373
720
  },
374
721
  flush(controller) {
722
+ buffer += decoder.decode();
723
+ emitCompleteBlocks(controller, true);
375
724
  if (buffer.length > 0) {
376
- controller.enqueue(encoder.encode(patchSseBlock(buffer, items)));
725
+ const patched = patchSseBlock(buffer, items, res);
726
+ successfulTerminalSeen ||= patched.successfulTerminal;
727
+ controller.enqueue(encoder.encode(patched.block));
728
+ buffer = "";
729
+ }
730
+ if (!successfulTerminalSeen) {
731
+ throw codexSseFailureError(
732
+ res,
733
+ null,
734
+ "invalid_sse_terminal",
735
+ "The Codex response stream ended without a terminal response",
736
+ );
377
737
  }
378
738
  },
379
739
  });
380
740
  const headers = new Headers(res.headers);
381
741
  headers.delete("content-length");
382
- return new Response(res.body.pipeThrough(transform), { status: res.status, headers });
742
+ return new Response(res.body.pipeThrough(transform), {
743
+ status: res.status,
744
+ headers,
745
+ });
746
+ }
747
+
748
+ type SseBlockBoundary = { start: number; end: number };
749
+
750
+ /**
751
+ * Find two consecutive SSE line endings without misreading one CRLF as a bare
752
+ * CR followed by a bare LF. A trailing CR is intentionally held until the next
753
+ * chunk (or final flush), because only then can it be distinguished from the
754
+ * first byte of CRLF.
755
+ */
756
+ function findSseBlockBoundary(value: string, final: boolean): SseBlockBoundary | null {
757
+ for (let index = 0; index < value.length; index += 1) {
758
+ const firstEnd = sseLineEndingEnd(value, index, final);
759
+ if (firstEnd === null) continue;
760
+ const secondEnd = sseLineEndingEnd(value, firstEnd, final);
761
+ if (secondEnd !== null) {
762
+ return { start: index, end: secondEnd };
763
+ }
764
+ index = firstEnd - 1;
765
+ }
766
+ return null;
383
767
  }
384
768
 
385
- /** Collect output_item.done items (mutating `items`); rewrite the terminal event's empty output. */
386
- function patchSseBlock(block: string, items: unknown[]): string {
387
- const lines = block.split("\n");
769
+ function sseLineEndingEnd(value: string, index: number, final: boolean): number | null {
770
+ const current = value[index];
771
+ if (current === "\n") return index + 1;
772
+ if (current !== "\r") return null;
773
+ if (index + 1 < value.length) {
774
+ return value[index + 1] === "\n" ? index + 2 : index + 1;
775
+ }
776
+ return final ? index + 1 : null;
777
+ }
778
+
779
+ type PatchedSseBlock = { block: string; successfulTerminal: boolean };
780
+
781
+ /**
782
+ * Collect output_item.done items and rewrite only a successful terminal event.
783
+ * Failed/error/incomplete terminals throw before their provider message can be
784
+ * exposed to Agents as an ordinary response_done event.
785
+ */
786
+ function patchSseBlock(block: string, items: unknown[], source: Response): PatchedSseBlock {
787
+ const lines = block.split(/\r\n|\r|\n/);
388
788
  const dataStr = lines
389
789
  .filter((l) => l.startsWith("data:"))
390
790
  .map((l) => l.slice(5).trim())
391
791
  .join("\n");
392
792
  if (!dataStr || dataStr === "[DONE]") {
393
- return block;
793
+ return { block, successfulTerminal: false };
394
794
  }
395
- let ev: { type?: string; item?: unknown; response?: Record<string, unknown> };
795
+ let ev: {
796
+ type?: string;
797
+ item?: unknown;
798
+ response?: Record<string, unknown>;
799
+ error?: unknown;
800
+ code?: unknown;
801
+ message?: unknown;
802
+ param?: unknown;
803
+ };
396
804
  try {
397
805
  ev = JSON.parse(dataStr);
398
806
  } catch {
399
- return block;
807
+ return { block, successfulTerminal: false };
400
808
  }
401
809
  if (ev.type === "response.output_item.done" && ev.item !== undefined) {
402
810
  items.push(ev.item);
403
- return block;
811
+ return { block, successfulTerminal: false };
812
+ }
813
+ if (ev.type === "response.failed") {
814
+ throw codexSseFailureError(
815
+ source,
816
+ ev.response?.error,
817
+ "response_failed",
818
+ "The Codex response failed",
819
+ {
820
+ eventType: ev.type,
821
+ responseId: ev.response?.id,
822
+ responseStatus: ev.response?.status,
823
+ },
824
+ );
404
825
  }
405
- if (
406
- (ev.type === "response.completed" ||
407
- ev.type === "response.done" ||
408
- ev.type === "response.incomplete") &&
409
- ev.response
410
- ) {
826
+ if (ev.type === "error" || ev.type === "response.error") {
827
+ throw codexSseFailureError(
828
+ source,
829
+ ev.error ?? ev.response?.error ?? ev,
830
+ "response_error",
831
+ "The Codex response stream reported an error",
832
+ {
833
+ eventType: ev.type,
834
+ responseId: ev.response?.id,
835
+ responseStatus: ev.response?.status,
836
+ },
837
+ );
838
+ }
839
+ if (ev.type === "response.incomplete") {
840
+ const details = ev.response?.incomplete_details;
841
+ const reason =
842
+ details && typeof details === "object"
843
+ ? (details as Record<string, unknown>).reason
844
+ : undefined;
845
+ throw codexSseFailureError(
846
+ source,
847
+ {
848
+ code: "response_incomplete",
849
+ message:
850
+ typeof reason === "string" && reason.length > 0
851
+ ? `The Codex response was incomplete (${reason})`
852
+ : "The Codex response was incomplete",
853
+ },
854
+ "response_incomplete",
855
+ "The Codex response was incomplete",
856
+ {
857
+ eventType: ev.type,
858
+ responseId: ev.response?.id,
859
+ responseStatus: ev.response?.status,
860
+ },
861
+ );
862
+ }
863
+ if ((ev.type === "response.completed" || ev.type === "response.done") && ev.response) {
864
+ if (
865
+ ev.response.status === "failed" ||
866
+ ev.response.status === "incomplete" ||
867
+ (ev.response.error !== null && ev.response.error !== undefined)
868
+ ) {
869
+ throw codexSseFailureError(
870
+ source,
871
+ ev.response.error,
872
+ ev.response.status === "incomplete" ? "response_incomplete" : "response_failed",
873
+ ev.response.status === "incomplete"
874
+ ? "The Codex response was incomplete"
875
+ : "The Codex response failed",
876
+ {
877
+ eventType: ev.type,
878
+ responseId: ev.response.id,
879
+ responseStatus: ev.response.status,
880
+ },
881
+ );
882
+ }
411
883
  const out = ev.response.output;
412
884
  if ((!Array.isArray(out) || out.length === 0) && items.length > 0) {
413
885
  ev.response = { ...ev.response, output: items };
414
886
  const nonData = lines.filter((l) => !l.startsWith("data:"));
415
- return [...nonData, `data: ${JSON.stringify(ev)}`].join("\n");
887
+ const lineEnding = block.match(/\r\n|\r|\n/)?.[0] ?? "\n";
888
+ return {
889
+ block: [...nonData, `data: ${JSON.stringify(ev)}`].join(lineEnding),
890
+ successfulTerminal: true,
891
+ };
416
892
  }
893
+ return { block, successfulTerminal: true };
417
894
  }
418
- return block;
895
+ return { block, successfulTerminal: false };
419
896
  }
package/src/index.ts CHANGED
@@ -8,3 +8,4 @@ export * from "./api-client";
8
8
  export * from "./request-context";
9
9
  export * from "./fetch";
10
10
  export * from "./mcp-sanitize";
11
+ export * from "./model-output-truncation";