@f5-sales-demo/pi-ai 22.7.0 → 22.7.1
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.7.
|
|
4
|
+
"version": "22.7.1",
|
|
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.7.
|
|
49
|
+
"@f5-sales-demo/pi-utils": "22.7.1",
|
|
50
50
|
"@google/genai": "2.21.0",
|
|
51
51
|
"@sinclair/typebox": "0.34.52",
|
|
52
52
|
"@smithy/node-http-handler": "4.12.1",
|
|
@@ -26,6 +26,7 @@ import { getEnvApiKey } from "../stream";
|
|
|
26
26
|
import {
|
|
27
27
|
type Api,
|
|
28
28
|
type AssistantMessage,
|
|
29
|
+
type AssistantMessageEvent,
|
|
29
30
|
type Context,
|
|
30
31
|
isSpecialServiceTier,
|
|
31
32
|
type Model,
|
|
@@ -47,7 +48,7 @@ import {
|
|
|
47
48
|
} from "../utils";
|
|
48
49
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
49
50
|
import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
|
|
50
|
-
import { getOpenAIStreamIdleTimeoutMs
|
|
51
|
+
import { getOpenAIStreamIdleTimeoutMs } from "../utils/idle-iterator";
|
|
51
52
|
import { parseStreamingJson } from "../utils/json-parse";
|
|
52
53
|
import { isTransportClosureError } from "../utils/retry";
|
|
53
54
|
import { adaptSchemaForStrict, NO_STRICT } from "../utils/schema";
|
|
@@ -185,7 +186,6 @@ interface CodexRequestContext {
|
|
|
185
186
|
|
|
186
187
|
interface CodexRequestSetup {
|
|
187
188
|
requestSignal: AbortSignal;
|
|
188
|
-
wrapCodexSseStream: (source: AsyncGenerator<Record<string, unknown>>) => AsyncGenerator<Record<string, unknown>>;
|
|
189
189
|
requestAbortController: AbortController;
|
|
190
190
|
}
|
|
191
191
|
|
|
@@ -195,6 +195,8 @@ interface CodexWebSocketRequestState {
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
interface CodexStreamRuntime {
|
|
198
|
+
stagedEvents: AssistantMessageEvent[];
|
|
199
|
+
firstTransportFailure?: unknown;
|
|
198
200
|
websocketRequestState?: CodexWebSocketRequestState;
|
|
199
201
|
continuationRecoveryAttempt: number;
|
|
200
202
|
sawResponseAccepted: boolean;
|
|
@@ -453,15 +455,7 @@ function createRequestSetup(options: OpenAICodexResponsesOptions | undefined): C
|
|
|
453
455
|
const requestSignal = options?.signal
|
|
454
456
|
? AbortSignal.any([options.signal, requestAbortController.signal])
|
|
455
457
|
: requestAbortController.signal;
|
|
456
|
-
|
|
457
|
-
source: AsyncGenerator<Record<string, unknown>>,
|
|
458
|
-
): AsyncGenerator<Record<string, unknown>> =>
|
|
459
|
-
iterateWithIdleTimeout(source, {
|
|
460
|
-
idleTimeoutMs: getOpenAIStreamIdleTimeoutMs(),
|
|
461
|
-
errorMessage: "OpenAI Codex SSE stream stalled while waiting for the next event",
|
|
462
|
-
onIdle: () => requestAbortController.abort(),
|
|
463
|
-
});
|
|
464
|
-
return { requestAbortController, requestSignal, wrapCodexSseStream };
|
|
458
|
+
return { requestAbortController, requestSignal };
|
|
465
459
|
}
|
|
466
460
|
|
|
467
461
|
async function buildCodexRequestContext(
|
|
@@ -766,6 +760,60 @@ async function openCodexWebSocketTransport(
|
|
|
766
760
|
return { eventStream, requestBodyForState, transport: "websocket", websocketRequestState };
|
|
767
761
|
}
|
|
768
762
|
|
|
763
|
+
class CodexSseIdleTimeoutError extends Error {
|
|
764
|
+
constructor() {
|
|
765
|
+
super("OpenAI Codex SSE stream stalled while waiting for the next event");
|
|
766
|
+
this.name = "CodexSseIdleTimeoutError";
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
async function* iterateCodexSseAttempt(
|
|
771
|
+
source: AsyncGenerator<Record<string, unknown>>,
|
|
772
|
+
attempt: AbortController,
|
|
773
|
+
cleanup: () => void,
|
|
774
|
+
): AsyncGenerator<Record<string, unknown>> {
|
|
775
|
+
const timeoutMs = getOpenAIStreamIdleTimeoutMs();
|
|
776
|
+
try {
|
|
777
|
+
while (true) {
|
|
778
|
+
attempt.signal.throwIfAborted();
|
|
779
|
+
let timer: NodeJS.Timeout | undefined;
|
|
780
|
+
try {
|
|
781
|
+
const next = source.next();
|
|
782
|
+
const result =
|
|
783
|
+
timeoutMs === undefined
|
|
784
|
+
? await next
|
|
785
|
+
: await Promise.race([
|
|
786
|
+
next,
|
|
787
|
+
new Promise<never>((_resolve, reject) => {
|
|
788
|
+
timer = setTimeout(() => {
|
|
789
|
+
const error = new CodexSseIdleTimeoutError();
|
|
790
|
+
reject(error);
|
|
791
|
+
attempt.abort(error);
|
|
792
|
+
}, timeoutMs);
|
|
793
|
+
}),
|
|
794
|
+
]);
|
|
795
|
+
if (timer !== undefined) {
|
|
796
|
+
clearTimeout(timer);
|
|
797
|
+
timer = undefined;
|
|
798
|
+
}
|
|
799
|
+
attempt.signal.throwIfAborted();
|
|
800
|
+
if (result.done) return;
|
|
801
|
+
yield result.value;
|
|
802
|
+
} finally {
|
|
803
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
} finally {
|
|
807
|
+
attempt.abort();
|
|
808
|
+
cleanup();
|
|
809
|
+
try {
|
|
810
|
+
await source.return(undefined);
|
|
811
|
+
} catch {
|
|
812
|
+
/* Preserve the original stream failure. */
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
769
817
|
async function openCodexSseTransport(
|
|
770
818
|
requestContext: CodexRequestContext,
|
|
771
819
|
requestSetup: CodexRequestSetup,
|
|
@@ -779,8 +827,16 @@ async function openCodexSseTransport(
|
|
|
779
827
|
transport: CodexTransport;
|
|
780
828
|
websocketRequestState?: CodexWebSocketRequestState;
|
|
781
829
|
}> {
|
|
782
|
-
const
|
|
783
|
-
|
|
830
|
+
const attempt = new AbortController();
|
|
831
|
+
const signal = requestSetup.requestSignal;
|
|
832
|
+
const onAbort = () => attempt.abort(signal.reason);
|
|
833
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
834
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
835
|
+
if (signal.aborted) onAbort();
|
|
836
|
+
let source: AsyncGenerator<Record<string, unknown>>;
|
|
837
|
+
try {
|
|
838
|
+
attempt.signal.throwIfAborted();
|
|
839
|
+
source = await openCodexSseEventStream(
|
|
784
840
|
requestContext.url,
|
|
785
841
|
requestContext.requestHeaders,
|
|
786
842
|
requestContext.accountId,
|
|
@@ -788,10 +844,16 @@ async function openCodexSseTransport(
|
|
|
788
844
|
options?.sessionId,
|
|
789
845
|
body,
|
|
790
846
|
state,
|
|
791
|
-
|
|
847
|
+
attempt.signal,
|
|
792
848
|
retryFetch,
|
|
793
|
-
)
|
|
794
|
-
)
|
|
849
|
+
);
|
|
850
|
+
} catch (error) {
|
|
851
|
+
attempt.abort();
|
|
852
|
+
cleanup();
|
|
853
|
+
throw error;
|
|
854
|
+
}
|
|
855
|
+
const eventStream = iterateCodexSseAttempt(source, attempt, cleanup);
|
|
856
|
+
|
|
795
857
|
return { eventStream, requestBodyForState: structuredCloneJSON(body), transport: "sse" };
|
|
796
858
|
}
|
|
797
859
|
|
|
@@ -837,6 +899,7 @@ function createCodexStreamRuntime(initial: {
|
|
|
837
899
|
websocketRequestState?: CodexWebSocketRequestState;
|
|
838
900
|
}): CodexStreamRuntime {
|
|
839
901
|
return {
|
|
902
|
+
stagedEvents: [],
|
|
840
903
|
websocketRequestState: initial.websocketRequestState,
|
|
841
904
|
continuationRecoveryAttempt: 0,
|
|
842
905
|
sawResponseAccepted: false,
|
|
@@ -860,6 +923,30 @@ function createCodexStreamRuntime(initial: {
|
|
|
860
923
|
};
|
|
861
924
|
}
|
|
862
925
|
|
|
926
|
+
// Empty normalized blocks belong to the attempt until replay becomes unsafe.
|
|
927
|
+
class CodexAttemptEventStream extends AssistantMessageEventStream {
|
|
928
|
+
constructor(
|
|
929
|
+
private readonly target: AssistantMessageEventStream,
|
|
930
|
+
private readonly runtime: CodexStreamRuntime,
|
|
931
|
+
) {
|
|
932
|
+
super();
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
override push(event: AssistantMessageEvent): void {
|
|
936
|
+
if (this.runtime.sawReplayUnsafeActivity || this.runtime.sawTerminalEvent) {
|
|
937
|
+
this.flush();
|
|
938
|
+
this.target.push(event);
|
|
939
|
+
} else {
|
|
940
|
+
this.runtime.stagedEvents.push(structuredCloneJSON(event));
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
flush(): void {
|
|
945
|
+
for (const event of this.runtime.stagedEvents) this.target.push(event);
|
|
946
|
+
this.runtime.stagedEvents.length = 0;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
863
950
|
async function processCodexResponseStream(
|
|
864
951
|
context: CodexStreamProcessingContext,
|
|
865
952
|
runtime: CodexStreamRuntime,
|
|
@@ -867,6 +954,7 @@ async function processCodexResponseStream(
|
|
|
867
954
|
const { output, stream } = context;
|
|
868
955
|
runtime.grammarToolInputProperties = context.requestContext.grammarToolInputProperties;
|
|
869
956
|
stream.push({ type: "start", partial: output });
|
|
957
|
+
const attemptStream = new CodexAttemptEventStream(stream, runtime);
|
|
870
958
|
|
|
871
959
|
while (true) {
|
|
872
960
|
try {
|
|
@@ -875,10 +963,12 @@ async function processCodexResponseStream(
|
|
|
875
963
|
await context.options?.onProviderStreamEvent?.(rawEvent, context.model);
|
|
876
964
|
firstTokenTime = handleCodexStreamEvent({
|
|
877
965
|
...context,
|
|
966
|
+
stream: attemptStream,
|
|
878
967
|
runtime,
|
|
879
968
|
rawEvent,
|
|
880
969
|
firstTokenTime,
|
|
881
970
|
});
|
|
971
|
+
if (runtime.sawReplayUnsafeActivity || runtime.sawTerminalEvent) attemptStream.flush();
|
|
882
972
|
}
|
|
883
973
|
if (!runtime.sawTerminalEvent) {
|
|
884
974
|
throw Object.assign(new Error("Codex stream ended before terminal completion event"), {
|
|
@@ -1409,7 +1499,7 @@ async function recoverCodexStreamError(
|
|
|
1409
1499
|
runtime: CodexStreamRuntime,
|
|
1410
1500
|
error: unknown,
|
|
1411
1501
|
): Promise<boolean> {
|
|
1412
|
-
if (runtime.transport === "sse" && isTransportClosureError(error)) {
|
|
1502
|
+
if (runtime.transport === "sse" && (error instanceof CodexSseIdleTimeoutError || isTransportClosureError(error))) {
|
|
1413
1503
|
return tryRestartCodexSseTransport(context, runtime, error);
|
|
1414
1504
|
}
|
|
1415
1505
|
if (isCodexRejectedContinuation(error)) {
|
|
@@ -1427,11 +1517,27 @@ async function recoverCodexStreamError(
|
|
|
1427
1517
|
return false;
|
|
1428
1518
|
}
|
|
1429
1519
|
|
|
1520
|
+
async function sleepBeforeCodexSseRestart(delayMs: number, signal: AbortSignal): Promise<void> {
|
|
1521
|
+
signal.throwIfAborted();
|
|
1522
|
+
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
1523
|
+
const timer = setTimeout(resolve, delayMs);
|
|
1524
|
+
const onAbort = () => reject(signal.reason);
|
|
1525
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1526
|
+
try {
|
|
1527
|
+
await promise;
|
|
1528
|
+
signal.throwIfAborted();
|
|
1529
|
+
} finally {
|
|
1530
|
+
clearTimeout(timer);
|
|
1531
|
+
signal.removeEventListener("abort", onAbort);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1430
1535
|
async function tryRestartCodexSseTransport(
|
|
1431
1536
|
context: CodexStreamProcessingContext,
|
|
1432
1537
|
runtime: CodexStreamRuntime,
|
|
1433
1538
|
originalError: unknown,
|
|
1434
1539
|
): Promise<boolean> {
|
|
1540
|
+
runtime.firstTransportFailure ??= originalError;
|
|
1435
1541
|
let error = originalError;
|
|
1436
1542
|
while (true) {
|
|
1437
1543
|
const replayEligible =
|
|
@@ -1451,13 +1557,17 @@ async function tryRestartCodexSseTransport(
|
|
|
1451
1557
|
terminalEventSeen: runtime.sawTerminalEvent,
|
|
1452
1558
|
replayEligible,
|
|
1453
1559
|
});
|
|
1454
|
-
if (!replayEligible || !isTransportClosureError(error)) {
|
|
1560
|
+
if (!replayEligible || !(error instanceof CodexSseIdleTimeoutError || isTransportClosureError(error))) {
|
|
1455
1561
|
if (error !== originalError) throw error;
|
|
1456
1562
|
return false;
|
|
1457
1563
|
}
|
|
1458
|
-
if (runtime.transportRestartAttempt >= 2) throw
|
|
1564
|
+
if (runtime.transportRestartAttempt >= 2) throw runtime.firstTransportFailure;
|
|
1459
1565
|
runtime.transportRestartAttempt += 1;
|
|
1460
|
-
|
|
1566
|
+
try {
|
|
1567
|
+
await runtime.eventStream.return(undefined);
|
|
1568
|
+
} catch {
|
|
1569
|
+
/* Preserve the recovery failure. */
|
|
1570
|
+
}
|
|
1461
1571
|
runtime.currentItem = null;
|
|
1462
1572
|
runtime.currentBlock = null;
|
|
1463
1573
|
runtime.currentIndex = -1;
|
|
@@ -1467,6 +1577,7 @@ async function tryRestartCodexSseTransport(
|
|
|
1467
1577
|
runtime.sawResponseAccepted = false;
|
|
1468
1578
|
runtime.sawOutputEvent = false;
|
|
1469
1579
|
runtime.sawTerminalEvent = false;
|
|
1580
|
+
runtime.stagedEvents.length = 0;
|
|
1470
1581
|
resetOutputState(context.output);
|
|
1471
1582
|
delete context.output.responseId;
|
|
1472
1583
|
delete context.output.providerPayload;
|
|
@@ -1480,7 +1591,7 @@ async function tryRestartCodexSseTransport(
|
|
|
1480
1591
|
resetCodexWebSocketAppendState(state);
|
|
1481
1592
|
resetCodexSessionMetadata(state);
|
|
1482
1593
|
}
|
|
1483
|
-
await
|
|
1594
|
+
await sleepBeforeCodexSseRestart(500 * runtime.transportRestartAttempt, context.requestSetup.requestSignal);
|
|
1484
1595
|
try {
|
|
1485
1596
|
const next = await openCodexSseTransport(
|
|
1486
1597
|
context.requestContext,
|
|
@@ -1589,6 +1700,7 @@ async function tryReconnectCodexWebSocketOnConnectionLimit(
|
|
|
1589
1700
|
runtime.outputSlots.clear();
|
|
1590
1701
|
runtime.completedItemIds.clear();
|
|
1591
1702
|
runtime.nativeOutputItems.length = 0;
|
|
1703
|
+
runtime.stagedEvents.length = 0;
|
|
1592
1704
|
resetOutputState(context.output);
|
|
1593
1705
|
context.firstTokenTime = undefined;
|
|
1594
1706
|
recordCodexWebSocketFailure(websocketState, true);
|
|
@@ -1650,6 +1762,7 @@ async function tryReplayWebsocketFailureOverSse(
|
|
|
1650
1762
|
runtime.outputSlots.clear();
|
|
1651
1763
|
runtime.completedItemIds.clear();
|
|
1652
1764
|
runtime.nativeOutputItems.length = 0;
|
|
1765
|
+
runtime.stagedEvents.length = 0;
|
|
1653
1766
|
resetOutputState(context.output);
|
|
1654
1767
|
context.firstTokenTime = undefined;
|
|
1655
1768
|
}
|
|
@@ -1691,6 +1804,7 @@ async function tryRetryCodexProviderError(
|
|
|
1691
1804
|
runtime.outputSlots.clear();
|
|
1692
1805
|
runtime.completedItemIds.clear();
|
|
1693
1806
|
runtime.sawTerminalEvent = false;
|
|
1807
|
+
runtime.stagedEvents.length = 0;
|
|
1694
1808
|
resetOutputState(context.output);
|
|
1695
1809
|
context.firstTokenTime = undefined;
|
|
1696
1810
|
await abortableSleep(CODEX_RETRY_DELAY_MS * runtime.providerRetryAttempt, context.requestSetup.requestSignal);
|
|
@@ -2465,7 +2579,39 @@ async function openCodexSseEventStream(
|
|
|
2465
2579
|
if (!response.body) {
|
|
2466
2580
|
throw new Error("No response body");
|
|
2467
2581
|
}
|
|
2468
|
-
|
|
2582
|
+
signal?.throwIfAborted();
|
|
2583
|
+
const reader = response.body.getReader();
|
|
2584
|
+
let closed = false;
|
|
2585
|
+
const close = () => {
|
|
2586
|
+
if (closed) return;
|
|
2587
|
+
closed = true;
|
|
2588
|
+
// Cancel settles pending reads synchronously; do not await an upstream cancel hook.
|
|
2589
|
+
void reader.cancel().catch(() => {});
|
|
2590
|
+
reader.releaseLock();
|
|
2591
|
+
};
|
|
2592
|
+
const onAbort = () => close();
|
|
2593
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2594
|
+
const owned = new ReadableStream<Uint8Array>({
|
|
2595
|
+
async pull(controller) {
|
|
2596
|
+
try {
|
|
2597
|
+
const result = await reader.read();
|
|
2598
|
+
if (result.done) controller.close();
|
|
2599
|
+
else controller.enqueue(result.value);
|
|
2600
|
+
} catch (error) {
|
|
2601
|
+
controller.error(error);
|
|
2602
|
+
}
|
|
2603
|
+
},
|
|
2604
|
+
cancel: close,
|
|
2605
|
+
});
|
|
2606
|
+
async function* events(): AsyncGenerator<Record<string, unknown>> {
|
|
2607
|
+
try {
|
|
2608
|
+
yield* readSseJson<Record<string, unknown>>(owned);
|
|
2609
|
+
} finally {
|
|
2610
|
+
close();
|
|
2611
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
return events();
|
|
2469
2615
|
}
|
|
2470
2616
|
|
|
2471
2617
|
function createCodexHeaders(
|
|
@@ -2554,6 +2700,7 @@ async function fetchWithRetry(url: string, init: RequestInit, signal?: AbortSign
|
|
|
2554
2700
|
let rateLimitTimeSpent = 0;
|
|
2555
2701
|
while (true) {
|
|
2556
2702
|
try {
|
|
2703
|
+
signal?.throwIfAborted();
|
|
2557
2704
|
const response = await fetch(url, { ...init, signal: signal ?? init.signal });
|
|
2558
2705
|
if (!retry || !CODEX_RETRYABLE_STATUS.has(response.status)) {
|
|
2559
2706
|
return response;
|