@earendil-works/pi-ai 0.75.5 → 0.76.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.
Files changed (37) hide show
  1. package/dist/models.generated.d.ts +41 -58
  2. package/dist/models.generated.d.ts.map +1 -1
  3. package/dist/models.generated.js +115 -132
  4. package/dist/models.generated.js.map +1 -1
  5. package/dist/providers/anthropic.d.ts.map +1 -1
  6. package/dist/providers/anthropic.js +1 -1
  7. package/dist/providers/anthropic.js.map +1 -1
  8. package/dist/providers/azure-openai-responses.d.ts.map +1 -1
  9. package/dist/providers/azure-openai-responses.js +1 -1
  10. package/dist/providers/azure-openai-responses.js.map +1 -1
  11. package/dist/providers/images/openrouter.d.ts.map +1 -1
  12. package/dist/providers/images/openrouter.js +1 -1
  13. package/dist/providers/images/openrouter.js.map +1 -1
  14. package/dist/providers/openai-codex-responses.d.ts.map +1 -1
  15. package/dist/providers/openai-codex-responses.js +148 -76
  16. package/dist/providers/openai-codex-responses.js.map +1 -1
  17. package/dist/providers/openai-completions.d.ts.map +1 -1
  18. package/dist/providers/openai-completions.js +1 -1
  19. package/dist/providers/openai-completions.js.map +1 -1
  20. package/dist/providers/openai-responses.d.ts.map +1 -1
  21. package/dist/providers/openai-responses.js +1 -1
  22. package/dist/providers/openai-responses.js.map +1 -1
  23. package/dist/providers/simple-options.d.ts.map +1 -1
  24. package/dist/providers/simple-options.js +1 -0
  25. package/dist/providers/simple-options.js.map +1 -1
  26. package/dist/types.d.ts +6 -0
  27. package/dist/types.d.ts.map +1 -1
  28. package/dist/types.js.map +1 -1
  29. package/dist/utils/abort-signals.d.ts +6 -0
  30. package/dist/utils/abort-signals.d.ts.map +1 -0
  31. package/dist/utils/abort-signals.js +34 -0
  32. package/dist/utils/abort-signals.js.map +1 -0
  33. package/dist/utils/overflow.d.ts +2 -1
  34. package/dist/utils/overflow.d.ts.map +1 -1
  35. package/dist/utils/overflow.js +5 -2
  36. package/dist/utils/overflow.js.map +1 -1
  37. package/package.json +1 -1
@@ -18,6 +18,7 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version
18
18
  import { getEnvApiKey } from "../env-api-keys.js";
19
19
  import { clampThinkingLevel } from "../models.js";
20
20
  import { registerSessionResourceCleanup } from "../session-resources.js";
21
+ import { combineAbortSignals } from "../utils/abort-signals.js";
21
22
  import { appendAssistantMessageDiagnostic, createAssistantMessageDiagnostic, formatThrownValue, } from "../utils/diagnostics.js";
22
23
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
23
24
  import { headersToRecord } from "../utils/headers.js";
@@ -29,8 +30,11 @@ import { buildBaseOptions } from "./simple-options.js";
29
30
  // ============================================================================
30
31
  const DEFAULT_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
31
32
  const JWT_CLAIM_PATH = "https://api.openai.com/auth";
32
- const MAX_RETRIES = 3;
33
+ const DEFAULT_MAX_RETRIES = 0;
33
34
  const BASE_DELAY_MS = 1000;
35
+ const DEFAULT_MAX_RETRY_DELAY_MS = 60_000;
36
+ const DEFAULT_SSE_HEADER_TIMEOUT_MS = 10_000;
37
+ const DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS = 15_000;
34
38
  const CODEX_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
35
39
  const WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE = 1009;
36
40
  const CODEX_RESPONSE_STATUSES = new Set([
@@ -44,12 +48,44 @@ const CODEX_RESPONSE_STATUSES = new Set([
44
48
  // ============================================================================
45
49
  // Retry Helpers
46
50
  // ============================================================================
51
+ function isTerminalRateLimitError(errorText) {
52
+ return /GoUsageLimitError|FreeUsageLimitError|Monthly usage limit reached|available balance|insufficient_quota|out of budget|quota exceeded|billing/i.test(errorText);
53
+ }
47
54
  function isRetryableError(status, errorText) {
55
+ if (status === 429 && isTerminalRateLimitError(errorText)) {
56
+ return false;
57
+ }
48
58
  if (status === 429 || status === 500 || status === 502 || status === 503 || status === 504) {
49
59
  return true;
50
60
  }
51
61
  return /rate.?limit|overloaded|service.?unavailable|upstream.?connect|connection.?refused/i.test(errorText);
52
62
  }
63
+ function getRetryAfterDelayMs(headers) {
64
+ const retryAfterMs = headers.get("retry-after-ms");
65
+ if (retryAfterMs !== null) {
66
+ const millis = Number(retryAfterMs);
67
+ if (Number.isFinite(millis)) {
68
+ return Math.max(0, millis);
69
+ }
70
+ }
71
+ const retryAfter = headers.get("retry-after");
72
+ if (!retryAfter) {
73
+ return undefined;
74
+ }
75
+ const seconds = Number(retryAfter);
76
+ if (Number.isFinite(seconds)) {
77
+ return Math.max(0, seconds * 1000);
78
+ }
79
+ const date = Date.parse(retryAfter);
80
+ if (!Number.isNaN(date)) {
81
+ return Math.max(0, date - Date.now());
82
+ }
83
+ return undefined;
84
+ }
85
+ function capRetryDelayMs(delayMs, options) {
86
+ const maxRetryDelayMs = options?.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
87
+ return maxRetryDelayMs > 0 ? Math.min(delayMs, maxRetryDelayMs) : delayMs;
88
+ }
53
89
  function sleep(ms, signal) {
54
90
  return new Promise((resolve, reject) => {
55
91
  if (signal?.aborted) {
@@ -63,6 +99,27 @@ function sleep(ms, signal) {
63
99
  });
64
100
  });
65
101
  }
102
+ function normalizeTimeoutMs(value) {
103
+ if (value === undefined)
104
+ return undefined;
105
+ if (!Number.isFinite(value) || value < 0) {
106
+ throw new Error(`Invalid timeoutMs: ${String(value)}`);
107
+ }
108
+ return Math.floor(value);
109
+ }
110
+ function createSSEHeaderTimeout() {
111
+ const controller = new AbortController();
112
+ let error;
113
+ const timeout = setTimeout(() => {
114
+ error = new Error(`Codex SSE response headers timed out after ${DEFAULT_SSE_HEADER_TIMEOUT_MS}ms`);
115
+ controller.abort(error);
116
+ }, DEFAULT_SSE_HEADER_TIMEOUT_MS);
117
+ return {
118
+ signal: controller.signal,
119
+ clear: () => clearTimeout(timeout),
120
+ error: () => error,
121
+ };
122
+ }
66
123
  // ============================================================================
67
124
  // Main Stream Function
68
125
  // ============================================================================
@@ -101,6 +158,8 @@ export const streamOpenAICodexResponses = (model, context, options) => {
101
158
  const sseHeaders = buildSSEHeaders(model.headers, options?.headers, accountId, apiKey, options?.sessionId);
102
159
  const websocketHeaders = buildWebSocketHeaders(model.headers, options?.headers, accountId, apiKey, websocketRequestId);
103
160
  const bodyJson = JSON.stringify(body);
161
+ const idleTimeoutMs = normalizeTimeoutMs(options?.timeoutMs);
162
+ const websocketConnectTimeoutMs = normalizeTimeoutMs(options?.websocketConnectTimeoutMs);
104
163
  const transport = options?.transport || "auto";
105
164
  const websocketDisabledForSession = transport !== "sse" && isWebSocketSseFallbackActive(options?.sessionId);
106
165
  if (websocketDisabledForSession) {
@@ -111,7 +170,7 @@ export const streamOpenAICodexResponses = (model, context, options) => {
111
170
  try {
112
171
  await processWebSocketStream(resolveCodexWebSocketUrl(model.baseUrl), body, websocketHeaders, output, stream, model, () => {
113
172
  websocketStarted = true;
114
- }, options);
173
+ }, idleTimeoutMs, websocketConnectTimeoutMs, options);
115
174
  if (options?.signal?.aborted) {
116
175
  throw new Error("Request was aborted");
117
176
  }
@@ -145,46 +204,42 @@ export const streamOpenAICodexResponses = (model, context, options) => {
145
204
  // Fetch with retry logic for rate limits and transient errors
146
205
  let response;
147
206
  let lastError;
148
- for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
207
+ const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
208
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
149
209
  if (options?.signal?.aborted) {
150
210
  throw new Error("Request was aborted");
151
211
  }
152
212
  try {
153
- response = await fetch(resolveCodexUrl(model.baseUrl), {
154
- method: "POST",
155
- headers: sseHeaders,
156
- body: bodyJson,
157
- signal: options?.signal,
158
- });
213
+ const headerTimeout = createSSEHeaderTimeout();
214
+ const combinedSignal = combineAbortSignals([options?.signal, headerTimeout.signal]);
215
+ try {
216
+ response = await fetch(resolveCodexUrl(model.baseUrl), {
217
+ method: "POST",
218
+ headers: sseHeaders,
219
+ body: bodyJson,
220
+ signal: combinedSignal.signal,
221
+ });
222
+ }
223
+ catch (error) {
224
+ const timeoutError = headerTimeout.error();
225
+ throw timeoutError && !options?.signal?.aborted ? timeoutError : error;
226
+ }
227
+ finally {
228
+ combinedSignal.cleanup();
229
+ headerTimeout.clear();
230
+ }
159
231
  await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
160
232
  if (response.ok) {
161
233
  break;
162
234
  }
163
235
  const errorText = await response.text();
164
- if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) {
165
- let delayMs = BASE_DELAY_MS * 2 ** attempt;
166
- const retryAfterMs = response.headers.get("retry-after-ms");
167
- if (retryAfterMs !== null) {
168
- const millis = Number(retryAfterMs);
169
- if (Number.isFinite(millis)) {
170
- delayMs = Math.max(0, millis);
171
- }
172
- }
173
- else {
174
- const retryAfter = response.headers.get("retry-after");
175
- if (retryAfter) {
176
- const seconds = Number(retryAfter);
177
- if (Number.isFinite(seconds)) {
178
- delayMs = Math.max(0, seconds * 1000);
179
- }
180
- else {
181
- const date = Date.parse(retryAfter);
182
- if (!Number.isNaN(date)) {
183
- delayMs = Math.max(0, date - Date.now());
184
- }
185
- }
186
- }
187
- }
236
+ if (attempt < maxRetries && isRetryableError(response.status, errorText)) {
237
+ const retryAfterDelayMs = getRetryAfterDelayMs(response.headers);
238
+ const delayMs = retryAfterDelayMs === undefined
239
+ ? BASE_DELAY_MS * 2 ** attempt
240
+ : response.status === 429
241
+ ? capRetryDelayMs(retryAfterDelayMs, options)
242
+ : retryAfterDelayMs;
188
243
  await sleep(delayMs, options?.signal);
189
244
  continue;
190
245
  }
@@ -204,7 +259,7 @@ export const streamOpenAICodexResponses = (model, context, options) => {
204
259
  }
205
260
  lastError = error instanceof Error ? error : new Error(String(error));
206
261
  // Network errors are retryable
207
- if (attempt < MAX_RETRIES && !lastError.message.includes("usage limit")) {
262
+ if (attempt < maxRetries && !lastError.message.includes("usage limit")) {
208
263
  const delayMs = BASE_DELAY_MS * 2 ** attempt;
209
264
  await sleep(delayMs, options?.signal);
210
265
  continue;
@@ -602,7 +657,7 @@ function scheduleSessionWebSocketExpiry(sessionId, entry) {
602
657
  websocketSessionCache.delete(sessionId);
603
658
  }, SESSION_WEBSOCKET_CACHE_TTL_MS);
604
659
  }
605
- async function connectWebSocket(url, headers, signal) {
660
+ async function connectWebSocket(url, headers, signal, connectTimeoutMs = DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS) {
606
661
  const WebSocketCtor = await getWebSocketConstructor();
607
662
  if (!WebSocketCtor) {
608
663
  throw new Error("WebSocket transport is not available in this runtime");
@@ -611,6 +666,7 @@ async function connectWebSocket(url, headers, signal) {
611
666
  delete wsHeaders["OpenAI-Beta"];
612
667
  return new Promise((resolve, reject) => {
613
668
  let settled = false;
669
+ let timeout;
614
670
  let socket;
615
671
  try {
616
672
  socket = new WebSocketCtor(url, { headers: wsHeaders });
@@ -619,62 +675,63 @@ async function connectWebSocket(url, headers, signal) {
619
675
  reject(error instanceof Error ? error : new Error(String(error)));
620
676
  return;
621
677
  }
622
- const onOpen = () => {
623
- if (settled)
624
- return;
625
- settled = true;
626
- cleanup();
627
- resolve(socket);
678
+ const cleanup = () => {
679
+ if (timeout) {
680
+ clearTimeout(timeout);
681
+ timeout = undefined;
682
+ }
683
+ socket.removeEventListener("open", onOpen);
684
+ socket.removeEventListener("error", onError);
685
+ socket.removeEventListener("close", onClose);
686
+ signal?.removeEventListener("abort", onAbort);
628
687
  };
629
- const onError = (event) => {
630
- const error = extractWebSocketError(event);
688
+ const fail = (error, closeReason) => {
631
689
  if (settled)
632
690
  return;
633
691
  settled = true;
634
692
  cleanup();
693
+ if (closeReason) {
694
+ closeWebSocketSilently(socket, 1000, closeReason);
695
+ }
635
696
  reject(error);
636
697
  };
637
- const onClose = (event) => {
638
- const error = extractWebSocketCloseError(event);
698
+ const onOpen = () => {
639
699
  if (settled)
640
700
  return;
641
701
  settled = true;
642
702
  cleanup();
643
- reject(error);
703
+ resolve(socket);
644
704
  };
645
- const onAbort = () => {
646
- if (settled)
647
- return;
648
- settled = true;
649
- cleanup();
650
- socket.close(1000, "aborted");
651
- reject(new Error("Request was aborted"));
705
+ const onError = (event) => {
706
+ fail(extractWebSocketError(event));
652
707
  };
653
- const cleanup = () => {
654
- socket.removeEventListener("open", onOpen);
655
- socket.removeEventListener("error", onError);
656
- socket.removeEventListener("close", onClose);
657
- signal?.removeEventListener("abort", onAbort);
708
+ const onClose = (event) => {
709
+ fail(extractWebSocketCloseError(event));
710
+ };
711
+ const onAbort = () => {
712
+ fail(new Error("Request was aborted"), "aborted");
658
713
  };
659
714
  socket.addEventListener("open", onOpen);
660
715
  socket.addEventListener("error", onError);
661
716
  socket.addEventListener("close", onClose);
662
717
  signal?.addEventListener("abort", onAbort);
718
+ if (connectTimeoutMs > 0) {
719
+ timeout = setTimeout(() => {
720
+ fail(new Error(`WebSocket connect timeout after ${connectTimeoutMs}ms`), "connect_timeout");
721
+ }, connectTimeoutMs);
722
+ }
723
+ if (signal?.aborted) {
724
+ onAbort();
725
+ }
663
726
  });
664
727
  }
665
- async function acquireWebSocket(url, headers, sessionId, signal) {
728
+ async function acquireWebSocket(url, headers, sessionId, signal, connectTimeoutMs) {
666
729
  if (!sessionId) {
667
- const socket = await connectWebSocket(url, headers, signal);
730
+ const socket = await connectWebSocket(url, headers, signal, connectTimeoutMs);
668
731
  return {
669
732
  socket,
670
733
  reused: false,
671
- release: ({ keep } = {}) => {
672
- if (keep === false) {
673
- closeWebSocketSilently(socket);
674
- return;
675
- }
676
- closeWebSocketSilently(socket);
677
- },
734
+ release: () => closeWebSocketSilently(socket),
678
735
  };
679
736
  }
680
737
  const cached = websocketSessionCache.get(sessionId);
@@ -701,7 +758,7 @@ async function acquireWebSocket(url, headers, sessionId, signal) {
701
758
  };
702
759
  }
703
760
  if (cached.busy) {
704
- const socket = await connectWebSocket(url, headers, signal);
761
+ const socket = await connectWebSocket(url, headers, signal, connectTimeoutMs);
705
762
  return {
706
763
  socket,
707
764
  reused: false,
@@ -715,7 +772,7 @@ async function acquireWebSocket(url, headers, sessionId, signal) {
715
772
  websocketSessionCache.delete(sessionId);
716
773
  }
717
774
  }
718
- const socket = await connectWebSocket(url, headers, signal);
775
+ const socket = await connectWebSocket(url, headers, signal, connectTimeoutMs);
719
776
  const entry = { socket, busy: true };
720
777
  websocketSessionCache.set(sessionId, entry);
721
778
  return {
@@ -791,7 +848,7 @@ async function decodeWebSocketData(data) {
791
848
  }
792
849
  return null;
793
850
  }
794
- async function* parseWebSocket(socket, signal) {
851
+ async function* parseWebSocket(socket, signal, idleTimeoutMs) {
795
852
  const queue = [];
796
853
  let pending = null;
797
854
  let done = false;
@@ -869,8 +926,23 @@ async function* parseWebSocket(socket, signal) {
869
926
  }
870
927
  if (done)
871
928
  break;
872
- await new Promise((resolve) => {
929
+ let timeout;
930
+ await new Promise((resolve, reject) => {
873
931
  pending = resolve;
932
+ if (idleTimeoutMs !== undefined && idleTimeoutMs > 0) {
933
+ timeout = setTimeout(() => {
934
+ const error = new Error(`WebSocket idle timeout after ${idleTimeoutMs}ms`);
935
+ failed = error;
936
+ done = true;
937
+ pending = null;
938
+ closeWebSocketSilently(socket, 1000, "idle_timeout");
939
+ reject(error);
940
+ }, idleTimeoutMs);
941
+ }
942
+ }).finally(() => {
943
+ if (timeout) {
944
+ clearTimeout(timeout);
945
+ }
874
946
  });
875
947
  }
876
948
  if (failed) {
@@ -939,8 +1011,8 @@ async function* startWebSocketOutputOnFirstEvent(events, output, stream, onStart
939
1011
  yield event;
940
1012
  }
941
1013
  }
942
- async function processWebSocketStream(url, body, headers, output, stream, model, onStart, options) {
943
- const { socket, entry, reused, release } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal);
1014
+ async function processWebSocketStream(url, body, headers, output, stream, model, onStart, idleTimeoutMs, websocketConnectTimeoutMs, options) {
1015
+ const { socket, entry, reused, release } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal, websocketConnectTimeoutMs);
944
1016
  let keepConnection = true;
945
1017
  const useCachedContext = options?.transport === "websocket-cached" || options?.transport === "auto";
946
1018
  // ChatGPT Codex Responses rejects `store: true` ("Store must be set to false").
@@ -972,7 +1044,7 @@ async function processWebSocketStream(url, body, headers, output, stream, model,
972
1044
  }
973
1045
  try {
974
1046
  socket.send(JSON.stringify({ type: "response.create", ...requestBody }));
975
- await processResponsesStream(startWebSocketOutputOnFirstEvent(mapCodexEvents(parseWebSocket(socket, options?.signal)), output, stream, onStart), output, stream, model, {
1047
+ await processResponsesStream(startWebSocketOutputOnFirstEvent(mapCodexEvents(parseWebSocket(socket, options?.signal, idleTimeoutMs)), output, stream, onStart), output, stream, model, {
976
1048
  serviceTier: options?.serviceTier,
977
1049
  resolveServiceTier: resolveCodexServiceTier,
978
1050
  applyServiceTierPricing: (usage, serviceTier) => applyServiceTierPricing(usage, serviceTier, model),
@@ -1070,7 +1142,7 @@ function buildSSEHeaders(initHeaders, additionalHeaders, accountId, token, sessi
1070
1142
  headers.set("accept", "text/event-stream");
1071
1143
  headers.set("content-type", "application/json");
1072
1144
  if (sessionId) {
1073
- headers.set("session_id", sessionId);
1145
+ headers.set("session-id", sessionId);
1074
1146
  headers.set("x-client-request-id", sessionId);
1075
1147
  }
1076
1148
  return headers;
@@ -1083,7 +1155,7 @@ function buildWebSocketHeaders(initHeaders, additionalHeaders, accountId, token,
1083
1155
  headers.delete("openai-beta");
1084
1156
  headers.set("OpenAI-Beta", OPENAI_BETA_RESPONSES_WEBSOCKETS);
1085
1157
  headers.set("x-client-request-id", requestId);
1086
- headers.set("session_id", requestId);
1158
+ headers.set("session-id", requestId);
1087
1159
  return headers;
1088
1160
  }
1089
1161
  //# sourceMappingURL=openai-codex-responses.js.map