ag-common 0.0.910 → 0.0.912
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/dist/api/helpers/ai/adapters/google.js +56 -7
- package/dist/api/helpers/ai/client.js +1 -0
- package/dist/api/helpers/ai/types.d.ts +6 -0
- package/dist/api/helpers/google/index.d.ts +3 -0
- package/dist/api/helpers/google/index.js +18 -0
- package/dist/api/helpers/retryOnError.d.ts +19 -1
- package/dist/api/helpers/retryOnError.js +37 -5
- package/package.json +1 -1
|
@@ -45,6 +45,41 @@ const sortModelsByPreference = (models, prefer) => {
|
|
|
45
45
|
return modelArray;
|
|
46
46
|
};
|
|
47
47
|
const getFetch = (dependencies) => dependencies?.fetch ?? globalThis.fetch;
|
|
48
|
+
/**
|
|
49
|
+
* Extract an HTTP status from a generation failure. The SDK surfaces ApiError
|
|
50
|
+
* with `status` plus a JSON body in `message`; REST failures arrive as
|
|
51
|
+
* Response objects or status-carrying records, so probe all shapes.
|
|
52
|
+
*/
|
|
53
|
+
const generationFailureStatus = (error) => {
|
|
54
|
+
if (error instanceof Response)
|
|
55
|
+
return error.status;
|
|
56
|
+
if (typeof error !== "object" || error === null) {
|
|
57
|
+
return typeof error === "number" && Number.isInteger(error) ? error : undefined;
|
|
58
|
+
}
|
|
59
|
+
const record = error;
|
|
60
|
+
const candidates = [
|
|
61
|
+
record.status,
|
|
62
|
+
record.statusCode,
|
|
63
|
+
record.response?.status,
|
|
64
|
+
record.response?.statusCode,
|
|
65
|
+
record.error?.code,
|
|
66
|
+
];
|
|
67
|
+
for (const candidate of candidates) {
|
|
68
|
+
const status = typeof candidate === "string" && /^\d+$/.test(candidate) ? Number(candidate) : candidate;
|
|
69
|
+
if (typeof status === "number" && Number.isInteger(status))
|
|
70
|
+
return status;
|
|
71
|
+
}
|
|
72
|
+
const nested = error.cause;
|
|
73
|
+
if (nested !== undefined && nested !== error)
|
|
74
|
+
return generationFailureStatus(nested);
|
|
75
|
+
const message = error instanceof Error ? error.message : undefined;
|
|
76
|
+
if (message !== undefined) {
|
|
77
|
+
const match = /"code"\s*:\s*(\d{3})/.exec(message) ?? /\bstatus\D{0,16}(\d{3})\b/i.exec(message);
|
|
78
|
+
if (match?.[1])
|
|
79
|
+
return Number(match[1]);
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
};
|
|
48
83
|
const createAbortError = () => {
|
|
49
84
|
const error = new Error("Google AI request aborted");
|
|
50
85
|
error.name = "AbortError";
|
|
@@ -338,6 +373,11 @@ const generateGoogle = async (request, config = {}) => {
|
|
|
338
373
|
const effectiveRequest = { ...request, signal: controller.signal };
|
|
339
374
|
try {
|
|
340
375
|
const requestConfig = toRequestConfig(effectiveRequest);
|
|
376
|
+
const staggerMs = Math.max(0, request.staggerMs ?? 0);
|
|
377
|
+
const sleep = config.dependencies?.sleep ??
|
|
378
|
+
((ms) => new Promise((resolve) => {
|
|
379
|
+
setTimeout(resolve, ms);
|
|
380
|
+
}));
|
|
341
381
|
const result = await (0, retryOnError_1.retryOnError)(`generation:${request.ident ?? "unknown"}`, async () => {
|
|
342
382
|
const combinations = await getCombinations(request.model, request.prefer, request.output, controller.signal, config);
|
|
343
383
|
if (combinations.length === 0)
|
|
@@ -377,18 +417,19 @@ const generateGoogle = async (request, config = {}) => {
|
|
|
377
417
|
catch (error) {
|
|
378
418
|
if (controller.signal.aborted)
|
|
379
419
|
throw createAbortError();
|
|
380
|
-
const
|
|
420
|
+
const failureStatus = generationFailureStatus(error);
|
|
381
421
|
const message = error instanceof Error ? error.message : String(error);
|
|
382
|
-
const rateLimited =
|
|
383
|
-
const
|
|
422
|
+
const rateLimited = failureStatus === 429 || (0, retryOnError_1.isOverloadedApiKeyError)(error);
|
|
423
|
+
const overloadedModel = failureStatus === 503 || failureStatus === 500 || failureStatus === 502;
|
|
424
|
+
const unavailableModel = failureStatus === 404 && message.includes(selectedModel);
|
|
384
425
|
// Fallback only; per-model backoff below is keyed by rate limiting.
|
|
385
|
-
if (rateLimited || unavailableModel) {
|
|
426
|
+
if (rateLimited || overloadedModel || unavailableModel) {
|
|
386
427
|
(0, log_1.warn)("generation attempt failed; trying next available combination", {
|
|
387
428
|
model: selectedModel,
|
|
388
|
-
status,
|
|
429
|
+
status: failureStatus,
|
|
389
430
|
});
|
|
390
431
|
(0, apikey_1.blockKeyService)(key, `gemini-${selectedModel}`);
|
|
391
|
-
if (rateLimited) {
|
|
432
|
+
if (rateLimited || overloadedModel) {
|
|
392
433
|
(0, quota_1.recordModelRateLimit)(selectedModel, {
|
|
393
434
|
cause: error,
|
|
394
435
|
nowMs: config.dependencies?.now?.(),
|
|
@@ -401,7 +442,15 @@ const generateGoogle = async (request, config = {}) => {
|
|
|
401
442
|
}
|
|
402
443
|
}
|
|
403
444
|
throw lastFailure ?? new Error("No available model for this request");
|
|
404
|
-
},
|
|
445
|
+
}, {
|
|
446
|
+
retries: 1,
|
|
447
|
+
errorDelay: 5000,
|
|
448
|
+
initialDelayMs: staggerMs,
|
|
449
|
+
signal: controller.signal,
|
|
450
|
+
sleepFn: async (ms) => {
|
|
451
|
+
await sleep(ms);
|
|
452
|
+
},
|
|
453
|
+
});
|
|
405
454
|
return result;
|
|
406
455
|
}
|
|
407
456
|
catch (error) {
|
|
@@ -114,6 +114,7 @@ const mergeRequest = (defaults, request) => ({
|
|
|
114
114
|
onGenerated: request.onGenerated ?? defaults.onGenerated,
|
|
115
115
|
signal: request.signal ?? defaults.signal,
|
|
116
116
|
timeoutMs: request.timeoutMs ?? defaults.timeoutMs,
|
|
117
|
+
staggerMs: request.staggerMs ?? defaults.staggerMs,
|
|
117
118
|
});
|
|
118
119
|
const requestInput = async (options, dependencies, signal, timeoutMs) => {
|
|
119
120
|
const images = options.images ?? [];
|
|
@@ -69,6 +69,12 @@ export type AIRequest = {
|
|
|
69
69
|
onGenerated?: (metadata: GenerationMetadata) => void;
|
|
70
70
|
signal?: AbortSignal;
|
|
71
71
|
timeoutMs?: number;
|
|
72
|
+
/**
|
|
73
|
+
* Fixed pacing delay before the generation attempt, in ms. Lets bursty
|
|
74
|
+
* fan-out callers (e.g. a nightly job over N sites) stagger requests against
|
|
75
|
+
* low provider quotas without serializing the whole job. Defaults to 0.
|
|
76
|
+
*/
|
|
77
|
+
staggerMs?: number;
|
|
72
78
|
};
|
|
73
79
|
export type BinaryMedia = {
|
|
74
80
|
arraybuffer: ArrayBuffer;
|
|
@@ -1 +1,4 @@
|
|
|
1
1
|
export * from "./apikey";
|
|
2
|
+
export { generate, generateText, models, promptDirect, promptImage } from "../ai/client";
|
|
3
|
+
export type { AIClient, AIClientDefaults, AIClientDependencies, AIEndpoint, AIInput, AIModel, AIModelPreference, AIPart, AIRequest, BinaryMedia, GenerateTextOptions, GenerationResult, PromptDirectOptions, PromptImageOptions, } from "../ai/types";
|
|
4
|
+
export { AI_QUOTA_EXHAUSTED_MESSAGE, QuotaDeferredError, clearModelBackoff, getQuotaRetryDelayMs, isModelBackedOff, isQuotaExhaustedError, modelBackoffRemainingMs, quotaBackoffDelayMs, recordModelRateLimit, resetModelBackoff, } from "../ai/quota";
|
|
@@ -14,4 +14,22 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.resetModelBackoff = exports.recordModelRateLimit = exports.quotaBackoffDelayMs = exports.modelBackoffRemainingMs = exports.isQuotaExhaustedError = exports.isModelBackedOff = exports.getQuotaRetryDelayMs = exports.clearModelBackoff = exports.QuotaDeferredError = exports.AI_QUOTA_EXHAUSTED_MESSAGE = exports.promptImage = exports.promptDirect = exports.models = exports.generateText = exports.generate = void 0;
|
|
17
18
|
__exportStar(require("./apikey"), exports);
|
|
19
|
+
var client_1 = require("../ai/client");
|
|
20
|
+
Object.defineProperty(exports, "generate", { enumerable: true, get: function () { return client_1.generate; } });
|
|
21
|
+
Object.defineProperty(exports, "generateText", { enumerable: true, get: function () { return client_1.generateText; } });
|
|
22
|
+
Object.defineProperty(exports, "models", { enumerable: true, get: function () { return client_1.models; } });
|
|
23
|
+
Object.defineProperty(exports, "promptDirect", { enumerable: true, get: function () { return client_1.promptDirect; } });
|
|
24
|
+
Object.defineProperty(exports, "promptImage", { enumerable: true, get: function () { return client_1.promptImage; } });
|
|
25
|
+
var quota_1 = require("../ai/quota");
|
|
26
|
+
Object.defineProperty(exports, "AI_QUOTA_EXHAUSTED_MESSAGE", { enumerable: true, get: function () { return quota_1.AI_QUOTA_EXHAUSTED_MESSAGE; } });
|
|
27
|
+
Object.defineProperty(exports, "QuotaDeferredError", { enumerable: true, get: function () { return quota_1.QuotaDeferredError; } });
|
|
28
|
+
Object.defineProperty(exports, "clearModelBackoff", { enumerable: true, get: function () { return quota_1.clearModelBackoff; } });
|
|
29
|
+
Object.defineProperty(exports, "getQuotaRetryDelayMs", { enumerable: true, get: function () { return quota_1.getQuotaRetryDelayMs; } });
|
|
30
|
+
Object.defineProperty(exports, "isModelBackedOff", { enumerable: true, get: function () { return quota_1.isModelBackedOff; } });
|
|
31
|
+
Object.defineProperty(exports, "isQuotaExhaustedError", { enumerable: true, get: function () { return quota_1.isQuotaExhaustedError; } });
|
|
32
|
+
Object.defineProperty(exports, "modelBackoffRemainingMs", { enumerable: true, get: function () { return quota_1.modelBackoffRemainingMs; } });
|
|
33
|
+
Object.defineProperty(exports, "quotaBackoffDelayMs", { enumerable: true, get: function () { return quota_1.quotaBackoffDelayMs; } });
|
|
34
|
+
Object.defineProperty(exports, "recordModelRateLimit", { enumerable: true, get: function () { return quota_1.recordModelRateLimit; } });
|
|
35
|
+
Object.defineProperty(exports, "resetModelBackoff", { enumerable: true, get: function () { return quota_1.resetModelBackoff; } });
|
|
@@ -2,7 +2,25 @@ export declare const overloadedMessages: string[];
|
|
|
2
2
|
export declare const retryableErrorMessages: string[];
|
|
3
3
|
export declare const isOverloadedApiKeyError: (error: unknown) => boolean;
|
|
4
4
|
export declare const isRetryableApiError: (error: unknown) => boolean;
|
|
5
|
+
export type RetryOnErrorOptions = {
|
|
6
|
+
/** Number of retries after the initial attempt. Defaults to 1. */
|
|
7
|
+
retries?: number;
|
|
8
|
+
/** Delay between attempts in ms. Defaults to 2000. */
|
|
9
|
+
errorDelay?: number;
|
|
10
|
+
/** Predicate deciding whether a failure is worth retrying. */
|
|
11
|
+
errorCheck?: (error: unknown) => boolean;
|
|
12
|
+
/** AbortSignal cancelling the backoff sleep and further attempts. */
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
/**
|
|
15
|
+
* Delay before each attempt after the first, in ms. It composes with
|
|
16
|
+
* errorDelay: the attempt waits for initialDelayMs plus errorDelay.
|
|
17
|
+
* Defaults to 0 (no pacing).
|
|
18
|
+
*/
|
|
19
|
+
initialDelayMs?: number;
|
|
20
|
+
/** Sleep implementation, overridable in tests. */
|
|
21
|
+
sleepFn?: (ms: number) => Promise<unknown>;
|
|
22
|
+
};
|
|
5
23
|
export declare function retryOnError<T>(
|
|
6
24
|
/** so we can log retries with useful info */
|
|
7
|
-
debugIdent: string, fn: () => Promise<T>,
|
|
25
|
+
debugIdent: string, fn: () => Promise<T>, retriesOrOptions?: number | RetryOnErrorOptions, errorDelay?: number, errorCheck?: (error: unknown) => boolean, signal?: AbortSignal): Promise<T>;
|
|
8
26
|
export declare const sleep: (ms: number) => Promise<unknown>;
|
|
@@ -70,19 +70,51 @@ const awaitWithSignal = async (promise, signal) => {
|
|
|
70
70
|
});
|
|
71
71
|
return Promise.race([promise, abort]);
|
|
72
72
|
};
|
|
73
|
+
const normalizeRetryOnErrorOptions = (retriesOrOptions, errorDelay, errorCheck, signal) => ({
|
|
74
|
+
retries: typeof retriesOrOptions === "number" ? retriesOrOptions : (retriesOrOptions?.retries ?? 1),
|
|
75
|
+
errorDelay: typeof retriesOrOptions === "number"
|
|
76
|
+
? (errorDelay ?? 2000)
|
|
77
|
+
: (retriesOrOptions?.errorDelay ?? 2000),
|
|
78
|
+
errorCheck: typeof retriesOrOptions === "number"
|
|
79
|
+
? (errorCheck ?? exports.isRetryableApiError)
|
|
80
|
+
: (retriesOrOptions?.errorCheck ?? exports.isRetryableApiError),
|
|
81
|
+
signal: typeof retriesOrOptions === "number" ? signal : (retriesOrOptions?.signal ?? signal),
|
|
82
|
+
initialDelayMs: typeof retriesOrOptions === "number" ? 0 : Math.max(0, retriesOrOptions?.initialDelayMs ?? 0),
|
|
83
|
+
sleepFn: typeof retriesOrOptions === "number" ? undefined : retriesOrOptions?.sleepFn,
|
|
84
|
+
});
|
|
73
85
|
async function retryOnError(
|
|
74
86
|
/** so we can log retries with useful info */
|
|
75
|
-
debugIdent, fn,
|
|
87
|
+
debugIdent, fn, retriesOrOptions, errorDelay, errorCheck = exports.isRetryableApiError, signal) {
|
|
88
|
+
const options = normalizeRetryOnErrorOptions(retriesOrOptions, errorDelay, errorCheck, signal);
|
|
89
|
+
const sleepWithSignal = async (ms) => {
|
|
90
|
+
if (ms <= 0)
|
|
91
|
+
return;
|
|
92
|
+
if (options.sleepFn !== undefined) {
|
|
93
|
+
await options.sleepFn(ms);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
await awaitWithSignal((0, exports.sleep)(ms), options.signal);
|
|
97
|
+
};
|
|
98
|
+
// Pacing delay before attempts after the first: it spreads bursty fan-out
|
|
99
|
+
// (e.g. nightly jobs hitting a low free-tier quota) without changing the
|
|
100
|
+
// retry semantics for direct callers.
|
|
101
|
+
await sleepWithSignal(options.initialDelayMs);
|
|
76
102
|
try {
|
|
77
103
|
const result = await fn();
|
|
78
104
|
return result;
|
|
79
105
|
}
|
|
80
106
|
catch (error) {
|
|
81
107
|
const message = error instanceof Error ? error.message : String(error);
|
|
82
|
-
if (retries > 0 && errorCheck(error)) {
|
|
83
|
-
(0, log_1.info)(`Operation ${debugIdent} failed. Retrying after ${errorDelay}ms...`, message);
|
|
84
|
-
await
|
|
85
|
-
return retryOnError(debugIdent, fn,
|
|
108
|
+
if (options.retries > 0 && options.errorCheck(error)) {
|
|
109
|
+
(0, log_1.info)(`Operation ${debugIdent} failed. Retrying after ${options.errorDelay}ms...`, message);
|
|
110
|
+
await sleepWithSignal(options.errorDelay);
|
|
111
|
+
return retryOnError(debugIdent, fn, {
|
|
112
|
+
retries: options.retries - 1,
|
|
113
|
+
errorDelay: options.errorDelay,
|
|
114
|
+
errorCheck: options.errorCheck,
|
|
115
|
+
signal: options.signal,
|
|
116
|
+
sleepFn: options.sleepFn,
|
|
117
|
+
});
|
|
86
118
|
}
|
|
87
119
|
throw error;
|
|
88
120
|
}
|