@f5-sales-demo/pi-ai 22.5.1 → 22.6.0
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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/pi-ai",
|
|
4
|
-
"version": "22.
|
|
4
|
+
"version": "22.6.0",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://github.com/f5-sales-demo/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"@anthropic-ai/sdk": "0.124.0",
|
|
47
47
|
"@aws-sdk/client-bedrock-runtime": "3.1127.0",
|
|
48
48
|
"@bufbuild/protobuf": "2.14.1",
|
|
49
|
-
"@f5-sales-demo/pi-utils": "22.
|
|
49
|
+
"@f5-sales-demo/pi-utils": "22.6.0",
|
|
50
50
|
"@google/genai": "2.21.0",
|
|
51
51
|
"@sinclair/typebox": "0.34.52",
|
|
52
52
|
"@smithy/node-http-handler": "4.12.1",
|
|
@@ -192,7 +192,7 @@ function getCacheControl(
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
// Stealth mode: Mimic Claude Code headers and tool prefixing.
|
|
195
|
-
export const claudeCodeVersion = "2.1.
|
|
195
|
+
export const claudeCodeVersion = "2.1.286";
|
|
196
196
|
export const claudeToolPrefix: string = "proxy_";
|
|
197
197
|
export const claudeCodeSystemInstruction = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
198
198
|
|
|
@@ -169,6 +169,7 @@ interface CodexProviderSessionState extends ProviderSessionState {
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
interface CodexRequestContext {
|
|
172
|
+
model: Model<"openai-codex-responses">;
|
|
172
173
|
grammarToolInputProperties?: Map<string, string>;
|
|
173
174
|
apiKey: string;
|
|
174
175
|
accountId: string;
|
|
@@ -187,7 +188,16 @@ interface CodexRequestSetup {
|
|
|
187
188
|
requestAbortController: AbortController;
|
|
188
189
|
}
|
|
189
190
|
|
|
191
|
+
interface CodexWebSocketRequestState {
|
|
192
|
+
usedContinuation: boolean;
|
|
193
|
+
connection?: CodexWebSocketConnection;
|
|
194
|
+
}
|
|
195
|
+
|
|
190
196
|
interface CodexStreamRuntime {
|
|
197
|
+
websocketRequestState?: CodexWebSocketRequestState;
|
|
198
|
+
continuationRecoveryAttempt: number;
|
|
199
|
+
sawResponseAccepted: boolean;
|
|
200
|
+
sawOutputEvent: boolean;
|
|
191
201
|
grammarToolInputProperties?: Map<string, string>;
|
|
192
202
|
eventStream: AsyncGenerator<Record<string, unknown>>;
|
|
193
203
|
requestBodyForState: RequestBody;
|
|
@@ -470,6 +480,7 @@ async function buildCodexRequestContext(
|
|
|
470
480
|
const replacement = await options?.onPayload?.(transformedBody, model);
|
|
471
481
|
if (replacement !== undefined) transformedBody = replacement as typeof transformedBody;
|
|
472
482
|
validateFinalResponsesRequest(model, transformedBody);
|
|
483
|
+
transformedBody = structuredCloneJSON(transformedBody);
|
|
473
484
|
|
|
474
485
|
const requestHeaders = { ...(model.headers ?? {}), ...(options?.headers ?? {}) };
|
|
475
486
|
const rawRequestDump: RawHttpRequestDump = {
|
|
@@ -489,6 +500,33 @@ async function buildCodexRequestContext(
|
|
|
489
500
|
}
|
|
490
501
|
const websocketState =
|
|
491
502
|
sessionKey && providerSessionState ? getCodexWebSocketSessionState(sessionKey, providerSessionState) : undefined;
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
model,
|
|
506
|
+
apiKey,
|
|
507
|
+
grammarToolInputProperties: new Map(
|
|
508
|
+
(context.tools ?? []).flatMap(tool =>
|
|
509
|
+
tool.constrainedSampling && tool.constrainedSampling.type === "grammar"
|
|
510
|
+
? [[tool.name, (tool.parameters as { required?: string[] }).required?.[0] ?? "input"]]
|
|
511
|
+
: [],
|
|
512
|
+
),
|
|
513
|
+
),
|
|
514
|
+
accountId,
|
|
515
|
+
baseUrl,
|
|
516
|
+
url,
|
|
517
|
+
requestHeaders,
|
|
518
|
+
providerSessionState,
|
|
519
|
+
websocketState,
|
|
520
|
+
transformedBody,
|
|
521
|
+
rawRequestDump,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function applyCodexCachedReasoning(
|
|
526
|
+
model: Model<"openai-codex-responses">,
|
|
527
|
+
transformedBody: RequestBody,
|
|
528
|
+
websocketState: CodexWebSocketSessionState,
|
|
529
|
+
): void {
|
|
492
530
|
if (
|
|
493
531
|
model.compat?.supportsCachedReasoningUpdates &&
|
|
494
532
|
websocketState?.lastRequest?.reasoning?.effort &&
|
|
@@ -520,25 +558,6 @@ async function buildCodexRequestContext(
|
|
|
520
558
|
transformedBody.reasoning.effort = previous.reasoning!.effort;
|
|
521
559
|
}
|
|
522
560
|
}
|
|
523
|
-
|
|
524
|
-
return {
|
|
525
|
-
apiKey,
|
|
526
|
-
grammarToolInputProperties: new Map(
|
|
527
|
-
(context.tools ?? []).flatMap(tool =>
|
|
528
|
-
tool.constrainedSampling && tool.constrainedSampling.type === "grammar"
|
|
529
|
-
? [[tool.name, (tool.parameters as { required?: string[] }).required?.[0] ?? "input"]]
|
|
530
|
-
: [],
|
|
531
|
-
),
|
|
532
|
-
),
|
|
533
|
-
accountId,
|
|
534
|
-
baseUrl,
|
|
535
|
-
url,
|
|
536
|
-
requestHeaders,
|
|
537
|
-
providerSessionState,
|
|
538
|
-
websocketState,
|
|
539
|
-
transformedBody,
|
|
540
|
-
rawRequestDump,
|
|
541
|
-
};
|
|
542
561
|
}
|
|
543
562
|
|
|
544
563
|
async function buildTransformedCodexRequestBody(
|
|
@@ -626,6 +645,7 @@ async function openInitialCodexEventStream(
|
|
|
626
645
|
eventStream: AsyncGenerator<Record<string, unknown>>;
|
|
627
646
|
requestBodyForState: RequestBody;
|
|
628
647
|
transport: CodexTransport;
|
|
648
|
+
websocketRequestState?: CodexWebSocketRequestState;
|
|
629
649
|
}> {
|
|
630
650
|
const { transformedBody, websocketState } = requestContext;
|
|
631
651
|
if (websocketState && shouldUseCodexWebSocket(model, websocketState, options?.preferWebsockets)) {
|
|
@@ -641,6 +661,7 @@ async function openInitialCodexEventStream(
|
|
|
641
661
|
websocketRetries,
|
|
642
662
|
);
|
|
643
663
|
} catch (error) {
|
|
664
|
+
if (requestSetup.requestSignal.aborted) throw error;
|
|
644
665
|
const websocketError = error instanceof Error ? error : new Error(String(error));
|
|
645
666
|
const isFatal = isCodexWebSocketFatalError(websocketError);
|
|
646
667
|
const activateFallback = isFatal || websocketRetries >= websocketRetryBudget;
|
|
@@ -673,55 +694,73 @@ async function openCodexWebSocketTransport(
|
|
|
673
694
|
eventStream: AsyncGenerator<Record<string, unknown>>;
|
|
674
695
|
requestBodyForState: RequestBody;
|
|
675
696
|
transport: CodexTransport;
|
|
697
|
+
websocketRequestState?: CodexWebSocketRequestState;
|
|
676
698
|
}> {
|
|
677
|
-
|
|
678
|
-
|
|
699
|
+
const headers = () =>
|
|
700
|
+
createCodexHeaders(
|
|
701
|
+
requestContext.requestHeaders,
|
|
702
|
+
requestContext.accountId,
|
|
703
|
+
requestContext.apiKey,
|
|
704
|
+
options?.sessionId,
|
|
705
|
+
"websocket",
|
|
706
|
+
websocketState,
|
|
707
|
+
);
|
|
708
|
+
let connection = await getOrCreateCodexWebSocketConnection(
|
|
709
|
+
websocketState,
|
|
710
|
+
toWebSocketUrl(requestContext.url),
|
|
711
|
+
headers(),
|
|
712
|
+
requestSetup.requestSignal,
|
|
713
|
+
);
|
|
714
|
+
// Divergent accepted steering belongs to the old socket; reconnect before preparing the request.
|
|
679
715
|
if (websocketState.acceptedSteering?.length) {
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
|
|
716
|
+
const preview = structuredCloneJSON(requestContext.transformedBody);
|
|
717
|
+
applyCodexCachedReasoning(requestContext.model, preview, websocketState);
|
|
718
|
+
const request = buildCodexWebSocketRequest(preview, { ...websocketState });
|
|
683
719
|
const plan = planSteeredRequest(
|
|
684
|
-
|
|
720
|
+
request.previous_response_id ? (request.input as InputItem[]) : undefined,
|
|
685
721
|
websocketState.acceptedSteering.flatMap(steer => steer.items),
|
|
686
722
|
);
|
|
687
723
|
if (plan.kind === "discard") {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
requestContext.accountId,
|
|
698
|
-
requestContext.apiKey,
|
|
699
|
-
options?.sessionId,
|
|
700
|
-
"websocket",
|
|
701
|
-
websocketState,
|
|
702
|
-
);
|
|
724
|
+
connection.close("steering-diverged");
|
|
725
|
+
connection = await getOrCreateCodexWebSocketConnection(
|
|
726
|
+
websocketState,
|
|
727
|
+
toWebSocketUrl(requestContext.url),
|
|
728
|
+
headers(),
|
|
729
|
+
requestSetup.requestSignal,
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
703
733
|
const requestBodyForState = structuredCloneJSON(requestContext.transformedBody);
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
734
|
+
const websocketRequestState: CodexWebSocketRequestState = { usedContinuation: false, connection };
|
|
735
|
+
const eventStream = connection.streamRequest(
|
|
736
|
+
() => {
|
|
737
|
+
applyCodexCachedReasoning(requestContext.model, requestBodyForState, websocketState);
|
|
738
|
+
const request = buildCodexWebSocketRequest(requestBodyForState, websocketState);
|
|
739
|
+
let attach = false;
|
|
740
|
+
if (websocketState.acceptedSteering?.length) {
|
|
741
|
+
const plan = planSteeredRequest(
|
|
742
|
+
request.previous_response_id ? (request.input as InputItem[]) : undefined,
|
|
743
|
+
websocketState.acceptedSteering.flatMap(steer => steer.items),
|
|
744
|
+
);
|
|
745
|
+
if (plan.kind === "attach") attach = true;
|
|
746
|
+
else if (plan.kind === "create") request.input = plan.input;
|
|
747
|
+
websocketState.acceptedSteering = undefined;
|
|
748
|
+
}
|
|
749
|
+
websocketRequestState.usedContinuation =
|
|
750
|
+
typeof request.previous_response_id === "string" &&
|
|
751
|
+
request.previous_response_id === websocketState.lastResponseId;
|
|
752
|
+
logCodexDebug("codex websocket request", {
|
|
753
|
+
retry,
|
|
754
|
+
retryBudget: getCodexWebSocketRetryBudget(),
|
|
755
|
+
requestType: request.type,
|
|
756
|
+
});
|
|
757
|
+
return { request, attach };
|
|
758
|
+
},
|
|
720
759
|
requestSetup.requestSignal,
|
|
721
760
|
options?.liveSteering,
|
|
722
|
-
|
|
761
|
+
websocketState,
|
|
723
762
|
);
|
|
724
|
-
return { eventStream, requestBodyForState, transport: "websocket" };
|
|
763
|
+
return { eventStream, requestBodyForState, transport: "websocket", websocketRequestState };
|
|
725
764
|
}
|
|
726
765
|
|
|
727
766
|
async function openCodexSseTransport(
|
|
@@ -734,6 +773,7 @@ async function openCodexSseTransport(
|
|
|
734
773
|
eventStream: AsyncGenerator<Record<string, unknown>>;
|
|
735
774
|
requestBodyForState: RequestBody;
|
|
736
775
|
transport: CodexTransport;
|
|
776
|
+
websocketRequestState?: CodexWebSocketRequestState;
|
|
737
777
|
}> {
|
|
738
778
|
const eventStream = requestSetup.wrapCodexSseStream(
|
|
739
779
|
await openCodexSseEventStream(
|
|
@@ -762,6 +802,7 @@ async function reopenCodexWebSocketRuntimeStream(
|
|
|
762
802
|
state,
|
|
763
803
|
runtime.websocketStreamRetries,
|
|
764
804
|
);
|
|
805
|
+
runtime.websocketRequestState = next.websocketRequestState;
|
|
765
806
|
runtime.eventStream = next.eventStream;
|
|
766
807
|
runtime.requestBodyForState = next.requestBodyForState;
|
|
767
808
|
runtime.transport = next.transport;
|
|
@@ -774,6 +815,7 @@ async function reopenCodexSseRuntimeStream(
|
|
|
774
815
|
state: CodexWebSocketSessionState | undefined,
|
|
775
816
|
): Promise<void> {
|
|
776
817
|
const next = await openCodexSseTransport(context.requestContext, context.requestSetup, context.options, state);
|
|
818
|
+
runtime.websocketRequestState = next.websocketRequestState;
|
|
777
819
|
runtime.eventStream = next.eventStream;
|
|
778
820
|
runtime.requestBodyForState = next.requestBodyForState;
|
|
779
821
|
runtime.transport = next.transport;
|
|
@@ -787,8 +829,13 @@ function createCodexStreamRuntime(initial: {
|
|
|
787
829
|
requestBodyForState: RequestBody;
|
|
788
830
|
transport: CodexTransport;
|
|
789
831
|
websocketState?: CodexWebSocketSessionState;
|
|
832
|
+
websocketRequestState?: CodexWebSocketRequestState;
|
|
790
833
|
}): CodexStreamRuntime {
|
|
791
834
|
return {
|
|
835
|
+
websocketRequestState: initial.websocketRequestState,
|
|
836
|
+
continuationRecoveryAttempt: 0,
|
|
837
|
+
sawResponseAccepted: false,
|
|
838
|
+
sawOutputEvent: false,
|
|
792
839
|
eventStream: initial.eventStream,
|
|
793
840
|
requestBodyForState: initial.requestBodyForState,
|
|
794
841
|
transport: initial.transport,
|
|
@@ -847,6 +894,14 @@ function handleCodexStreamEvent(args: {
|
|
|
847
894
|
const { model, output, stream, runtime, rawEvent } = args;
|
|
848
895
|
const eventType = typeof rawEvent.type === "string" ? rawEvent.type : "";
|
|
849
896
|
if (!eventType) return args.firstTokenTime;
|
|
897
|
+
if (
|
|
898
|
+
eventType === "response.created" ||
|
|
899
|
+
eventType === "response.in_progress" ||
|
|
900
|
+
(eventType === "response.failed" && typeof asRecord(rawEvent.response)?.id === "string")
|
|
901
|
+
)
|
|
902
|
+
runtime.sawResponseAccepted = true;
|
|
903
|
+
if (/^response\.(?:output|content|reasoning|function_call|custom_tool_call)/.test(eventType))
|
|
904
|
+
runtime.sawOutputEvent = true;
|
|
850
905
|
|
|
851
906
|
if (eventType !== "response.output_item.added" && typeof rawEvent.output_index === "number") {
|
|
852
907
|
const slot = runtime.outputSlots.get(rawEvent.output_index);
|
|
@@ -1215,7 +1270,14 @@ function handleResponseCreated(
|
|
|
1215
1270
|
};
|
|
1216
1271
|
}
|
|
1217
1272
|
const state = runtime.websocketState;
|
|
1218
|
-
if (
|
|
1273
|
+
if (
|
|
1274
|
+
runtime.transport === "websocket" &&
|
|
1275
|
+
state &&
|
|
1276
|
+
state.connection === runtime.websocketRequestState?.connection &&
|
|
1277
|
+
state.connection?.isOpen() &&
|
|
1278
|
+
typeof response?.id === "string" &&
|
|
1279
|
+
response.id.length > 0
|
|
1280
|
+
) {
|
|
1219
1281
|
state.lastResponseId = response.id;
|
|
1220
1282
|
}
|
|
1221
1283
|
}
|
|
@@ -1281,7 +1343,12 @@ function handleResponseCompleted(
|
|
|
1281
1343
|
}
|
|
1282
1344
|
|
|
1283
1345
|
const state = runtime.websocketState;
|
|
1284
|
-
if (
|
|
1346
|
+
if (
|
|
1347
|
+
runtime.transport === "websocket" &&
|
|
1348
|
+
state &&
|
|
1349
|
+
state.connection === runtime.websocketRequestState?.connection &&
|
|
1350
|
+
state.connection?.isOpen()
|
|
1351
|
+
) {
|
|
1285
1352
|
state.lastRequest = structuredCloneJSON(runtime.requestBodyForState);
|
|
1286
1353
|
if (typeof response?.id === "string" && response.id.length > 0) {
|
|
1287
1354
|
state.lastResponseId = response.id;
|
|
@@ -1309,6 +1376,9 @@ async function recoverCodexStreamError(
|
|
|
1309
1376
|
runtime: CodexStreamRuntime,
|
|
1310
1377
|
error: unknown,
|
|
1311
1378
|
): Promise<boolean> {
|
|
1379
|
+
if (isCodexRejectedContinuation(error)) {
|
|
1380
|
+
return tryRecoverCodexContinuation(context, runtime);
|
|
1381
|
+
}
|
|
1312
1382
|
if (await tryReconnectCodexWebSocketOnConnectionLimit(context, runtime, error)) {
|
|
1313
1383
|
return true;
|
|
1314
1384
|
}
|
|
@@ -1321,6 +1391,55 @@ async function recoverCodexStreamError(
|
|
|
1321
1391
|
return false;
|
|
1322
1392
|
}
|
|
1323
1393
|
|
|
1394
|
+
function isCodexRejectedContinuation(error: unknown): boolean {
|
|
1395
|
+
return (
|
|
1396
|
+
error instanceof CodexProviderStreamError &&
|
|
1397
|
+
(error.code === "previous_response_not_found" ||
|
|
1398
|
+
(error.code === "invalid_request_error" &&
|
|
1399
|
+
(error.param === "previous_response_id" || /\bprevious_response_id\b/i.test(error.providerMessage))))
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
async function tryRecoverCodexContinuation(
|
|
1404
|
+
context: CodexStreamProcessingContext,
|
|
1405
|
+
runtime: CodexStreamRuntime,
|
|
1406
|
+
): Promise<boolean> {
|
|
1407
|
+
const state = runtime.websocketState;
|
|
1408
|
+
if (!state || runtime.transport !== "websocket") return false;
|
|
1409
|
+
const eligible =
|
|
1410
|
+
runtime.websocketRequestState?.usedContinuation &&
|
|
1411
|
+
runtime.continuationRecoveryAttempt === 0 &&
|
|
1412
|
+
!runtime.sawResponseAccepted &&
|
|
1413
|
+
!runtime.sawOutputEvent &&
|
|
1414
|
+
!runtime.sawTerminalEvent &&
|
|
1415
|
+
context.output.content.length === 0 &&
|
|
1416
|
+
!context.requestSetup.requestSignal.aborted;
|
|
1417
|
+
state.connection?.close("continuation-rejected");
|
|
1418
|
+
state.connection = undefined;
|
|
1419
|
+
resetCodexWebSocketAppendState(state);
|
|
1420
|
+
resetCodexSessionMetadata(state);
|
|
1421
|
+
if (!eligible) return false;
|
|
1422
|
+
runtime.continuationRecoveryAttempt += 1;
|
|
1423
|
+
logCodexDebug("codex continuation recovery", {
|
|
1424
|
+
reason: "previous_response_unavailable",
|
|
1425
|
+
attempt: runtime.continuationRecoveryAttempt,
|
|
1426
|
+
transport: "websocket",
|
|
1427
|
+
});
|
|
1428
|
+
// Connection failures retain the normal WebSocket retry/SSE fallback policy, with full context.
|
|
1429
|
+
const next = await openInitialCodexEventStream(
|
|
1430
|
+
context.model,
|
|
1431
|
+
context.options,
|
|
1432
|
+
context.requestSetup,
|
|
1433
|
+
context.requestContext,
|
|
1434
|
+
);
|
|
1435
|
+
runtime.eventStream = next.eventStream;
|
|
1436
|
+
runtime.requestBodyForState = next.requestBodyForState;
|
|
1437
|
+
runtime.websocketRequestState = next.websocketRequestState;
|
|
1438
|
+
runtime.transport = next.transport;
|
|
1439
|
+
state.lastTransport = next.transport;
|
|
1440
|
+
return true;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1324
1443
|
/**
|
|
1325
1444
|
* Handles `websocket_connection_limit_reached` errors by closing the stale connection
|
|
1326
1445
|
* and opening a fresh websocket. If content has already been emitted to the caller,
|
|
@@ -1586,6 +1705,7 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
|
|
|
1586
1705
|
options,
|
|
1587
1706
|
requestSetup,
|
|
1588
1707
|
requestContext: {
|
|
1708
|
+
model,
|
|
1589
1709
|
apiKey: "",
|
|
1590
1710
|
accountId: "",
|
|
1591
1711
|
baseUrl: model.baseUrl || CODEX_BASE_URL,
|
|
@@ -1684,6 +1804,7 @@ function getCodexWebSocketSessionState(
|
|
|
1684
1804
|
}
|
|
1685
1805
|
|
|
1686
1806
|
function resetCodexWebSocketAppendState(state: CodexWebSocketSessionState): void {
|
|
1807
|
+
state.acceptedSteering = undefined;
|
|
1687
1808
|
state.canAppend = false;
|
|
1688
1809
|
state.lastRequest = undefined;
|
|
1689
1810
|
state.lastResponseItems = undefined;
|
|
@@ -1698,6 +1819,7 @@ function resetCodexSessionMetadata(state: CodexWebSocketSessionState): void {
|
|
|
1698
1819
|
|
|
1699
1820
|
function recordCodexWebSocketFailure(state: CodexWebSocketSessionState, activateFallback: boolean): void {
|
|
1700
1821
|
resetCodexWebSocketAppendState(state);
|
|
1822
|
+
resetCodexSessionMetadata(state);
|
|
1701
1823
|
state.connection?.close("fallback");
|
|
1702
1824
|
state.connection = undefined;
|
|
1703
1825
|
state.lastFallbackAt = Date.now();
|
|
@@ -1843,6 +1965,7 @@ interface CodexWebSocketConnectionOptions {
|
|
|
1843
1965
|
idleTimeoutMs: number;
|
|
1844
1966
|
firstEventTimeoutMs: number;
|
|
1845
1967
|
onHandshakeHeaders?: (headers: Headers) => void;
|
|
1968
|
+
onClose?: () => void;
|
|
1846
1969
|
}
|
|
1847
1970
|
|
|
1848
1971
|
class CodexWebSocketConnection {
|
|
@@ -1851,6 +1974,7 @@ class CodexWebSocketConnection {
|
|
|
1851
1974
|
#idleTimeoutMs: number;
|
|
1852
1975
|
#firstEventTimeoutMs: number;
|
|
1853
1976
|
#onHandshakeHeaders?: (headers: Headers) => void;
|
|
1977
|
+
#onClose?: () => void;
|
|
1854
1978
|
#socket: WebSocket | null = null;
|
|
1855
1979
|
#queue: Array<Record<string, unknown> | Error | null> = [];
|
|
1856
1980
|
#waiters: Array<() => void> = [];
|
|
@@ -1864,6 +1988,7 @@ class CodexWebSocketConnection {
|
|
|
1864
1988
|
this.#idleTimeoutMs = options.idleTimeoutMs;
|
|
1865
1989
|
this.#firstEventTimeoutMs = options.firstEventTimeoutMs;
|
|
1866
1990
|
this.#onHandshakeHeaders = options.onHandshakeHeaders;
|
|
1991
|
+
this.#onClose = options.onClose;
|
|
1867
1992
|
}
|
|
1868
1993
|
|
|
1869
1994
|
isOpen(): boolean {
|
|
@@ -1882,6 +2007,7 @@ class CodexWebSocketConnection {
|
|
|
1882
2007
|
this.#socket.close(1000, reason);
|
|
1883
2008
|
}
|
|
1884
2009
|
this.#socket = null;
|
|
2010
|
+
this.#onClose?.();
|
|
1885
2011
|
for (const waiter of this.#steerWaiters.values()) waiter.reject(new Error("Steering socket closed"));
|
|
1886
2012
|
this.#steerWaiters.clear();
|
|
1887
2013
|
}
|
|
@@ -1929,6 +2055,7 @@ class CodexWebSocketConnection {
|
|
|
1929
2055
|
}, CODEX_WEBSOCKET_CONNECT_TIMEOUT_MS);
|
|
1930
2056
|
|
|
1931
2057
|
socket.addEventListener("open", event => {
|
|
2058
|
+
if (this.#socket !== socket) return;
|
|
1932
2059
|
if (!settled) {
|
|
1933
2060
|
settled = true;
|
|
1934
2061
|
clearPending();
|
|
@@ -1937,6 +2064,7 @@ class CodexWebSocketConnection {
|
|
|
1937
2064
|
}
|
|
1938
2065
|
});
|
|
1939
2066
|
socket.addEventListener("error", event => {
|
|
2067
|
+
if (this.#socket !== socket) return;
|
|
1940
2068
|
const eventRecord = event as unknown as Record<string, unknown>;
|
|
1941
2069
|
const detail =
|
|
1942
2070
|
(typeof eventRecord.message === "string" && eventRecord.message) ||
|
|
@@ -1952,6 +2080,7 @@ class CodexWebSocketConnection {
|
|
|
1952
2080
|
this.#push(error);
|
|
1953
2081
|
});
|
|
1954
2082
|
socket.addEventListener("close", event => {
|
|
2083
|
+
if (this.#socket !== socket) return;
|
|
1955
2084
|
this.close("closed");
|
|
1956
2085
|
if (!settled) {
|
|
1957
2086
|
settled = true;
|
|
@@ -1963,6 +2092,7 @@ class CodexWebSocketConnection {
|
|
|
1963
2092
|
this.#push(null);
|
|
1964
2093
|
});
|
|
1965
2094
|
socket.addEventListener("message", event => {
|
|
2095
|
+
if (this.#socket !== socket) return;
|
|
1966
2096
|
if (typeof event.data !== "string") return;
|
|
1967
2097
|
try {
|
|
1968
2098
|
const parsed = JSON.parse(event.data) as Record<string, unknown>;
|
|
@@ -2016,11 +2146,10 @@ class CodexWebSocketConnection {
|
|
|
2016
2146
|
return promise;
|
|
2017
2147
|
}
|
|
2018
2148
|
async *streamRequest(
|
|
2019
|
-
request: Record<string, unknown
|
|
2149
|
+
prepare: () => { request: Record<string, unknown>; attach: boolean },
|
|
2020
2150
|
signal?: AbortSignal,
|
|
2021
2151
|
liveSteering?: import("../types").LiveSteering,
|
|
2022
2152
|
state?: CodexWebSocketSessionState,
|
|
2023
|
-
attach = false,
|
|
2024
2153
|
): AsyncGenerator<Record<string, unknown>> {
|
|
2025
2154
|
if (!this.#socket || this.#socket.readyState !== WebSocket.OPEN) {
|
|
2026
2155
|
throw createCodexWebSocketTransportError("websocket connection is unavailable");
|
|
@@ -2050,7 +2179,9 @@ class CodexWebSocketConnection {
|
|
|
2050
2179
|
)
|
|
2051
2180
|
: undefined;
|
|
2052
2181
|
try {
|
|
2053
|
-
if (
|
|
2182
|
+
if (signal?.aborted) throw createCodexWebSocketTransportError("request was aborted");
|
|
2183
|
+
const { request, attach } = prepare();
|
|
2184
|
+
if (!attach) this.#socket!.send(JSON.stringify(request));
|
|
2054
2185
|
let sawFirstEvent = false;
|
|
2055
2186
|
while (true) {
|
|
2056
2187
|
const next = await this.#nextMessage(
|
|
@@ -2079,7 +2210,7 @@ class CodexWebSocketConnection {
|
|
|
2079
2210
|
) {
|
|
2080
2211
|
if (pump) {
|
|
2081
2212
|
const outcome = await pump.finish();
|
|
2082
|
-
if (state) state.acceptedSteering = outcome.accepted;
|
|
2213
|
+
if (state && state.connection === this && this.isOpen()) state.acceptedSteering = outcome.accepted;
|
|
2083
2214
|
if (outcome.uncertain) {
|
|
2084
2215
|
this.close("steering-uncertain");
|
|
2085
2216
|
if (state) resetCodexWebSocketAppendState(state);
|
|
@@ -2153,16 +2284,25 @@ async function getOrCreateCodexWebSocketConnection(
|
|
|
2153
2284
|
}
|
|
2154
2285
|
state.connection?.close("reconnect");
|
|
2155
2286
|
resetCodexWebSocketAppendState(state);
|
|
2287
|
+
resetCodexSessionMetadata(state);
|
|
2288
|
+
for (const name of [X_CODEX_TURN_STATE_HEADER, X_MODELS_ETAG_HEADER, X_REASONING_INCLUDED_HEADER])
|
|
2289
|
+
delete headerRecord[name];
|
|
2156
2290
|
logger.time("codexWs:newSocket");
|
|
2157
|
-
|
|
2291
|
+
const connection = new CodexWebSocketConnection(url, headerRecord, {
|
|
2158
2292
|
idleTimeoutMs: getCodexWebSocketIdleTimeoutMs(),
|
|
2159
2293
|
firstEventTimeoutMs: getCodexWebSocketFirstEventTimeoutMs(),
|
|
2160
2294
|
onHandshakeHeaders: handshakeHeaders => {
|
|
2161
|
-
updateCodexSessionMetadataFromHeaders(state, handshakeHeaders);
|
|
2295
|
+
if (state.connection === connection) updateCodexSessionMetadataFromHeaders(state, handshakeHeaders);
|
|
2296
|
+
},
|
|
2297
|
+
onClose: () => {
|
|
2298
|
+
if (state.connection !== connection) return;
|
|
2299
|
+
resetCodexWebSocketAppendState(state);
|
|
2300
|
+
resetCodexSessionMetadata(state);
|
|
2162
2301
|
},
|
|
2163
2302
|
});
|
|
2164
|
-
|
|
2165
|
-
|
|
2303
|
+
state.connection = connection;
|
|
2304
|
+
await connection.connect(signal);
|
|
2305
|
+
return connection;
|
|
2166
2306
|
}
|
|
2167
2307
|
|
|
2168
2308
|
async function openCodexSseEventStream(
|
|
@@ -2216,19 +2356,6 @@ async function openCodexSseEventStream(
|
|
|
2216
2356
|
return readSseJson<Record<string, unknown>>(response.body, signal);
|
|
2217
2357
|
}
|
|
2218
2358
|
|
|
2219
|
-
async function openCodexWebSocketEventStream(
|
|
2220
|
-
url: string,
|
|
2221
|
-
headers: Headers,
|
|
2222
|
-
request: Record<string, unknown>,
|
|
2223
|
-
state: CodexWebSocketSessionState,
|
|
2224
|
-
signal?: AbortSignal,
|
|
2225
|
-
liveSteering?: import("../types").LiveSteering,
|
|
2226
|
-
attach = false,
|
|
2227
|
-
): Promise<AsyncGenerator<Record<string, unknown>>> {
|
|
2228
|
-
const connection = await getOrCreateCodexWebSocketConnection(state, url, headers, signal);
|
|
2229
|
-
return connection.streamRequest(request, signal, liveSteering, state, attach);
|
|
2230
|
-
}
|
|
2231
|
-
|
|
2232
2359
|
function createCodexHeaders(
|
|
2233
2360
|
initHeaders: Record<string, string> | undefined,
|
|
2234
2361
|
accountId: string,
|
|
@@ -2579,12 +2706,16 @@ function sanitizeProviderFailureCode(value: unknown): string | undefined {
|
|
|
2579
2706
|
class CodexProviderStreamError extends Error {
|
|
2580
2707
|
readonly retryable: boolean;
|
|
2581
2708
|
readonly code?: string;
|
|
2709
|
+
readonly param?: string;
|
|
2710
|
+
readonly providerMessage: string;
|
|
2582
2711
|
|
|
2583
|
-
constructor(message: string, retryable: boolean, code?: string) {
|
|
2712
|
+
constructor(message: string, retryable: boolean, code?: string, param?: string, providerMessage = message) {
|
|
2584
2713
|
super(message);
|
|
2585
2714
|
this.name = "CodexProviderStreamError";
|
|
2586
2715
|
this.retryable = retryable;
|
|
2587
2716
|
this.code = sanitizeProviderFailureCode(code);
|
|
2717
|
+
this.param = param;
|
|
2718
|
+
this.providerMessage = providerMessage;
|
|
2588
2719
|
}
|
|
2589
2720
|
}
|
|
2590
2721
|
|
|
@@ -2595,6 +2726,7 @@ function isRetryableCodexFailureEvent(rawEvent: Record<string, unknown>): boolea
|
|
|
2595
2726
|
if (code && CODEX_RETRYABLE_EVENT_CODES.has(code.toLowerCase())) {
|
|
2596
2727
|
return true;
|
|
2597
2728
|
}
|
|
2729
|
+
if (code === "invalid_request_error" || code === "previous_response_not_found") return false;
|
|
2598
2730
|
const message = getString(error?.message) ?? getString(rawEvent.message) ?? getString(response?.message);
|
|
2599
2731
|
return !!message && CODEX_RETRYABLE_EVENT_MESSAGE.test(message);
|
|
2600
2732
|
}
|
|
@@ -2603,12 +2735,19 @@ function createCodexProviderStreamError(rawEvent: Record<string, unknown>): Code
|
|
|
2603
2735
|
const response = asRecord(rawEvent.response);
|
|
2604
2736
|
const providerError = asRecord(rawEvent.error) ?? (response ? asRecord(response.error) : null);
|
|
2605
2737
|
const code = getString(providerError?.code) ?? getString(providerError?.type) ?? getString(rawEvent.code) ?? "";
|
|
2606
|
-
const message = getString(rawEvent.message) ?? "";
|
|
2738
|
+
const message = getString(providerError?.message) ?? getString(rawEvent.message) ?? "";
|
|
2739
|
+
const param = getString(providerError?.param) ?? getString(rawEvent.param);
|
|
2607
2740
|
const formattedMessage =
|
|
2608
2741
|
typeof rawEvent.type === "string" && rawEvent.type === "error"
|
|
2609
2742
|
? formatCodexErrorEvent(rawEvent, code, message)
|
|
2610
2743
|
: (formatCodexFailure(rawEvent) ?? "Codex response failed");
|
|
2611
|
-
return new CodexProviderStreamError(
|
|
2744
|
+
return new CodexProviderStreamError(
|
|
2745
|
+
formattedMessage,
|
|
2746
|
+
isRetryableCodexFailureEvent(rawEvent),
|
|
2747
|
+
code || undefined,
|
|
2748
|
+
param,
|
|
2749
|
+
message,
|
|
2750
|
+
);
|
|
2612
2751
|
}
|
|
2613
2752
|
|
|
2614
2753
|
function isRetryableCodexProviderError(error: unknown): boolean {
|