@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.5.2",
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.5.2",
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.285";
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);
@@ -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 {