@oh-my-pi/pi-ai 3.34.0 → 3.36.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-ai",
3
- "version": "3.34.0",
3
+ "version": "3.36.0",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -24,6 +24,7 @@ import type {
24
24
  } from "../types";
25
25
  import { AssistantMessageEventStream } from "../utils/event-stream";
26
26
  import { parseStreamingJson } from "../utils/json-parse";
27
+ import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
27
28
  import { sanitizeSurrogates } from "../utils/sanitize-unicode";
28
29
 
29
30
  import { transformMessages } from "./transorm-messages";
@@ -279,7 +280,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
279
280
  } catch (error) {
280
281
  for (const block of output.content) delete (block as any).index;
281
282
  output.stopReason = options?.signal?.aborted ? "aborted" : "error";
282
- output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
283
+ output.errorMessage = formatErrorMessageWithRetryAfter(error);
283
284
  stream.push({ type: "error", reason: output.stopReason, error: output });
284
285
  stream.end();
285
286
  }
@@ -18,6 +18,7 @@ import type {
18
18
  ToolCall,
19
19
  } from "../types";
20
20
  import { AssistantMessageEventStream } from "../utils/event-stream";
21
+ import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
21
22
  import { sanitizeSurrogates } from "../utils/sanitize-unicode";
22
23
  import {
23
24
  convertMessages,
@@ -638,7 +639,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
638
639
  }
639
640
  }
640
641
  output.stopReason = options?.signal?.aborted ? "aborted" : "error";
641
- output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
642
+ output.errorMessage = formatErrorMessageWithRetryAfter(error);
642
643
  stream.push({ type: "error", reason: output.stopReason, error: output });
643
644
  stream.end();
644
645
  }
@@ -18,6 +18,7 @@ import type {
18
18
  ToolCall,
19
19
  } from "../types";
20
20
  import { AssistantMessageEventStream } from "../utils/event-stream";
21
+ import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
21
22
  import { sanitizeSurrogates } from "../utils/sanitize-unicode";
22
23
  import type { GoogleThinkingLevel } from "./google-gemini-cli";
23
24
  import {
@@ -262,7 +263,7 @@ export const streamGoogleVertex: StreamFunction<"google-vertex"> = (
262
263
  }
263
264
  }
264
265
  output.stopReason = options?.signal?.aborted ? "aborted" : "error";
265
- output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
266
+ output.errorMessage = formatErrorMessageWithRetryAfter(error);
266
267
  stream.push({ type: "error", reason: output.stopReason, error: output });
267
268
  stream.end();
268
269
  }
@@ -18,6 +18,7 @@ import type {
18
18
  ToolCall,
19
19
  } from "../types";
20
20
  import { AssistantMessageEventStream } from "../utils/event-stream";
21
+ import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
21
22
  import { sanitizeSurrogates } from "../utils/sanitize-unicode";
22
23
  import type { GoogleThinkingLevel } from "./google-gemini-cli";
23
24
  import {
@@ -250,7 +251,7 @@ export const streamGoogle: StreamFunction<"google-generative-ai"> = (
250
251
  }
251
252
  }
252
253
  output.stopReason = options?.signal?.aborted ? "aborted" : "error";
253
- output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
254
+ output.errorMessage = formatErrorMessageWithRetryAfter(error);
254
255
  stream.push({ type: "error", reason: output.stopReason, error: output });
255
256
  stream.end();
256
257
  }
@@ -24,6 +24,7 @@ import type {
24
24
  } from "../types";
25
25
  import { AssistantMessageEventStream } from "../utils/event-stream";
26
26
  import { parseStreamingJson } from "../utils/json-parse";
27
+ import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
27
28
  import { sanitizeSurrogates } from "../utils/sanitize-unicode";
28
29
  import {
29
30
  CODEX_BASE_URL,
@@ -151,7 +152,9 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
151
152
 
152
153
  if (!response.ok) {
153
154
  const info = await parseCodexError(response);
154
- throw new Error(info.friendlyMessage || info.message);
155
+ const error = new Error(info.friendlyMessage || info.message);
156
+ (error as { headers?: Headers }).headers = response.headers;
157
+ throw error;
155
158
  }
156
159
 
157
160
  if (!response.body) {
@@ -362,7 +365,7 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
362
365
  } catch (error) {
363
366
  for (const block of output.content) delete (block as { index?: number }).index;
364
367
  output.stopReason = options?.signal?.aborted ? "aborted" : "error";
365
- output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
368
+ output.errorMessage = formatErrorMessageWithRetryAfter(error);
366
369
  stream.push({ type: "error", reason: output.stopReason, error: output });
367
370
  stream.end();
368
371
  }
@@ -26,6 +26,7 @@ import type {
26
26
  } from "../types";
27
27
  import { AssistantMessageEventStream } from "../utils/event-stream";
28
28
  import { parseStreamingJson } from "../utils/json-parse";
29
+ import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
29
30
  import { sanitizeSurrogates } from "../utils/sanitize-unicode";
30
31
  import { transformMessages } from "./transorm-messages";
31
32
 
@@ -306,7 +307,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
306
307
  } catch (error) {
307
308
  for (const block of output.content) delete (block as any).index;
308
309
  output.stopReason = options?.signal?.aborted ? "aborted" : "error";
309
- output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
310
+ output.errorMessage = formatErrorMessageWithRetryAfter(error);
310
311
  stream.push({ type: "error", reason: output.stopReason, error: output });
311
312
  stream.end();
312
313
  }
@@ -27,6 +27,7 @@ import type {
27
27
  } from "../types";
28
28
  import { AssistantMessageEventStream } from "../utils/event-stream";
29
29
  import { parseStreamingJson } from "../utils/json-parse";
30
+ import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
30
31
  import { sanitizeSurrogates } from "../utils/sanitize-unicode";
31
32
  import { transformMessages } from "./transorm-messages";
32
33
 
@@ -303,7 +304,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
303
304
  } catch (error) {
304
305
  for (const block of output.content) delete (block as any).index;
305
306
  output.stopReason = options?.signal?.aborted ? "aborted" : "error";
306
- output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
307
+ output.errorMessage = formatErrorMessageWithRetryAfter(error);
307
308
  stream.push({ type: "error", reason: output.stopReason, error: output });
308
309
  stream.end();
309
310
  }
@@ -0,0 +1,110 @@
1
+ export type HeadersLike = Headers | Record<string, string | undefined> | undefined | null;
2
+
3
+ const RETRY_AFTER_HINT = "retry-after-ms=";
4
+
5
+ export function formatErrorMessageWithRetryAfter(error: unknown, headers?: HeadersLike): string {
6
+ const message = error instanceof Error ? error.message : JSON.stringify(error);
7
+ if (message.includes(RETRY_AFTER_HINT)) {
8
+ return message;
9
+ }
10
+
11
+ const retryAfterMs = getRetryAfterMsFromHeaders(headers ?? getHeadersFromError(error));
12
+ if (retryAfterMs === undefined) {
13
+ return message;
14
+ }
15
+
16
+ return `${message} ${RETRY_AFTER_HINT}${retryAfterMs}`;
17
+ }
18
+
19
+ export function getRetryAfterMsFromHeaders(headers: HeadersLike): number | undefined {
20
+ if (!headers) return undefined;
21
+
22
+ const retryAfter = parseRetryAfterHeader(getHeaderValue(headers, "retry-after"));
23
+ const resetMs = parseResetHeader(getHeaderValue(headers, "x-ratelimit-reset-ms"), "ms");
24
+ const resetSeconds = parseResetHeader(getHeaderValue(headers, "x-ratelimit-reset"), "s");
25
+
26
+ const candidates = [retryAfter, resetMs, resetSeconds].filter((value): value is number => value !== undefined);
27
+ if (candidates.length === 0) return undefined;
28
+ return Math.max(...candidates);
29
+ }
30
+
31
+ function getHeadersFromError(error: unknown): HeadersLike {
32
+ if (!error || typeof error !== "object") return undefined;
33
+ const record = error as { headers?: unknown; response?: { headers?: unknown }; cause?: unknown };
34
+ const direct = extractHeaders(record.headers) ?? extractHeaders(record.response?.headers);
35
+ if (direct) return direct;
36
+ if (record.cause) return getHeadersFromError(record.cause);
37
+ return undefined;
38
+ }
39
+
40
+ function extractHeaders(value: unknown): HeadersLike {
41
+ if (!value) return undefined;
42
+ if (value instanceof Headers) return value;
43
+ if (typeof value === "object") return value as Record<string, string | undefined>;
44
+ return undefined;
45
+ }
46
+
47
+ function getHeaderValue(headers: Headers | Record<string, string | undefined>, name: string): string | undefined {
48
+ if (headers instanceof Headers) {
49
+ const value = headers.get(name);
50
+ return value ?? undefined;
51
+ }
52
+
53
+ const target = name.toLowerCase();
54
+ for (const [key, value] of Object.entries(headers)) {
55
+ if (key.toLowerCase() === target && typeof value === "string") {
56
+ return value;
57
+ }
58
+ }
59
+ return undefined;
60
+ }
61
+
62
+ function parseRetryAfterHeader(value: string | undefined): number | undefined {
63
+ if (!value) return undefined;
64
+ const trimmed = value.trim();
65
+ if (!trimmed) return undefined;
66
+
67
+ const numeric = Number(trimmed);
68
+ if (Number.isFinite(numeric)) {
69
+ if (numeric <= 0) return undefined;
70
+ return Math.ceil(numeric * 1000);
71
+ }
72
+
73
+ const dateMs = Date.parse(trimmed);
74
+ if (!Number.isNaN(dateMs)) {
75
+ const delay = dateMs - Date.now();
76
+ return delay > 0 ? Math.ceil(delay) : undefined;
77
+ }
78
+
79
+ return undefined;
80
+ }
81
+
82
+ function parseResetHeader(value: string | undefined, unit: "ms" | "s"): number | undefined {
83
+ if (!value) return undefined;
84
+ const numeric = Number(value);
85
+ if (!Number.isFinite(numeric) || numeric <= 0) return undefined;
86
+
87
+ const nowMs = Date.now();
88
+ let targetMs: number | undefined;
89
+
90
+ if (unit === "ms") {
91
+ if (numeric > 1e12) {
92
+ targetMs = numeric;
93
+ } else if (numeric > 1e9) {
94
+ targetMs = numeric * 1000;
95
+ } else {
96
+ return Math.ceil(numeric);
97
+ }
98
+ } else {
99
+ if (numeric > 1e12) {
100
+ targetMs = numeric;
101
+ } else if (numeric > 1e9) {
102
+ targetMs = numeric * 1000;
103
+ } else {
104
+ return Math.ceil(numeric * 1000);
105
+ }
106
+ }
107
+
108
+ if (targetMs <= nowMs) return undefined;
109
+ return Math.ceil(targetMs - nowMs);
110
+ }