@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/dist/{chunk-KKKJKW6M.js → chunk-AMRVHBCD.js} +2 -2
- package/dist/{chunk-KKKJKW6M.js.map → chunk-AMRVHBCD.js.map} +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/index.d.ts +89 -1
- package/dist/index.js +767 -27
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +2 -2
- package/src/fetch.ts +515 -38
- package/src/index.ts +1 -0
- package/src/model-output-truncation.ts +590 -0
- package/src/oversized-image-card.ts +18 -0
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-
|
|
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.
|
|
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, {
|
|
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
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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), {
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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: {
|
|
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
|
-
(
|
|
407
|
-
|
|
408
|
-
ev.
|
|
409
|
-
|
|
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
|
-
|
|
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