ag-common 0.0.908 → 0.0.910
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/README.md +32 -0
- package/dist/api/helpers/ai/adapters/codex.d.ts +12 -0
- package/dist/api/helpers/ai/adapters/codex.js +573 -0
- package/dist/api/helpers/ai/adapters/google.d.ts +12 -0
- package/dist/api/helpers/ai/adapters/google.js +429 -0
- package/dist/api/helpers/ai/client.d.ts +11 -0
- package/dist/api/helpers/ai/client.js +191 -0
- package/dist/api/helpers/ai/index.d.ts +3 -0
- package/dist/api/helpers/ai/index.js +18 -0
- package/dist/api/helpers/ai/quota.d.ts +55 -0
- package/dist/api/helpers/ai/quota.js +148 -0
- package/dist/api/helpers/ai/types.d.ts +118 -0
- package/dist/api/helpers/ai/types.js +2 -0
- package/dist/api/helpers/google/index.d.ts +0 -1
- package/dist/api/helpers/google/index.js +0 -1
- package/dist/api/helpers/index.d.ts +1 -0
- package/dist/api/helpers/index.js +1 -0
- package/dist/api/helpers/retryOnError.d.ts +3 -3
- package/dist/api/helpers/retryOnError.js +60 -10
- package/package.json +1 -1
- package/dist/api/helpers/google/gemini.d.ts +0 -26
- package/dist/api/helpers/google/gemini.js +0 -239
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resetGoogleAdapterForTests = exports.resetModelBackoff = exports.listGoogleModels = exports.generateGoogle = void 0;
|
|
4
|
+
const genai_1 = require("@google/genai");
|
|
5
|
+
const log_1 = require("../../../../common/helpers/log");
|
|
6
|
+
const node_cache_1 = require("../../../../common/helpers/node-cache");
|
|
7
|
+
const retryOnError_1 = require("../../retryOnError");
|
|
8
|
+
const quota_1 = require("../quota");
|
|
9
|
+
Object.defineProperty(exports, "resetModelBackoff", { enumerable: true, get: function () { return quota_1.resetModelBackoff; } });
|
|
10
|
+
const apikey_1 = require("../../google/apikey");
|
|
11
|
+
const FALLBACK_GEMINI_MODELS = [
|
|
12
|
+
"gemini-3-flash-preview",
|
|
13
|
+
"gemini-3-pro-preview",
|
|
14
|
+
"gemini-2.5-pro",
|
|
15
|
+
"gemini-2.5-flash",
|
|
16
|
+
"gemini-2.5-flash-lite",
|
|
17
|
+
];
|
|
18
|
+
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
19
|
+
const NON_TEXT_MODEL_CAPABILITIES = [
|
|
20
|
+
"audio",
|
|
21
|
+
"computer-use",
|
|
22
|
+
"embedding",
|
|
23
|
+
"image",
|
|
24
|
+
"live",
|
|
25
|
+
"robotics",
|
|
26
|
+
"tts",
|
|
27
|
+
];
|
|
28
|
+
let genAIs = new Map();
|
|
29
|
+
const geminiModelsCache = new node_cache_1.TypedNodeCache({ stdTTL: 86400 });
|
|
30
|
+
const geminiModelsCacheKey = "gemini-models-v3";
|
|
31
|
+
const normalizeModelName = (name) => name.replace(/^models\//, "");
|
|
32
|
+
const isTextGenerationModel = (name) => {
|
|
33
|
+
const normalizedName = name.toLowerCase();
|
|
34
|
+
return (normalizedName.startsWith("gemini") &&
|
|
35
|
+
!NON_TEXT_MODEL_CAPABILITIES.some((capability) => normalizedName.includes(capability)));
|
|
36
|
+
};
|
|
37
|
+
const sortModelsByPreference = (models, prefer) => {
|
|
38
|
+
const modelArray = [...models];
|
|
39
|
+
if (prefer === "quality") {
|
|
40
|
+
return modelArray.sort((a, b) => Number(b.includes("pro")) - Number(a.includes("pro")));
|
|
41
|
+
}
|
|
42
|
+
if (prefer === "fast") {
|
|
43
|
+
return modelArray.sort((a, b) => Number(b.includes("flash")) - Number(a.includes("flash")));
|
|
44
|
+
}
|
|
45
|
+
return modelArray;
|
|
46
|
+
};
|
|
47
|
+
const getFetch = (dependencies) => dependencies?.fetch ?? globalThis.fetch;
|
|
48
|
+
const createAbortError = () => {
|
|
49
|
+
const error = new Error("Google AI request aborted");
|
|
50
|
+
error.name = "AbortError";
|
|
51
|
+
return error;
|
|
52
|
+
};
|
|
53
|
+
const awaitWithSignal = async (promise, signal) => {
|
|
54
|
+
if (signal === undefined)
|
|
55
|
+
return promise;
|
|
56
|
+
if (signal.aborted)
|
|
57
|
+
throw createAbortError();
|
|
58
|
+
const abort = new Promise((_, reject) => {
|
|
59
|
+
const onAbort = () => reject(createAbortError());
|
|
60
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
61
|
+
void promise.then(() => signal.removeEventListener("abort", onAbort), () => signal.removeEventListener("abort", onAbort));
|
|
62
|
+
});
|
|
63
|
+
return Promise.race([promise, abort]);
|
|
64
|
+
};
|
|
65
|
+
const parseGoogleModalities = (value) => {
|
|
66
|
+
if (!Array.isArray(value))
|
|
67
|
+
return undefined;
|
|
68
|
+
const modalities = value.filter((item) => item === "text" ||
|
|
69
|
+
item === "image" ||
|
|
70
|
+
item === "audio" ||
|
|
71
|
+
item === "video" ||
|
|
72
|
+
item === "file");
|
|
73
|
+
return [...new Set(modalities)];
|
|
74
|
+
};
|
|
75
|
+
const parseGoogleStrings = (value) => {
|
|
76
|
+
if (!Array.isArray(value))
|
|
77
|
+
return undefined;
|
|
78
|
+
return [
|
|
79
|
+
...new Set(value.flatMap((item) => {
|
|
80
|
+
if (typeof item === "string")
|
|
81
|
+
return item.trim().length > 0 ? [item.trim()] : [];
|
|
82
|
+
if (typeof item === "object" &&
|
|
83
|
+
item !== null &&
|
|
84
|
+
"reasoningEffort" in item &&
|
|
85
|
+
typeof item.reasoningEffort === "string") {
|
|
86
|
+
return item.reasoningEffort.trim().length > 0 ? [item.reasoningEffort.trim()] : [];
|
|
87
|
+
}
|
|
88
|
+
return [];
|
|
89
|
+
})),
|
|
90
|
+
];
|
|
91
|
+
};
|
|
92
|
+
const googleModelMetadata = (model) => {
|
|
93
|
+
const id = normalizeModelName(model.name ?? "");
|
|
94
|
+
const metadata = { id };
|
|
95
|
+
const inputModalities = parseGoogleModalities(model.inputModalities);
|
|
96
|
+
const outputModalities = parseGoogleModalities(model.outputModalities);
|
|
97
|
+
const supportedReasoningEfforts = parseGoogleStrings(model.supportedReasoningEfforts);
|
|
98
|
+
const defaultReasoningEffort = typeof model.defaultReasoningEffort === "string"
|
|
99
|
+
? model.defaultReasoningEffort.trim()
|
|
100
|
+
: undefined;
|
|
101
|
+
const prefer = Array.isArray(model.prefer)
|
|
102
|
+
? model.prefer.filter((item) => item === "fast" || item === "quality")
|
|
103
|
+
: undefined;
|
|
104
|
+
if (inputModalities !== undefined)
|
|
105
|
+
metadata.inputModalities = inputModalities;
|
|
106
|
+
if (outputModalities !== undefined)
|
|
107
|
+
metadata.outputModalities = outputModalities;
|
|
108
|
+
if (supportedReasoningEfforts !== undefined) {
|
|
109
|
+
metadata.supportedReasoningEfforts = supportedReasoningEfforts;
|
|
110
|
+
}
|
|
111
|
+
if (defaultReasoningEffort !== undefined && defaultReasoningEffort.length > 0) {
|
|
112
|
+
metadata.defaultReasoningEffort = defaultReasoningEffort;
|
|
113
|
+
}
|
|
114
|
+
if (prefer !== undefined)
|
|
115
|
+
metadata.prefer = [...new Set(prefer)];
|
|
116
|
+
for (const key of ["webSearch", "isDefault", "ready"]) {
|
|
117
|
+
const value = model[key];
|
|
118
|
+
if (typeof value === "boolean")
|
|
119
|
+
metadata[key] = value;
|
|
120
|
+
}
|
|
121
|
+
return metadata;
|
|
122
|
+
};
|
|
123
|
+
const getAvailableGoogleModels = async (dependencies, signal) => {
|
|
124
|
+
const cachedModels = geminiModelsCache.get(geminiModelsCacheKey);
|
|
125
|
+
if (cachedModels && cachedModels.length > 0)
|
|
126
|
+
return cachedModels;
|
|
127
|
+
const key = (0, apikey_1.getAvailableCombinations)("gemini")[0]?.key;
|
|
128
|
+
if (!key) {
|
|
129
|
+
(0, log_1.warn)("No GOOGLE_API_KEY available. Falling back to default model list.");
|
|
130
|
+
return FALLBACK_GEMINI_MODELS.map((id) => ({ id }));
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const response = await awaitWithSignal(getFetch(dependencies)(`https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(key)}`, { redirect: "manual", ...(signal === undefined ? {} : { signal }) }), signal);
|
|
134
|
+
if (!response.ok)
|
|
135
|
+
throw new Error(`model list fetch failed with status ${response.status}`);
|
|
136
|
+
const payload = (await awaitWithSignal(response.json(), signal));
|
|
137
|
+
const discoveredModels = [
|
|
138
|
+
...new Set((payload.models ?? [])
|
|
139
|
+
.filter((model) => (model.supportedGenerationMethods ?? []).includes("generateContent"))
|
|
140
|
+
.map(googleModelMetadata)
|
|
141
|
+
.filter((model) => model.id.length > 0)),
|
|
142
|
+
];
|
|
143
|
+
if (discoveredModels.length === 0)
|
|
144
|
+
throw new Error("no generateContent models discovered");
|
|
145
|
+
geminiModelsCache.set(geminiModelsCacheKey, discoveredModels);
|
|
146
|
+
(0, log_1.info)(`loaded ${discoveredModels.length} Google AI models from API`);
|
|
147
|
+
return discoveredModels;
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (signal?.aborted)
|
|
151
|
+
throw createAbortError();
|
|
152
|
+
(0, log_1.warn)(`Failed to load Google AI models. Falling back to defaults. ${String(error)}`);
|
|
153
|
+
return FALLBACK_GEMINI_MODELS.map((id) => ({ id }));
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const getAvailableGeminiModels = async (dependencies, signal) => (await getAvailableGoogleModels(dependencies, signal)).map((model) => model.id);
|
|
157
|
+
const getClients = (createAI = (apiKey) => new genai_1.GoogleGenAI({ apiKey })) => {
|
|
158
|
+
const combinations = (0, apikey_1.getAvailableCombinations)("gemini");
|
|
159
|
+
for (const { key } of combinations) {
|
|
160
|
+
if (!genAIs.has(key))
|
|
161
|
+
genAIs.set(key, createAI(key));
|
|
162
|
+
}
|
|
163
|
+
return combinations.flatMap(({ key }) => {
|
|
164
|
+
const ai = genAIs.get(key);
|
|
165
|
+
return ai === undefined ? [] : [[key, ai]];
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
const getCombinations = async (model, prefer, output, signal, config) => {
|
|
169
|
+
const clients = getClients(config.createAI);
|
|
170
|
+
const requiresNonTextModel = (output ?? ["text"]).some((modality) => modality !== "text");
|
|
171
|
+
if (model === undefined && requiresNonTextModel) {
|
|
172
|
+
throw new Error("Google AI requires an explicit model for non-text output");
|
|
173
|
+
}
|
|
174
|
+
const models = model
|
|
175
|
+
? [model]
|
|
176
|
+
: sortModelsByPreference((await getAvailableGeminiModels(config.dependencies, signal)).filter(isTextGenerationModel), prefer);
|
|
177
|
+
if (models.length === 0)
|
|
178
|
+
throw new Error("No text generation models are available");
|
|
179
|
+
const combinations = [];
|
|
180
|
+
for (const [key, ai] of clients) {
|
|
181
|
+
for (const selectedModel of models) {
|
|
182
|
+
const available = (0, apikey_1.getAvailableCombinations)(`gemini-${selectedModel}`, false);
|
|
183
|
+
if (available.some((entry) => entry.key === key)) {
|
|
184
|
+
combinations.push([key, ai, selectedModel]);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return combinations;
|
|
189
|
+
};
|
|
190
|
+
const mediaType = (mimeType) => {
|
|
191
|
+
const normalized = mimeType.toLowerCase();
|
|
192
|
+
if (normalized.startsWith("image/"))
|
|
193
|
+
return "image";
|
|
194
|
+
if (normalized.startsWith("audio/"))
|
|
195
|
+
return "audio";
|
|
196
|
+
if (normalized.startsWith("video/"))
|
|
197
|
+
return "video";
|
|
198
|
+
return "file";
|
|
199
|
+
};
|
|
200
|
+
const toGeminiPart = (part) => {
|
|
201
|
+
if (part.type === "text")
|
|
202
|
+
return { text: part.text };
|
|
203
|
+
if (part.name !== undefined) {
|
|
204
|
+
throw new Error("Google AI inline media does not support media names");
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
inlineData: {
|
|
208
|
+
data: part.data,
|
|
209
|
+
mimeType: part.mimeType,
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
const toGeminiContents = (request) => {
|
|
214
|
+
const contents = [];
|
|
215
|
+
const systemParts = request.instructions === undefined ? [] : [{ text: request.instructions }];
|
|
216
|
+
for (const input of request.input) {
|
|
217
|
+
const parts = input.content.map(toGeminiPart);
|
|
218
|
+
if (input.role === "system" || input.role === "developer") {
|
|
219
|
+
systemParts.push(...parts);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
contents.push({ role: input.role === "assistant" ? "model" : "user", parts });
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
contents,
|
|
226
|
+
...(systemParts.length === 0
|
|
227
|
+
? {}
|
|
228
|
+
: { systemInstruction: { role: "user", parts: systemParts } }),
|
|
229
|
+
};
|
|
230
|
+
};
|
|
231
|
+
const responseModalities = (request) => {
|
|
232
|
+
const output = request.output ?? ["text"];
|
|
233
|
+
if (output.length === 0)
|
|
234
|
+
throw new Error("AI output modalities cannot be empty");
|
|
235
|
+
for (const modality of output) {
|
|
236
|
+
if (modality !== "text" && modality !== "image" && modality !== "audio") {
|
|
237
|
+
throw new Error(`Google AI does not support ${modality} output`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return output.map((modality) => modality.toUpperCase());
|
|
241
|
+
};
|
|
242
|
+
const toOutputSchema = (outputSchema) => {
|
|
243
|
+
if (outputSchema.name.trim().length === 0)
|
|
244
|
+
throw new Error("AI output schema name is required");
|
|
245
|
+
const rawSchema = outputSchema.schema;
|
|
246
|
+
if (typeof rawSchema !== "object" || rawSchema === null || Array.isArray(rawSchema)) {
|
|
247
|
+
throw new Error("Google AI output schema must be an object");
|
|
248
|
+
}
|
|
249
|
+
const schema = rawSchema;
|
|
250
|
+
const existingTitle = schema.title;
|
|
251
|
+
if (existingTitle !== undefined && existingTitle !== outputSchema.name) {
|
|
252
|
+
throw new Error("Google AI output schema name conflicts with its title");
|
|
253
|
+
}
|
|
254
|
+
return existingTitle === outputSchema.name ? schema : { ...schema, title: outputSchema.name };
|
|
255
|
+
};
|
|
256
|
+
const toRequestConfig = (request) => {
|
|
257
|
+
if (request.reasoningEffort !== undefined) {
|
|
258
|
+
throw new Error("Google AI adapter does not support reasoningEffort");
|
|
259
|
+
}
|
|
260
|
+
const { contents, systemInstruction } = toGeminiContents(request);
|
|
261
|
+
if (contents.length === 0)
|
|
262
|
+
throw new Error("AI request must contain user or assistant input");
|
|
263
|
+
const config = {
|
|
264
|
+
responseModalities: responseModalities(request),
|
|
265
|
+
...(systemInstruction === undefined ? {} : { systemInstruction }),
|
|
266
|
+
...(request.maxOutputTokens === undefined ? {} : { maxOutputTokens: request.maxOutputTokens }),
|
|
267
|
+
...(request.outputSchema === undefined
|
|
268
|
+
? {}
|
|
269
|
+
: {
|
|
270
|
+
responseMimeType: "application/json",
|
|
271
|
+
responseJsonSchema: toOutputSchema(request.outputSchema),
|
|
272
|
+
}),
|
|
273
|
+
...(request.webSearch === true ? { tools: [{ googleSearch: {} }] } : {}),
|
|
274
|
+
...(request.signal === undefined ? {} : { abortSignal: request.signal }),
|
|
275
|
+
};
|
|
276
|
+
return {
|
|
277
|
+
model: "",
|
|
278
|
+
contents,
|
|
279
|
+
config,
|
|
280
|
+
};
|
|
281
|
+
};
|
|
282
|
+
const responseOutput = (response) => {
|
|
283
|
+
const candidate = response.candidates?.[0];
|
|
284
|
+
if (candidate === undefined)
|
|
285
|
+
throw new Error("Google AI returned no output");
|
|
286
|
+
const parts = candidate.content?.parts ?? [];
|
|
287
|
+
if (parts.length === 0) {
|
|
288
|
+
const finishReason = candidate.finishReason;
|
|
289
|
+
if (finishReason === undefined || String(finishReason) === "STOP") {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
throw new Error(`Google AI returned no output (${String(finishReason)})`);
|
|
293
|
+
}
|
|
294
|
+
return parts.flatMap((part) => {
|
|
295
|
+
if (part.thought === true)
|
|
296
|
+
return [];
|
|
297
|
+
if (typeof part.text === "string")
|
|
298
|
+
return [{ type: "text", text: part.text }];
|
|
299
|
+
if (part.inlineData?.data === undefined || part.inlineData.mimeType === undefined) {
|
|
300
|
+
throw new Error("Google AI returned an unsupported output part");
|
|
301
|
+
}
|
|
302
|
+
const type = mediaType(part.inlineData.mimeType);
|
|
303
|
+
const name = part.inlineData.displayName;
|
|
304
|
+
return [
|
|
305
|
+
name === undefined
|
|
306
|
+
? {
|
|
307
|
+
type,
|
|
308
|
+
mimeType: part.inlineData.mimeType,
|
|
309
|
+
data: part.inlineData.data,
|
|
310
|
+
}
|
|
311
|
+
: {
|
|
312
|
+
type,
|
|
313
|
+
mimeType: part.inlineData.mimeType,
|
|
314
|
+
data: part.inlineData.data,
|
|
315
|
+
name,
|
|
316
|
+
},
|
|
317
|
+
];
|
|
318
|
+
});
|
|
319
|
+
};
|
|
320
|
+
const responseUsage = (response) => {
|
|
321
|
+
const usage = response.usageMetadata;
|
|
322
|
+
if (!usage)
|
|
323
|
+
return undefined;
|
|
324
|
+
return {
|
|
325
|
+
inputTokens: usage.promptTokenCount ?? 0,
|
|
326
|
+
outputTokens: usage.candidatesTokenCount ?? 0,
|
|
327
|
+
totalTokens: usage.totalTokenCount ?? 0,
|
|
328
|
+
};
|
|
329
|
+
};
|
|
330
|
+
const generateGoogle = async (request, config = {}) => {
|
|
331
|
+
const timeoutMs = request.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
332
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
333
|
+
throw new Error("generation timeout must be positive");
|
|
334
|
+
const controller = new AbortController();
|
|
335
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
336
|
+
const onAbort = () => controller.abort();
|
|
337
|
+
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
338
|
+
const effectiveRequest = { ...request, signal: controller.signal };
|
|
339
|
+
try {
|
|
340
|
+
const requestConfig = toRequestConfig(effectiveRequest);
|
|
341
|
+
const result = await (0, retryOnError_1.retryOnError)(`generation:${request.ident ?? "unknown"}`, async () => {
|
|
342
|
+
const combinations = await getCombinations(request.model, request.prefer, request.output, controller.signal, config);
|
|
343
|
+
if (combinations.length === 0)
|
|
344
|
+
throw new Error("No available model for this request");
|
|
345
|
+
let lastFailure;
|
|
346
|
+
for (const [key, ai, selectedModel] of combinations) {
|
|
347
|
+
if (!(0, apikey_1.getAvailableCombinations)(`gemini-${selectedModel}`, false).some((entry) => entry.key === key)) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
// Reactive per-model backoff: a 429 skips the model until its window
|
|
351
|
+
// elapses while every other model keeps serving.
|
|
352
|
+
if ((0, quota_1.isModelBackedOff)(selectedModel)) {
|
|
353
|
+
(0, log_1.debug)("skipping rate-limited model", {
|
|
354
|
+
model: selectedModel,
|
|
355
|
+
remainingMs: (0, quota_1.modelBackoffRemainingMs)(selectedModel),
|
|
356
|
+
});
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const selectedRequest = {
|
|
360
|
+
...requestConfig,
|
|
361
|
+
model: selectedModel,
|
|
362
|
+
};
|
|
363
|
+
(0, log_1.info)("generation request", { model: selectedModel, ident: request.ident });
|
|
364
|
+
try {
|
|
365
|
+
// oxlint-disable-next-line no-await-in-loop -- model fallback is sequential by design
|
|
366
|
+
const response = await awaitWithSignal(ai.models.generateContent(selectedRequest), controller.signal);
|
|
367
|
+
const usage = responseUsage(response);
|
|
368
|
+
const result = {
|
|
369
|
+
output: responseOutput(response),
|
|
370
|
+
model: response.modelVersion || selectedModel,
|
|
371
|
+
generatedAt: config.dependencies?.now?.() ?? Date.now(),
|
|
372
|
+
...(usage === undefined ? {} : { usage }),
|
|
373
|
+
};
|
|
374
|
+
(0, log_1.debug)("generation completed", request.ident);
|
|
375
|
+
return result;
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
if (controller.signal.aborted)
|
|
379
|
+
throw createAbortError();
|
|
380
|
+
const status = error.status;
|
|
381
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
382
|
+
const rateLimited = status === 429 || (0, retryOnError_1.isOverloadedApiKeyError)(error);
|
|
383
|
+
const unavailableModel = status === 404 && message.includes(selectedModel);
|
|
384
|
+
// Fallback only; per-model backoff below is keyed by rate limiting.
|
|
385
|
+
if (rateLimited || unavailableModel) {
|
|
386
|
+
(0, log_1.warn)("generation attempt failed; trying next available combination", {
|
|
387
|
+
model: selectedModel,
|
|
388
|
+
status,
|
|
389
|
+
});
|
|
390
|
+
(0, apikey_1.blockKeyService)(key, `gemini-${selectedModel}`);
|
|
391
|
+
if (rateLimited) {
|
|
392
|
+
(0, quota_1.recordModelRateLimit)(selectedModel, {
|
|
393
|
+
cause: error,
|
|
394
|
+
nowMs: config.dependencies?.now?.(),
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
lastFailure = error;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
throw lastFailure ?? new Error("No available model for this request");
|
|
404
|
+
}, 1, 5000, undefined, controller.signal);
|
|
405
|
+
return result;
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
if (controller.signal.aborted) {
|
|
409
|
+
throw request.signal?.aborted
|
|
410
|
+
? new Error("generation aborted")
|
|
411
|
+
: new Error("generation timed out");
|
|
412
|
+
}
|
|
413
|
+
throw error;
|
|
414
|
+
}
|
|
415
|
+
finally {
|
|
416
|
+
clearTimeout(timeout);
|
|
417
|
+
request.signal?.removeEventListener("abort", onAbort);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
exports.generateGoogle = generateGoogle;
|
|
421
|
+
const listGoogleModels = async (dependencies) => getAvailableGoogleModels(dependencies);
|
|
422
|
+
exports.listGoogleModels = listGoogleModels;
|
|
423
|
+
/** Test seam: reset adapter clients, model cache, and per-model rate-limit backoff. */
|
|
424
|
+
const resetGoogleAdapterForTests = () => {
|
|
425
|
+
genAIs = new Map();
|
|
426
|
+
geminiModelsCache.flushAll();
|
|
427
|
+
(0, quota_1.resetModelBackoff)();
|
|
428
|
+
};
|
|
429
|
+
exports.resetGoogleAdapterForTests = resetGoogleAdapterForTests;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AIClient, AIClientDefaults, AIClientDependencies, AIEndpoint, AIInput, AIModel, AIModelPreference, AIPart, AIRequest, BinaryMedia, GenerateTextOptions, GenerationResult, PromptDirectOptions, PromptImageOptions } from "./types";
|
|
2
|
+
export declare const createAIClient: (defaults?: AIClientDefaults) => AIClient;
|
|
3
|
+
export declare const generate: (request: AIRequest) => Promise<GenerationResult>;
|
|
4
|
+
export declare function generateText(prompt: string, options?: GenerateTextOptions): Promise<string>;
|
|
5
|
+
export declare function generateText(options: GenerateTextOptions): Promise<string>;
|
|
6
|
+
export declare const promptDirect: (options: PromptDirectOptions) => Promise<string>;
|
|
7
|
+
export declare const promptImage: (options: PromptImageOptions) => Promise<string>;
|
|
8
|
+
export declare const models: (options?: {
|
|
9
|
+
endpoint?: AIEndpoint;
|
|
10
|
+
}) => Promise<AIModel[]>;
|
|
11
|
+
export type { AIClient, AIClientDefaults, AIClientDependencies, AIEndpoint, AIInput, AIModel, AIModelPreference, AIPart, AIRequest, BinaryMedia, GenerateTextOptions, GenerationResult, PromptDirectOptions, PromptImageOptions, };
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.models = exports.promptImage = exports.promptDirect = exports.generate = exports.createAIClient = void 0;
|
|
4
|
+
exports.generateText = generateText;
|
|
5
|
+
const codex_1 = require("./adapters/codex");
|
|
6
|
+
const google_1 = require("./adapters/google");
|
|
7
|
+
const MAX_DOWNLOAD_BYTES = 25 * 1024 * 1024;
|
|
8
|
+
const DOWNLOAD_TIMEOUT_MS = 30 * 1000;
|
|
9
|
+
const defaultSleep = (milliseconds) => new Promise((resolve) => {
|
|
10
|
+
setTimeout(resolve, milliseconds);
|
|
11
|
+
});
|
|
12
|
+
const createAbortError = () => {
|
|
13
|
+
const error = new Error("AI media download aborted");
|
|
14
|
+
error.name = "AbortError";
|
|
15
|
+
return error;
|
|
16
|
+
};
|
|
17
|
+
const awaitWithSignal = async (promise, signal) => {
|
|
18
|
+
if (signal === undefined)
|
|
19
|
+
return promise;
|
|
20
|
+
if (signal.aborted)
|
|
21
|
+
throw createAbortError();
|
|
22
|
+
const abort = new Promise((_, reject) => {
|
|
23
|
+
const onAbort = () => reject(createAbortError());
|
|
24
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
25
|
+
void promise.then(() => signal.removeEventListener("abort", onAbort), () => signal.removeEventListener("abort", onAbort));
|
|
26
|
+
});
|
|
27
|
+
return Promise.race([promise, abort]);
|
|
28
|
+
};
|
|
29
|
+
const getDependencies = (defaults) => ({
|
|
30
|
+
fetch: defaults.fetch ?? defaults.dependencies?.fetch ?? globalThis.fetch,
|
|
31
|
+
sleep: defaults.sleep ?? defaults.dependencies?.sleep ?? defaultSleep,
|
|
32
|
+
now: defaults.now ?? defaults.dependencies?.now ?? (() => Date.now()),
|
|
33
|
+
createIdempotencyKey: defaults.createIdempotencyKey ?? defaults.dependencies?.createIdempotencyKey,
|
|
34
|
+
});
|
|
35
|
+
const mediaType = (mimeType) => {
|
|
36
|
+
const normalized = mimeType.toLowerCase();
|
|
37
|
+
if (normalized.startsWith("image/"))
|
|
38
|
+
return "image";
|
|
39
|
+
if (normalized.startsWith("audio/"))
|
|
40
|
+
return "audio";
|
|
41
|
+
if (normalized.startsWith("video/"))
|
|
42
|
+
return "video";
|
|
43
|
+
return "file";
|
|
44
|
+
};
|
|
45
|
+
const bytesToPart = (media) => {
|
|
46
|
+
if (media.type.trim().length === 0)
|
|
47
|
+
throw new Error("AI media MIME type is required");
|
|
48
|
+
const part = {
|
|
49
|
+
type: mediaType(media.type),
|
|
50
|
+
mimeType: media.type,
|
|
51
|
+
data: Buffer.from(media.arraybuffer).toString("base64"),
|
|
52
|
+
};
|
|
53
|
+
return media.name === undefined ? part : { ...part, name: media.name };
|
|
54
|
+
};
|
|
55
|
+
const downloadURL = async (url, fetch, signal, timeoutMs) => {
|
|
56
|
+
const controller = new AbortController();
|
|
57
|
+
const downloadTimeout = timeoutMs === undefined || !Number.isFinite(timeoutMs)
|
|
58
|
+
? DOWNLOAD_TIMEOUT_MS
|
|
59
|
+
: Math.max(0, Math.min(DOWNLOAD_TIMEOUT_MS, timeoutMs));
|
|
60
|
+
const timeout = setTimeout(() => controller.abort(), downloadTimeout);
|
|
61
|
+
const onAbort = () => controller.abort();
|
|
62
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
63
|
+
try {
|
|
64
|
+
const response = await awaitWithSignal(fetch(url, { redirect: "follow", signal: controller.signal }), controller.signal);
|
|
65
|
+
if (!response.ok)
|
|
66
|
+
return undefined;
|
|
67
|
+
const contentLength = response.headers.get("content-length");
|
|
68
|
+
if (contentLength !== null && Number(contentLength) > MAX_DOWNLOAD_BYTES)
|
|
69
|
+
return undefined;
|
|
70
|
+
let type = response.headers.get("content-type") ?? "";
|
|
71
|
+
if (type.includes(";"))
|
|
72
|
+
type = type.split(";", 1)[0] ?? type;
|
|
73
|
+
if (type.length === 0 || type.includes("octet"))
|
|
74
|
+
return undefined;
|
|
75
|
+
const arraybuffer = await awaitWithSignal(response.arrayBuffer(), controller.signal);
|
|
76
|
+
if (arraybuffer.byteLength > MAX_DOWNLOAD_BYTES)
|
|
77
|
+
return undefined;
|
|
78
|
+
return { type, arraybuffer };
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (signal?.aborted)
|
|
82
|
+
throw error;
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
clearTimeout(timeout);
|
|
87
|
+
signal?.removeEventListener("abort", onAbort);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const downloadURLs = async (urls, fetch, signal, timeoutMs) => {
|
|
91
|
+
if (urls === undefined || urls.length === 0)
|
|
92
|
+
return [];
|
|
93
|
+
const images = await Promise.all(urls.map((url) => downloadURL(url, fetch, signal, timeoutMs)));
|
|
94
|
+
if (images.some((image) => image === undefined))
|
|
95
|
+
throw new Error("image not downloaded correctly");
|
|
96
|
+
return images.filter((image) => image !== undefined);
|
|
97
|
+
};
|
|
98
|
+
const textInput = (prompt, images) => ({
|
|
99
|
+
role: "user",
|
|
100
|
+
content: [{ type: "text", text: prompt }, ...images.map(bytesToPart)],
|
|
101
|
+
});
|
|
102
|
+
const textOutput = (output) => output.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("");
|
|
103
|
+
const mergeRequest = (defaults, request) => ({
|
|
104
|
+
...request,
|
|
105
|
+
instructions: request.instructions ?? defaults.instructions,
|
|
106
|
+
output: request.output ?? defaults.output,
|
|
107
|
+
endpoint: request.endpoint ?? defaults.endpoint,
|
|
108
|
+
model: request.model ?? defaults.model,
|
|
109
|
+
prefer: request.prefer ?? defaults.prefer,
|
|
110
|
+
reasoningEffort: request.reasoningEffort ?? defaults.reasoningEffort,
|
|
111
|
+
maxOutputTokens: request.maxOutputTokens ?? defaults.maxOutputTokens,
|
|
112
|
+
outputSchema: request.outputSchema ?? defaults.outputSchema,
|
|
113
|
+
webSearch: request.webSearch ?? defaults.webSearch,
|
|
114
|
+
onGenerated: request.onGenerated ?? defaults.onGenerated,
|
|
115
|
+
signal: request.signal ?? defaults.signal,
|
|
116
|
+
timeoutMs: request.timeoutMs ?? defaults.timeoutMs,
|
|
117
|
+
});
|
|
118
|
+
const requestInput = async (options, dependencies, signal, timeoutMs) => {
|
|
119
|
+
const images = options.images ?? [];
|
|
120
|
+
const downloaded = await downloadURLs(options.urls, dependencies.fetch ?? globalThis.fetch, signal, timeoutMs);
|
|
121
|
+
if (options.input !== undefined) {
|
|
122
|
+
if (options.prompt !== undefined || images.length > 0 || downloaded.length > 0) {
|
|
123
|
+
throw new Error("AI text options cannot combine input with prompt or media convenience fields");
|
|
124
|
+
}
|
|
125
|
+
return options.input;
|
|
126
|
+
}
|
|
127
|
+
if (options.prompt === undefined)
|
|
128
|
+
throw new Error("AI text prompt or multimodal input is required");
|
|
129
|
+
return [textInput(options.prompt, [...images, ...downloaded])];
|
|
130
|
+
};
|
|
131
|
+
const createAIClient = (defaults = {}) => {
|
|
132
|
+
const dependencies = getDependencies(defaults);
|
|
133
|
+
const generate = async (request) => {
|
|
134
|
+
const merged = mergeRequest(defaults, request);
|
|
135
|
+
const result = merged.endpoint === undefined
|
|
136
|
+
? await (0, google_1.generateGoogle)(merged, { dependencies })
|
|
137
|
+
: await (0, codex_1.generateCodex)(merged, {
|
|
138
|
+
endpoint: merged.endpoint,
|
|
139
|
+
dependencies,
|
|
140
|
+
timeoutMs: defaults.timeoutMs,
|
|
141
|
+
pollIntervalMs: defaults.pollIntervalMs,
|
|
142
|
+
maxPollIntervalMs: defaults.maxPollIntervalMs,
|
|
143
|
+
});
|
|
144
|
+
merged.onGenerated?.({ model: result.model, generatedAt: result.generatedAt });
|
|
145
|
+
return result;
|
|
146
|
+
};
|
|
147
|
+
async function generateText(promptOrOptions, options) {
|
|
148
|
+
const textOptions = typeof promptOrOptions === "string"
|
|
149
|
+
? { ...options, prompt: promptOrOptions }
|
|
150
|
+
: promptOrOptions;
|
|
151
|
+
const { prompt: _prompt, input: _input, images: _images, urls: _urls, ...requestOptions } = textOptions;
|
|
152
|
+
const input = await requestInput(textOptions, dependencies, textOptions.signal ?? defaults.signal, textOptions.timeoutMs ?? defaults.timeoutMs);
|
|
153
|
+
const result = await generate({
|
|
154
|
+
...requestOptions,
|
|
155
|
+
input,
|
|
156
|
+
output: requestOptions.output ?? ["text"],
|
|
157
|
+
});
|
|
158
|
+
return textOutput(result.output);
|
|
159
|
+
}
|
|
160
|
+
const promptDirect = async (options) => {
|
|
161
|
+
const { prompt, images = [], ...requestOptions } = options;
|
|
162
|
+
return generateText({ ...requestOptions, prompt, images });
|
|
163
|
+
};
|
|
164
|
+
const promptImage = async (options) => {
|
|
165
|
+
const { prompt, urls, images = [], ...requestOptions } = options;
|
|
166
|
+
const downloaded = await downloadURLs(urls, dependencies.fetch ?? globalThis.fetch, requestOptions.signal ?? defaults.signal, requestOptions.timeoutMs ?? defaults.timeoutMs);
|
|
167
|
+
return promptDirect({ ...requestOptions, prompt, images: [...images, ...downloaded] });
|
|
168
|
+
};
|
|
169
|
+
const models = async (options) => {
|
|
170
|
+
const endpoint = options?.endpoint ?? defaults.endpoint;
|
|
171
|
+
return endpoint === undefined
|
|
172
|
+
? (0, google_1.listGoogleModels)(dependencies)
|
|
173
|
+
: (0, codex_1.listCodexModels)(endpoint, dependencies);
|
|
174
|
+
};
|
|
175
|
+
return { generate, generateText, promptDirect, promptImage, models };
|
|
176
|
+
};
|
|
177
|
+
exports.createAIClient = createAIClient;
|
|
178
|
+
const defaultClient = (0, exports.createAIClient)();
|
|
179
|
+
const generate = (request) => defaultClient.generate(request);
|
|
180
|
+
exports.generate = generate;
|
|
181
|
+
function generateText(promptOrOptions, options) {
|
|
182
|
+
return typeof promptOrOptions === "string"
|
|
183
|
+
? defaultClient.generateText(promptOrOptions, options)
|
|
184
|
+
: defaultClient.generateText(promptOrOptions);
|
|
185
|
+
}
|
|
186
|
+
const promptDirect = (options) => defaultClient.promptDirect(options);
|
|
187
|
+
exports.promptDirect = promptDirect;
|
|
188
|
+
const promptImage = (options) => defaultClient.promptImage(options);
|
|
189
|
+
exports.promptImage = promptImage;
|
|
190
|
+
const models = (options) => defaultClient.models(options);
|
|
191
|
+
exports.models = models;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./client"), exports);
|
|
18
|
+
__exportStar(require("./quota"), exports);
|