@f5-sales-demo/pi-ai 22.7.1 → 22.7.2

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.1",
4
+ "version": "22.7.2",
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.1",
49
+ "@f5-sales-demo/pi-utils": "22.7.2",
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.286";
195
+ export const claudeCodeVersion = "2.1.287";
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
 
@@ -210,6 +210,7 @@ interface CodexStreamRuntime {
210
210
  outputSlots: Map<number, { item: CodexEventItem | null; block: CodexOutputBlock | null; index: number }>;
211
211
  currentIndex: number;
212
212
  completedItemIds: Set<string>;
213
+ completedContentIndices: Set<number>;
213
214
  currentBlock: CodexOutputBlock | null;
214
215
  nativeOutputItems: Array<Record<string, unknown>>;
215
216
  websocketStreamRetries: number;
@@ -221,6 +222,7 @@ interface CodexStreamRuntime {
221
222
  }
222
223
 
223
224
  interface CodexStreamProcessingContext {
225
+ runtime?: CodexStreamRuntime;
224
226
  model: Model<"openai-codex-responses">;
225
227
  output: AssistantMessage;
226
228
  stream: AssistantMessageEventStream;
@@ -300,7 +302,13 @@ function getCodexProviderSessionState(
300
302
  }
301
303
 
302
304
  function createCodexWebSocketTransportError(message: string): Error {
303
- return new Error(`${CODEX_WEBSOCKET_TRANSPORT_ERROR_PREFIX}: ${message}`);
305
+ return Object.assign(new Error(`${CODEX_WEBSOCKET_TRANSPORT_ERROR_PREFIX}: ${message}`), {
306
+ codexTransportInterruption: message.startsWith("websocket closed")
307
+ ? "socket_closed"
308
+ : message === "idle timeout waiting for websocket" || message === "timeout waiting for first websocket event"
309
+ ? "idle_timeout"
310
+ : undefined,
311
+ });
304
312
  }
305
313
 
306
314
  function isCodexWebSocketFatalError(error: Error): boolean {
@@ -913,6 +921,7 @@ function createCodexStreamRuntime(initial: {
913
921
  outputSlots: new Map(),
914
922
  currentIndex: -1,
915
923
  completedItemIds: new Set(),
924
+ completedContentIndices: new Set(),
916
925
  nativeOutputItems: [],
917
926
  websocketStreamRetries: 0,
918
927
  providerRetryAttempt: 0,
@@ -1137,6 +1146,7 @@ function handleCodexStreamEvent(args: {
1137
1146
  if (itemId && runtime.completedItemIds.has(itemId)) return firstTokenTime;
1138
1147
  if (itemId) runtime.completedItemIds.add(itemId);
1139
1148
  handleOutputItemDone(model, output, stream, runtime, rawEvent, blockIndex);
1149
+ if (runtime.currentIndex >= 0) runtime.completedContentIndices.add(runtime.currentIndex);
1140
1150
  runtime.outputSlots.delete(
1141
1151
  typeof rawEvent.output_index === "number" ? rawEvent.output_index : runtime.currentIndex,
1142
1152
  );
@@ -1305,6 +1315,7 @@ function handleOutputItemDone(
1305
1315
  ): void {
1306
1316
  const item = structuredCloneJSON(rawEvent.item) as CodexEventItem;
1307
1317
  runtime.nativeOutputItems.push(item as unknown as Record<string, unknown>);
1318
+ output.providerPayload = createOpenAIResponsesHistoryPayload(model.provider, runtime.nativeOutputItems);
1308
1319
  if (item.type === "custom_tool_call" && runtime.currentBlock?.type === "toolCall") {
1309
1320
  const property = runtime.grammarToolInputProperties?.get(item.name) ?? "input";
1310
1321
  Object.assign(runtime.currentBlock, {
@@ -1360,7 +1371,7 @@ function handleOutputItemDone(
1360
1371
  type: "toolCall",
1361
1372
  id: `${item.call_id}|${item.id}`,
1362
1373
  name: item.name,
1363
- arguments: parseStreamingJson(item.arguments || "{}"),
1374
+ arguments: JSON.parse(item.arguments || "{}"),
1364
1375
  };
1365
1376
  if (runtime.currentBlock?.type === "toolCall") {
1366
1377
  Object.assign(runtime.currentBlock, toolCall);
@@ -1561,7 +1572,8 @@ async function tryRestartCodexSseTransport(
1561
1572
  if (error !== originalError) throw error;
1562
1573
  return false;
1563
1574
  }
1564
- if (runtime.transportRestartAttempt >= 2) throw runtime.firstTransportFailure;
1575
+ if (runtime.transportRestartAttempt >= Math.max(0, context.options?.maxRetries ?? 2))
1576
+ throw runtime.firstTransportFailure;
1565
1577
  runtime.transportRestartAttempt += 1;
1566
1578
  try {
1567
1579
  await runtime.eventStream.return(undefined);
@@ -1573,6 +1585,7 @@ async function tryRestartCodexSseTransport(
1573
1585
  runtime.currentIndex = -1;
1574
1586
  runtime.outputSlots.clear();
1575
1587
  runtime.completedItemIds.clear();
1588
+ runtime.completedContentIndices.clear();
1576
1589
  runtime.nativeOutputItems.length = 0;
1577
1590
  runtime.sawResponseAccepted = false;
1578
1591
  runtime.sawOutputEvent = false;
@@ -1591,7 +1604,10 @@ async function tryRestartCodexSseTransport(
1591
1604
  resetCodexWebSocketAppendState(state);
1592
1605
  resetCodexSessionMetadata(state);
1593
1606
  }
1594
- await sleepBeforeCodexSseRestart(500 * runtime.transportRestartAttempt, context.requestSetup.requestSignal);
1607
+ await sleepBeforeCodexSseRestart(
1608
+ 500 * (runtime.transportRestartAttempt + Math.max(0, 2 - (context.options?.maxRetries ?? 2))),
1609
+ context.requestSetup.requestSignal,
1610
+ );
1595
1611
  try {
1596
1612
  const next = await openCodexSseTransport(
1597
1613
  context.requestContext,
@@ -1640,6 +1656,14 @@ async function tryRecoverCodexContinuation(
1640
1656
  resetCodexWebSocketAppendState(state);
1641
1657
  resetCodexSessionMetadata(state);
1642
1658
  if (!eligible) return false;
1659
+ if (context.options?.maxRetries !== undefined) {
1660
+ if (runtime.transportRestartAttempt >= context.options.maxRetries) return false;
1661
+ runtime.transportRestartAttempt += 1;
1662
+ await sleepBeforeCodexSseRestart(
1663
+ 500 * (runtime.transportRestartAttempt + Math.max(0, 2 - (context.options?.maxRetries ?? 2))),
1664
+ context.requestSetup.requestSignal,
1665
+ );
1666
+ }
1643
1667
  runtime.continuationRecoveryAttempt += 1;
1644
1668
  logCodexDebug("codex continuation recovery", {
1645
1669
  reason: "previous_response_unavailable",
@@ -1681,6 +1705,14 @@ async function tryReconnectCodexWebSocketOnConnectionLimit(
1681
1705
  }
1682
1706
 
1683
1707
  // Close the stale connection so getOrCreateCodexWebSocketConnection creates a fresh one.
1708
+ if (context.options?.maxRetries !== undefined) {
1709
+ if (runtime.transportRestartAttempt >= context.options.maxRetries) return false;
1710
+ runtime.transportRestartAttempt += 1;
1711
+ await sleepBeforeCodexSseRestart(
1712
+ 500 * (runtime.transportRestartAttempt + Math.max(0, 2 - (context.options?.maxRetries ?? 2))),
1713
+ context.requestSetup.requestSignal,
1714
+ );
1715
+ }
1684
1716
  websocketState.connection?.close("connection_limit");
1685
1717
  websocketState.connection = undefined;
1686
1718
  resetCodexWebSocketAppendState(websocketState);
@@ -1699,6 +1731,7 @@ async function tryReconnectCodexWebSocketOnConnectionLimit(
1699
1731
  runtime.currentBlock = null;
1700
1732
  runtime.outputSlots.clear();
1701
1733
  runtime.completedItemIds.clear();
1734
+ runtime.completedContentIndices.clear();
1702
1735
  runtime.nativeOutputItems.length = 0;
1703
1736
  runtime.stagedEvents.length = 0;
1704
1737
  resetOutputState(context.output);
@@ -1728,6 +1761,14 @@ async function tryReplayWebsocketFailureOverSse(
1728
1761
  !runtime.sawTerminalEvent &&
1729
1762
  !context.options?.signal?.aborted;
1730
1763
  if (!canReplay) return false;
1764
+ if (context.options?.maxRetries !== undefined) {
1765
+ if (runtime.transportRestartAttempt >= context.options.maxRetries) return false;
1766
+ runtime.transportRestartAttempt += 1;
1767
+ await sleepBeforeCodexSseRestart(
1768
+ 500 * (runtime.transportRestartAttempt + Math.max(0, 2 - (context.options?.maxRetries ?? 2))),
1769
+ context.requestSetup.requestSignal,
1770
+ );
1771
+ }
1731
1772
 
1732
1773
  const state = websocketState;
1733
1774
  const streamError = error instanceof Error ? error : new Error(String(error));
@@ -1761,6 +1802,7 @@ async function tryReplayWebsocketFailureOverSse(
1761
1802
  runtime.currentBlock = null;
1762
1803
  runtime.outputSlots.clear();
1763
1804
  runtime.completedItemIds.clear();
1805
+ runtime.completedContentIndices.clear();
1764
1806
  runtime.nativeOutputItems.length = 0;
1765
1807
  runtime.stagedEvents.length = 0;
1766
1808
  resetOutputState(context.output);
@@ -1803,6 +1845,7 @@ async function tryRetryCodexProviderError(
1803
1845
  runtime.currentBlock = null;
1804
1846
  runtime.outputSlots.clear();
1805
1847
  runtime.completedItemIds.clear();
1848
+ runtime.completedContentIndices.clear();
1806
1849
  runtime.sawTerminalEvent = false;
1807
1850
  runtime.stagedEvents.length = 0;
1808
1851
  resetOutputState(context.output);
@@ -1866,6 +1909,37 @@ async function handleCodexStreamFailure(
1866
1909
  resetCodexWebSocketAppendState(context.requestContext.websocketState);
1867
1910
  resetCodexSessionMetadata(context.requestContext.websocketState);
1868
1911
  }
1912
+ const runtime = context.runtime;
1913
+ const interrupted =
1914
+ !context.requestSetup.requestSignal.aborted &&
1915
+ runtime &&
1916
+ !runtime.sawTerminalEvent &&
1917
+ !(error instanceof CodexProviderStreamError) &&
1918
+ (error instanceof CodexSseIdleTimeoutError ||
1919
+ isTransportClosureError(error) ||
1920
+ (error as { codexTransportInterruption?: string })?.codexTransportInterruption);
1921
+ if (interrupted) {
1922
+ output.interruption = {
1923
+ transport: runtime.transport,
1924
+ classification:
1925
+ error instanceof CodexSseIdleTimeoutError ||
1926
+ (error as { codexTransportInterruption?: string }).codexTransportInterruption === "idle_timeout"
1927
+ ? "idle_timeout"
1928
+ : (error as { code?: string }).code === "ERR_STREAM_PREMATURE_CLOSE"
1929
+ ? "premature_eof"
1930
+ : "socket_closed",
1931
+ providerRetriesConsumed: runtime.transportRestartAttempt,
1932
+ completedContentIndices: [...runtime.completedContentIndices].sort((a, b) => a - b),
1933
+ };
1934
+ output.providerPayload = createOpenAIResponsesHistoryPayload(context.model.provider, runtime.nativeOutputItems);
1935
+ }
1936
+ if (runtime) {
1937
+ try {
1938
+ await runtime.eventStream.return(undefined);
1939
+ } catch {
1940
+ /* Preserve the original failure. */
1941
+ }
1942
+ }
1869
1943
  output.stopReason = context.options?.signal?.aborted ? "aborted" : "error";
1870
1944
  output.errorMessage = await finalizeErrorMessage(error, context.requestContext.rawRequestDump);
1871
1945
  output.providerFailureCode = sanitizeProviderFailureCode(
@@ -1905,6 +1979,7 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
1905
1979
  }
1906
1980
 
1907
1981
  processingContext = {
1982
+ runtime,
1908
1983
  model,
1909
1984
  output,
1910
1985
  stream,
@@ -2828,7 +2903,7 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
2828
2903
 
2829
2904
  const outputItems: ResponseInput = [];
2830
2905
  for (const block of msg.content) {
2831
- if (block.type === "thinking" && msg.stopReason !== "error") {
2906
+ if (block.type === "thinking" && (msg.stopReason !== "error" || !!assistantMsg.interruption)) {
2832
2907
  if (block.thinkingSignature) {
2833
2908
  outputItems.push(JSON.parse(block.thinkingSignature) as ResponseReasoningItem);
2834
2909
  }
@@ -135,7 +135,7 @@ export function convertResponsesAssistantMessage<TApi extends Api>(
135
135
  assistantMsg.model !== model.id && assistantMsg.provider === model.provider && assistantMsg.api === model.api;
136
136
 
137
137
  for (const block of assistantMsg.content) {
138
- if (block.type === "thinking" && assistantMsg.stopReason !== "error") {
138
+ if (block.type === "thinking" && (assistantMsg.stopReason !== "error" || !!assistantMsg.interruption)) {
139
139
  if (!includeThinkingSignatures) {
140
140
  continue;
141
141
  }
@@ -69,7 +69,9 @@ export function transformMessages<TApi extends Api>(
69
69
  // Aborted/errored messages may have partially-streamed thinking signatures.
70
70
  // A partial signature is invalid and will be rejected by the API, so we must
71
71
  // strip signatures from thinking blocks in these messages.
72
- const hasInvalidSignatures = assistantMsg.stopReason === "aborted" || assistantMsg.stopReason === "error";
72
+ const hasInvalidSignatures =
73
+ assistantMsg.stopReason === "aborted" ||
74
+ (assistantMsg.stopReason === "error" && !assistantMsg.interruption);
73
75
 
74
76
  const transformedContent = assistantMsg.content.flatMap(block => {
75
77
  if (block.type === "thinking") {
@@ -205,7 +207,10 @@ export function transformMessages<TApi extends Api>(
205
207
  const assistantMsg = msg as AssistantMessage;
206
208
  const toolCalls = assistantMsg.content.filter(b => b.type === "toolCall") as ToolCall[];
207
209
 
208
- if (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") {
210
+ if (
211
+ (assistantMsg.stopReason === "error" && !assistantMsg.interruption) ||
212
+ assistantMsg.stopReason === "aborted"
213
+ ) {
209
214
  // Keep the assistant message with tool calls intact. If real tool results follow, preserve them;
210
215
  // otherwise synthesize aborted results before the next turn boundary.
211
216
  result.push(msg);
package/src/types.ts CHANGED
@@ -442,6 +442,14 @@ export interface DeveloperMessage {
442
442
  }
443
443
 
444
444
  export interface AssistantMessage {
445
+ /** Completed content retained after a typed transport interruption. */
446
+ interruption?: {
447
+ transport: "sse" | "websocket";
448
+ classification: "socket_closed" | "premature_eof" | "idle_timeout";
449
+ providerRetriesConsumed: number;
450
+ toolChoiceServed?: boolean;
451
+ completedContentIndices: number[];
452
+ };
445
453
  /** A queued background request; poll using the same provider route. */
446
454
  deferred?: DeferredHandle;
447
455
  providerRewrite?: {