@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.
- package/dist/api-client.d.ts +2 -2
- package/dist/fetch.d.ts +13 -5
- package/dist/images.d.ts +34 -0
- package/dist/index.d.ts +17 -16
- package/dist/index.js +316 -62
- package/dist/index.js.map +1 -1
- package/dist/mcp-sanitize.d.ts +1 -1
- package/dist/model-output-truncation.d.ts +1 -1
- package/dist/normalize.d.ts +6 -0
- package/dist/realtime.d.ts +4 -4
- package/dist/refresh.d.ts +1 -1
- package/dist/response-timeout.d.ts +1 -1
- package/dist/usage-normalize.d.ts +1 -1
- package/package.json +2 -1
- package/src/fetch.ts +166 -77
- package/src/images.ts +209 -0
- package/src/index.ts +1 -0
- package/src/mcp-sanitize.ts +12 -8
- package/src/model-output-truncation.ts +7 -1
- package/src/normalize.ts +26 -0
package/dist/mcp-sanitize.d.ts
CHANGED
|
@@ -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;
|
package/dist/normalize.d.ts
CHANGED
|
@@ -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
|
package/dist/realtime.d.ts
CHANGED
|
@@ -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 { 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.
|
|
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
|
|
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
|
|
490
|
-
//
|
|
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
|
|
495
|
-
|
|
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
|
-
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
|
591
|
-
// account
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
|
598
|
-
//
|
|
599
|
-
//
|
|
600
|
-
//
|
|
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 ?
|
|
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
|
|
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(
|
|
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
|
|
1020
|
-
*
|
|
1021
|
-
*
|
|
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
|
|
1034
|
-
super(
|
|
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
|
-
*
|
|
1081
|
-
*
|
|
1082
|
-
*
|
|
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
|
|
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
|
-
|
|
1120
|
-
|
|
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
|
-
|
|
1135
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
1192
|
-
*
|
|
1193
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
1393
|
+
return false;
|
|
1305
1394
|
}
|