ag-common 0.0.905 → 0.0.907

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.
@@ -1,12 +1,18 @@
1
1
  export type ModelPreference = "quality" | "fast" | undefined;
2
+ /** Provenance of a successful response, with generation time in Unix milliseconds. */
3
+ export type GenerationMetadata = {
4
+ model: string;
5
+ generatedAt: number;
6
+ };
2
7
  export declare const isTextGenerationModel: (name: string) => boolean;
3
- export declare const geminiPromptImage: ({ prompt, urls, ident, prefer, }: {
8
+ export declare const geminiPromptImage: ({ prompt, urls, ident, prefer, onGenerated, }: {
4
9
  prompt: string;
5
10
  urls?: string[];
6
11
  ident: string | undefined;
7
12
  prefer?: ModelPreference;
13
+ onGenerated?: (metadata: GenerationMetadata) => void;
8
14
  }) => Promise<string>;
9
- export declare const geminiPromptDirect: ({ prompt, images, ident, prefer, groundedSearch, }: {
15
+ export declare const geminiPromptDirect: ({ prompt, images, ident, prefer, groundedSearch, onGenerated, }: {
10
16
  prompt: string;
11
17
  images?: {
12
18
  arraybuffer: ArrayBuffer;
@@ -15,5 +21,6 @@ export declare const geminiPromptDirect: ({ prompt, images, ident, prefer, groun
15
21
  ident: string | undefined;
16
22
  prefer?: ModelPreference;
17
23
  groundedSearch?: boolean;
24
+ onGenerated?: (metadata: GenerationMetadata) => void;
18
25
  }) => Promise<string>;
19
26
  export declare const resolveGroundedUrl: (url: string) => Promise<string>;
@@ -121,7 +121,7 @@ const getAvailableGeminiCombinations = async (prefer) => {
121
121
  }
122
122
  return combinations;
123
123
  };
124
- const geminiPromptImage = async ({ prompt, urls, ident, prefer, }) => {
124
+ const geminiPromptImage = async ({ prompt, urls, ident, prefer, onGenerated, }) => {
125
125
  let images = [];
126
126
  if (urls && urls.length > 0) {
127
127
  images = await (0, async_1.asyncMap)(urls, (i) => (0, fetch_1.fetchToMemory)(i));
@@ -134,11 +134,12 @@ const geminiPromptImage = async ({ prompt, urls, ident, prefer, }) => {
134
134
  images: images.filter(array_1.notEmpty),
135
135
  ident,
136
136
  prefer,
137
+ onGenerated,
137
138
  });
138
139
  return r;
139
140
  };
140
141
  exports.geminiPromptImage = geminiPromptImage;
141
- const geminiPromptDirect = async ({ prompt, images = [], ident, prefer, groundedSearch = false, }) => {
142
+ const geminiPromptDirect = async ({ prompt, images = [], ident, prefer, groundedSearch = false, onGenerated, }) => {
142
143
  const parts = images.map((i) => ({
143
144
  inlineData: {
144
145
  data: Buffer.from(i.arraybuffer).toString("base64"),
@@ -171,6 +172,7 @@ const geminiPromptDirect = async ({ prompt, images = [], ident, prefer, grounded
171
172
  (0, log_1.debug)("gem prompt:" + prompt, ident);
172
173
  (0, log_1.debug)("gem response:" + rawtext);
173
174
  (0, log_1.debug)("gem query usage:" + JSON.stringify(response.usageMetadata));
175
+ onGenerated?.({ model: response.modelVersion || selectedModel, generatedAt: Date.now() });
174
176
  return rawtext;
175
177
  }
176
178
  catch (e) {
@@ -1,4 +1,6 @@
1
+ export declare const isRetryableError: (error: unknown) => boolean;
1
2
  export declare const withRetry: <T>(operation: () => Promise<T>, operationName: string, opt?: {
2
3
  /** default 3. null for infinite */
3
4
  maxRetries?: number | null;
5
+ sleep?: (delay: number) => Promise<void>;
4
6
  }) => Promise<T>;
@@ -1,36 +1,120 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.withRetry = void 0;
3
+ exports.withRetry = exports.isRetryableError = void 0;
4
+ const const_1 = require("../const");
4
5
  const log_1 = require("./log");
5
6
  const sleep_1 = require("./sleep");
7
+ const RETRYABLE_ERROR_NAMES = new Set([
8
+ "BandwidthLimitExceeded",
9
+ "EC2ThrottledException",
10
+ "LimitExceededException",
11
+ "PriorRequestNotComplete",
12
+ "ProvisionedThroughputExceededException",
13
+ "RequestLimitExceeded",
14
+ "RequestThrottled",
15
+ "RequestThrottledException",
16
+ "SlowDown",
17
+ "ThrottledException",
18
+ "Throttling",
19
+ "ThrottlingException",
20
+ "TimeoutError",
21
+ "TooManyRequestsException",
22
+ "TransactionInProgressException",
23
+ ].map((name) => name.toLowerCase()));
24
+ const RETRYABLE_NETWORK_CODES = new Set([
25
+ "EAI_AGAIN",
26
+ "ECONNREFUSED",
27
+ "ECONNRESET",
28
+ "EHOSTUNREACH",
29
+ "ENETDOWN",
30
+ "ENETUNREACH",
31
+ "EPIPE",
32
+ "ETIMEDOUT",
33
+ "UND_ERR_CONNECT_TIMEOUT",
34
+ "UND_ERR_HEADERS_TIMEOUT",
35
+ ].map((code) => code.toLowerCase()));
36
+ const RETRYABLE_MESSAGE_PATTERNS = [
37
+ /\bthrottl(?:e|ed|ing|ingexception)\b/i,
38
+ /\bprovisioned\s*throughput\s*exceeded\b/i,
39
+ /\bthroughput\s*exceeds\b/i,
40
+ /\brate[\s-]*limit(?:ed|ing)?\b/i,
41
+ /\btoo\s*many\s*requests\b/i,
42
+ /\btoo\s*large\b/i,
43
+ ];
44
+ const HTTP_STATUS_MESSAGE_PATTERN = new RegExp(`\\b(?:http(?:\\s+error)?|status(?:\\s*code)?)[^0-9]{0,16}(?:${const_1.retryHttpCodes.join("|")})\\b`, "i");
45
+ const isRecord = (value) => typeof value === "object" && value !== null;
46
+ const isRetryableStatus = (value) => {
47
+ const status = typeof value === "string" && /^\d+$/.test(value) ? Number(value) : value;
48
+ return typeof status === "number" && const_1.retryHttpCodes.includes(status);
49
+ };
50
+ const getErrorText = (error, record) => {
51
+ if (typeof error === "string")
52
+ return error;
53
+ const message = typeof record?.message === "string" ? record.message : "";
54
+ const name = typeof record?.name === "string" ? record.name : "";
55
+ return `${name} ${message}`.trim();
56
+ };
57
+ const isRetryableErrorInternal = (error, seen) => {
58
+ if (seen.has(error))
59
+ return false;
60
+ seen.add(error);
61
+ if (isRetryableStatus(error))
62
+ return true;
63
+ const record = isRecord(error) ? error : undefined;
64
+ const retryable = isRecord(record?.$retryable) ? record.$retryable : undefined;
65
+ if (retryable?.throttling === true)
66
+ return true;
67
+ const metadata = isRecord(record?.$metadata) ? record.$metadata : undefined;
68
+ const response = isRecord(record?.response) ? record.response : undefined;
69
+ if ([
70
+ record?.code,
71
+ record?.status,
72
+ record?.statusCode,
73
+ metadata?.httpStatusCode,
74
+ response?.status,
75
+ response?.statusCode,
76
+ ].some(isRetryableStatus)) {
77
+ return true;
78
+ }
79
+ const errorName = typeof record?.name === "string" ? record.name.toLowerCase() : "";
80
+ const errorCode = typeof record?.code === "string" ? record.code.toLowerCase() : "";
81
+ if (RETRYABLE_ERROR_NAMES.has(errorName) ||
82
+ RETRYABLE_ERROR_NAMES.has(errorCode) ||
83
+ RETRYABLE_NETWORK_CODES.has(errorCode)) {
84
+ return true;
85
+ }
86
+ const errorText = getErrorText(error, record);
87
+ if (/^429$/i.test(errorText.trim()) ||
88
+ HTTP_STATUS_MESSAGE_PATTERN.test(errorText) ||
89
+ RETRYABLE_MESSAGE_PATTERNS.some((pattern) => pattern.test(errorText))) {
90
+ return true;
91
+ }
92
+ return record?.cause !== undefined ? isRetryableErrorInternal(record.cause, seen) : false;
93
+ };
94
+ const isRetryableError = (error) => isRetryableErrorInternal(error, new Set());
95
+ exports.isRetryableError = isRetryableError;
6
96
  const withRetry = async (operation, operationName, opt) => {
7
97
  let retryCount = 0;
8
98
  const baseDelay = 2000;
9
- const { maxRetries = 3 } = opt ?? {};
99
+ const { maxRetries = 3, sleep: sleepFor = sleep_1.sleep } = opt ?? {};
10
100
  for (;;) {
11
101
  try {
12
102
  // oxlint-disable-next-line no-await-in-loop -- retries are intentionally sequential
13
- return operation();
103
+ const result = await operation();
104
+ return result;
14
105
  }
15
- catch (e) {
16
- const error = e;
17
- const errorString = error.toString().toLowerCase().replace(/\s+/gim, "");
18
- if (errorString.includes("429") ||
19
- errorString.includes("provisionedthroughputexceeded") ||
20
- errorString.includes("toolarge") ||
21
- errorString.includes("ratelimited")) {
22
- retryCount++;
23
- if (maxRetries !== null && retryCount >= maxRetries) {
24
- (0, log_1.warn)(`${operationName}: Max retries exceeded`);
25
- throw error;
26
- }
27
- const delay = baseDelay + retryCount * 1000;
28
- (0, log_1.warn)(`${operationName}: Throttled. Retry ${retryCount}. Sleeping for ${delay}ms`);
29
- // oxlint-disable-next-line no-await-in-loop -- backoff must complete before retrying
30
- await (0, sleep_1.sleep)(delay);
31
- continue;
106
+ catch (error) {
107
+ if (!(0, exports.isRetryableError)(error))
108
+ throw error;
109
+ if (maxRetries !== null && retryCount >= maxRetries) {
110
+ (0, log_1.warn)(`${operationName}: Max retries exceeded`);
111
+ throw error;
32
112
  }
33
- throw error;
113
+ retryCount++;
114
+ const delay = baseDelay + retryCount * 1000;
115
+ (0, log_1.warn)(`${operationName}: Retryable failure. Retry ${retryCount}. Sleeping for ${delay}ms`);
116
+ // oxlint-disable-next-line no-await-in-loop -- backoff must complete before retrying
117
+ await sleepFor(delay);
34
118
  }
35
119
  }
36
120
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ag-common",
3
- "version": "0.0.905",
3
+ "version": "0.0.907",
4
4
  "license": "ISC",
5
5
  "author": "admin@gec.dev",
6
6
  "repository": {