@f5-sales-demo/pi-ai 22.5.2 → 22.7.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.7.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.7.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
|
|
|
@@ -49,6 +49,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
|
49
49
|
import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
|
|
50
50
|
import { getOpenAIStreamIdleTimeoutMs, iterateWithIdleTimeout } from "../utils/idle-iterator";
|
|
51
51
|
import { parseStreamingJson } from "../utils/json-parse";
|
|
52
|
+
import { isTransportClosureError } from "../utils/retry";
|
|
52
53
|
import { adaptSchemaForStrict, NO_STRICT } from "../utils/schema";
|
|
53
54
|
import {
|
|
54
55
|
CODEX_BASE_URL,
|
|
@@ -211,6 +212,8 @@ interface CodexStreamRuntime {
|
|
|
211
212
|
nativeOutputItems: Array<Record<string, unknown>>;
|
|
212
213
|
websocketStreamRetries: number;
|
|
213
214
|
providerRetryAttempt: number;
|
|
215
|
+
transportRestartAttempt: number;
|
|
216
|
+
sawReplayUnsafeActivity: boolean;
|
|
214
217
|
sawTerminalEvent: boolean;
|
|
215
218
|
canSafelyReplayWebsocketOverSse: boolean;
|
|
216
219
|
}
|
|
@@ -769,6 +772,7 @@ async function openCodexSseTransport(
|
|
|
769
772
|
options: OpenAICodexResponsesOptions | undefined,
|
|
770
773
|
state: CodexWebSocketSessionState | undefined,
|
|
771
774
|
body = requestContext.transformedBody,
|
|
775
|
+
retryFetch = true,
|
|
772
776
|
): Promise<{
|
|
773
777
|
eventStream: AsyncGenerator<Record<string, unknown>>;
|
|
774
778
|
requestBodyForState: RequestBody;
|
|
@@ -785,6 +789,7 @@ async function openCodexSseTransport(
|
|
|
785
789
|
body,
|
|
786
790
|
state,
|
|
787
791
|
requestSetup.requestSignal,
|
|
792
|
+
retryFetch,
|
|
788
793
|
),
|
|
789
794
|
);
|
|
790
795
|
return { eventStream, requestBodyForState: structuredCloneJSON(body), transport: "sse" };
|
|
@@ -848,6 +853,8 @@ function createCodexStreamRuntime(initial: {
|
|
|
848
853
|
nativeOutputItems: [],
|
|
849
854
|
websocketStreamRetries: 0,
|
|
850
855
|
providerRetryAttempt: 0,
|
|
856
|
+
transportRestartAttempt: 0,
|
|
857
|
+
sawReplayUnsafeActivity: false,
|
|
851
858
|
sawTerminalEvent: false,
|
|
852
859
|
canSafelyReplayWebsocketOverSse: true,
|
|
853
860
|
};
|
|
@@ -873,6 +880,11 @@ async function processCodexResponseStream(
|
|
|
873
880
|
firstTokenTime,
|
|
874
881
|
});
|
|
875
882
|
}
|
|
883
|
+
if (!runtime.sawTerminalEvent) {
|
|
884
|
+
throw Object.assign(new Error("Codex stream ended before terminal completion event"), {
|
|
885
|
+
code: "ERR_STREAM_PREMATURE_CLOSE",
|
|
886
|
+
});
|
|
887
|
+
}
|
|
876
888
|
return { firstTokenTime };
|
|
877
889
|
} catch (error) {
|
|
878
890
|
const recovered = await recoverCodexStreamError(context, runtime, error);
|
|
@@ -894,6 +906,27 @@ function handleCodexStreamEvent(args: {
|
|
|
894
906
|
const { model, output, stream, runtime, rawEvent } = args;
|
|
895
907
|
const eventType = typeof rawEvent.type === "string" ? rawEvent.type : "";
|
|
896
908
|
if (!eventType) return args.firstTokenTime;
|
|
909
|
+
// Tool activity is unsafe from its first event, even if no arguments or tool result exists yet.
|
|
910
|
+
const item = asRecord(rawEvent.item);
|
|
911
|
+
const part = asRecord(rawEvent.part);
|
|
912
|
+
const hasText = (value: unknown): boolean => typeof value === "string" && value.length > 0;
|
|
913
|
+
if (
|
|
914
|
+
/^response\.(?:function_call|custom_tool_call|.*_call\.)/.test(eventType) ||
|
|
915
|
+
(item && typeof item.type === "string" && item.type !== "message" && item.type !== "reasoning") ||
|
|
916
|
+
hasText(rawEvent.delta) ||
|
|
917
|
+
hasText(rawEvent.text) ||
|
|
918
|
+
hasText(part?.text) ||
|
|
919
|
+
hasText(part?.refusal) ||
|
|
920
|
+
(item &&
|
|
921
|
+
[item.content, item.summary].some(
|
|
922
|
+
parts =>
|
|
923
|
+
Array.isArray(parts) &&
|
|
924
|
+
parts.some(value => hasText(asRecord(value)?.text) || hasText(asRecord(value)?.refusal)),
|
|
925
|
+
))
|
|
926
|
+
) {
|
|
927
|
+
runtime.sawReplayUnsafeActivity = true;
|
|
928
|
+
}
|
|
929
|
+
|
|
897
930
|
if (
|
|
898
931
|
eventType === "response.created" ||
|
|
899
932
|
eventType === "response.in_progress" ||
|
|
@@ -1376,6 +1409,9 @@ async function recoverCodexStreamError(
|
|
|
1376
1409
|
runtime: CodexStreamRuntime,
|
|
1377
1410
|
error: unknown,
|
|
1378
1411
|
): Promise<boolean> {
|
|
1412
|
+
if (runtime.transport === "sse" && isTransportClosureError(error)) {
|
|
1413
|
+
return tryRestartCodexSseTransport(context, runtime, error);
|
|
1414
|
+
}
|
|
1379
1415
|
if (isCodexRejectedContinuation(error)) {
|
|
1380
1416
|
return tryRecoverCodexContinuation(context, runtime);
|
|
1381
1417
|
}
|
|
@@ -1391,6 +1427,80 @@ async function recoverCodexStreamError(
|
|
|
1391
1427
|
return false;
|
|
1392
1428
|
}
|
|
1393
1429
|
|
|
1430
|
+
async function tryRestartCodexSseTransport(
|
|
1431
|
+
context: CodexStreamProcessingContext,
|
|
1432
|
+
runtime: CodexStreamRuntime,
|
|
1433
|
+
originalError: unknown,
|
|
1434
|
+
): Promise<boolean> {
|
|
1435
|
+
let error = originalError;
|
|
1436
|
+
while (true) {
|
|
1437
|
+
const replayEligible =
|
|
1438
|
+
!runtime.sawReplayUnsafeActivity &&
|
|
1439
|
+
!runtime.sawTerminalEvent &&
|
|
1440
|
+
!context.requestSetup.requestSignal.aborted &&
|
|
1441
|
+
context.output.content.every(
|
|
1442
|
+
block =>
|
|
1443
|
+
(block.type === "text" && block.text.length === 0) ||
|
|
1444
|
+
(block.type === "thinking" && block.thinking.length === 0),
|
|
1445
|
+
);
|
|
1446
|
+
logCodexDebug("codex SSE transport recovery", {
|
|
1447
|
+
transport: runtime.transport,
|
|
1448
|
+
model: context.model.id,
|
|
1449
|
+
elapsedMs: Date.now() - context.startTime,
|
|
1450
|
+
attempt: runtime.transportRestartAttempt,
|
|
1451
|
+
terminalEventSeen: runtime.sawTerminalEvent,
|
|
1452
|
+
replayEligible,
|
|
1453
|
+
});
|
|
1454
|
+
if (!replayEligible || !isTransportClosureError(error)) {
|
|
1455
|
+
if (error !== originalError) throw error;
|
|
1456
|
+
return false;
|
|
1457
|
+
}
|
|
1458
|
+
if (runtime.transportRestartAttempt >= 2) throw originalError;
|
|
1459
|
+
runtime.transportRestartAttempt += 1;
|
|
1460
|
+
await runtime.eventStream.return(undefined);
|
|
1461
|
+
runtime.currentItem = null;
|
|
1462
|
+
runtime.currentBlock = null;
|
|
1463
|
+
runtime.currentIndex = -1;
|
|
1464
|
+
runtime.outputSlots.clear();
|
|
1465
|
+
runtime.completedItemIds.clear();
|
|
1466
|
+
runtime.nativeOutputItems.length = 0;
|
|
1467
|
+
runtime.sawResponseAccepted = false;
|
|
1468
|
+
runtime.sawOutputEvent = false;
|
|
1469
|
+
runtime.sawTerminalEvent = false;
|
|
1470
|
+
resetOutputState(context.output);
|
|
1471
|
+
delete context.output.responseId;
|
|
1472
|
+
delete context.output.providerPayload;
|
|
1473
|
+
delete context.output.duration;
|
|
1474
|
+
delete context.output.ttft;
|
|
1475
|
+
delete context.output.errorMessage;
|
|
1476
|
+
context.output.responseAttribution = { requestedModel: context.model.id };
|
|
1477
|
+
context.firstTokenTime = undefined;
|
|
1478
|
+
const state = context.requestContext.websocketState;
|
|
1479
|
+
if (state) {
|
|
1480
|
+
resetCodexWebSocketAppendState(state);
|
|
1481
|
+
resetCodexSessionMetadata(state);
|
|
1482
|
+
}
|
|
1483
|
+
await abortableSleep(500 * runtime.transportRestartAttempt, context.requestSetup.requestSignal);
|
|
1484
|
+
try {
|
|
1485
|
+
const next = await openCodexSseTransport(
|
|
1486
|
+
context.requestContext,
|
|
1487
|
+
context.requestSetup,
|
|
1488
|
+
context.options,
|
|
1489
|
+
state,
|
|
1490
|
+
context.requestContext.transformedBody,
|
|
1491
|
+
false,
|
|
1492
|
+
);
|
|
1493
|
+
runtime.eventStream = next.eventStream;
|
|
1494
|
+
runtime.requestBodyForState = next.requestBodyForState;
|
|
1495
|
+
runtime.websocketRequestState = undefined;
|
|
1496
|
+
return true;
|
|
1497
|
+
} catch (reopenError) {
|
|
1498
|
+
if (context.requestSetup.requestSignal.aborted) throw reopenError;
|
|
1499
|
+
error = reopenError;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1394
1504
|
function isCodexRejectedContinuation(error: unknown): boolean {
|
|
1395
1505
|
return (
|
|
1396
1506
|
error instanceof CodexProviderStreamError &&
|
|
@@ -2314,6 +2424,7 @@ async function openCodexSseEventStream(
|
|
|
2314
2424
|
body: RequestBody,
|
|
2315
2425
|
state: CodexWebSocketSessionState | undefined,
|
|
2316
2426
|
signal?: AbortSignal,
|
|
2427
|
+
retryFetch = true,
|
|
2317
2428
|
): Promise<AsyncGenerator<Record<string, unknown>>> {
|
|
2318
2429
|
const headers = createCodexHeaders(requestHeaders, accountId, apiKey, sessionId, "sse", state);
|
|
2319
2430
|
logCodexDebug("codex request", {
|
|
@@ -2332,6 +2443,7 @@ async function openCodexSseEventStream(
|
|
|
2332
2443
|
body: JSON.stringify(body),
|
|
2333
2444
|
},
|
|
2334
2445
|
signal,
|
|
2446
|
+
retryFetch,
|
|
2335
2447
|
);
|
|
2336
2448
|
logCodexDebug("codex response", {
|
|
2337
2449
|
url: response.url,
|
|
@@ -2437,13 +2549,13 @@ function getRetryDelayMs(
|
|
|
2437
2549
|
return { delay: CODEX_RETRY_DELAY_MS * (attempt + 1), serverProvided: false };
|
|
2438
2550
|
}
|
|
2439
2551
|
|
|
2440
|
-
async function fetchWithRetry(url: string, init: RequestInit, signal?: AbortSignal): Promise<Response> {
|
|
2552
|
+
async function fetchWithRetry(url: string, init: RequestInit, signal?: AbortSignal, retry = true): Promise<Response> {
|
|
2441
2553
|
let attempt = 0;
|
|
2442
2554
|
let rateLimitTimeSpent = 0;
|
|
2443
2555
|
while (true) {
|
|
2444
2556
|
try {
|
|
2445
2557
|
const response = await fetch(url, { ...init, signal: signal ?? init.signal });
|
|
2446
|
-
if (!CODEX_RETRYABLE_STATUS.has(response.status)) {
|
|
2558
|
+
if (!retry || !CODEX_RETRYABLE_STATUS.has(response.status)) {
|
|
2447
2559
|
return response;
|
|
2448
2560
|
}
|
|
2449
2561
|
if (signal?.aborted) return response;
|
|
@@ -2465,7 +2577,7 @@ async function fetchWithRetry(url: string, init: RequestInit, signal?: AbortSign
|
|
|
2465
2577
|
}
|
|
2466
2578
|
await abortableSleep(delay, signal);
|
|
2467
2579
|
} catch (error) {
|
|
2468
|
-
if (attempt >= CODEX_MAX_RETRIES || signal?.aborted) {
|
|
2580
|
+
if (!retry || attempt >= CODEX_MAX_RETRIES || signal?.aborted) {
|
|
2469
2581
|
throw error;
|
|
2470
2582
|
}
|
|
2471
2583
|
const delay = CODEX_RETRY_DELAY_MS * (attempt + 1);
|
package/src/utils/retry.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
type ErrorLike = {
|
|
2
2
|
message?: string;
|
|
3
3
|
name?: string;
|
|
4
|
+
code?: string;
|
|
4
5
|
status?: number;
|
|
5
6
|
statusCode?: number;
|
|
6
7
|
response?: { status?: number };
|
|
@@ -13,6 +14,31 @@ const TRANSIENT_MESSAGE_PATTERN =
|
|
|
13
14
|
const VALIDATION_MESSAGE_PATTERN =
|
|
14
15
|
/invalid|validation|bad request|unsupported|schema|missing required|not found|unauthorized|forbidden/i;
|
|
15
16
|
|
|
17
|
+
const BUN_SOCKET_CLOSE_MESSAGE =
|
|
18
|
+
"The socket connection was closed unexpectedly. For more information, pass `verbose: true` in the second argument to fetch()";
|
|
19
|
+
|
|
20
|
+
/** Recognize transport closures without interpreting arbitrary provider failure text as replay-safe. */
|
|
21
|
+
export function isTransportClosureError(error: unknown, depth = 0): boolean {
|
|
22
|
+
if (!error || typeof error !== "object" || depth > 2) return false;
|
|
23
|
+
const info = error as ErrorLike;
|
|
24
|
+
const message = info.message ?? "";
|
|
25
|
+
const status = extractHttpStatusFromError(error);
|
|
26
|
+
if (
|
|
27
|
+
info.name === "AbortError" ||
|
|
28
|
+
/aborted|cancelled|canceled/i.test(message) ||
|
|
29
|
+
VALIDATION_MESSAGE_PATTERN.test(message) ||
|
|
30
|
+
(status !== undefined && status >= 400 && status < 500)
|
|
31
|
+
)
|
|
32
|
+
return false;
|
|
33
|
+
return (
|
|
34
|
+
message === BUN_SOCKET_CLOSE_MESSAGE ||
|
|
35
|
+
info.code === "ECONNRESET" ||
|
|
36
|
+
info.code === "ERR_STREAM_PREMATURE_CLOSE" ||
|
|
37
|
+
/\b(?:ECONNRESET|ERR_STREAM_PREMATURE_CLOSE)\b/.test(message) ||
|
|
38
|
+
isTransportClosureError(info.cause, depth + 1)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
16
42
|
/**
|
|
17
43
|
* Identify errors that should be retried (timeouts, 5xx, 408, 429, transient network failures).
|
|
18
44
|
*/
|
|
@@ -31,7 +57,7 @@ export function isRetryableError(error: unknown): boolean {
|
|
|
31
57
|
|
|
32
58
|
if (VALIDATION_MESSAGE_PATTERN.test(message)) return false;
|
|
33
59
|
|
|
34
|
-
return TRANSIENT_MESSAGE_PATTERN.test(message);
|
|
60
|
+
return isTransportClosureError(error) || TRANSIENT_MESSAGE_PATTERN.test(message);
|
|
35
61
|
}
|
|
36
62
|
|
|
37
63
|
export function extractHttpStatusFromError(error: unknown): number | undefined {
|