@umituz/react-native-ai-gemini-provider 1.0.7 → 1.2.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
|
@@ -25,6 +25,37 @@ const DEFAULT_CONFIG: Partial<GeminiConfig> = {
|
|
|
25
25
|
imageModel: "gemini-2.0-flash-exp",
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
const RETRYABLE_ERROR_PATTERNS = [
|
|
29
|
+
"rate limit",
|
|
30
|
+
"too many requests",
|
|
31
|
+
"429",
|
|
32
|
+
"500",
|
|
33
|
+
"502",
|
|
34
|
+
"503",
|
|
35
|
+
"504",
|
|
36
|
+
"timeout",
|
|
37
|
+
"network",
|
|
38
|
+
"econnrefused",
|
|
39
|
+
"fetch failed",
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function isRetryableError(error: unknown): boolean {
|
|
43
|
+
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
44
|
+
return RETRYABLE_ERROR_PATTERNS.some((pattern) => message.includes(pattern));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sleep(ms: number): Promise<void> {
|
|
48
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function extractBase64Data(base64String: string): string {
|
|
52
|
+
if (!base64String.includes(",")) {
|
|
53
|
+
return base64String;
|
|
54
|
+
}
|
|
55
|
+
const parts = base64String.split(",");
|
|
56
|
+
return parts[1] ?? parts[0] ?? base64String;
|
|
57
|
+
}
|
|
58
|
+
|
|
28
59
|
class GeminiClientService {
|
|
29
60
|
private client: GoogleGenerativeAI | null = null;
|
|
30
61
|
private config: GeminiConfig | null = null;
|
|
@@ -98,37 +129,58 @@ class GeminiClientService {
|
|
|
98
129
|
}),
|
|
99
130
|
}));
|
|
100
131
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
132
|
+
try {
|
|
133
|
+
const result = await this.executeWithRetry(() =>
|
|
134
|
+
genModel.generateContent({
|
|
135
|
+
contents: sdkContents as Parameters<typeof genModel.generateContent>[0] extends { contents: infer C } ? C : never,
|
|
136
|
+
generationConfig,
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
105
139
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
140
|
+
const response = result.response;
|
|
141
|
+
|
|
142
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
143
|
+
// eslint-disable-next-line no-console
|
|
144
|
+
console.log("[Gemini] Content generated:", {
|
|
145
|
+
candidatesCount: response.candidates?.length ?? 0,
|
|
146
|
+
finishReason: response.candidates?.[0]?.finishReason,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
candidates: response.candidates?.map((candidate) => ({
|
|
152
|
+
content: {
|
|
153
|
+
parts: candidate.content.parts
|
|
154
|
+
.map((part): GeminiPart | null => {
|
|
155
|
+
if ("text" in part && part.text !== undefined) {
|
|
156
|
+
return { text: part.text };
|
|
157
|
+
}
|
|
158
|
+
if ("inlineData" in part && part.inlineData) {
|
|
159
|
+
return {
|
|
160
|
+
inlineData: {
|
|
161
|
+
mimeType: part.inlineData.mimeType,
|
|
162
|
+
data: part.inlineData.data,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
})
|
|
168
|
+
.filter((p): p is GeminiPart => p !== null),
|
|
169
|
+
role: (candidate.content.role || "model") as "user" | "model",
|
|
170
|
+
},
|
|
171
|
+
finishReason: candidate.finishReason as GeminiFinishReason | undefined,
|
|
172
|
+
})),
|
|
173
|
+
};
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
176
|
+
// eslint-disable-next-line no-console
|
|
177
|
+
console.error("[Gemini] Content generation failed:", {
|
|
178
|
+
model,
|
|
179
|
+
error: error instanceof Error ? error.message : String(error),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
132
184
|
}
|
|
133
185
|
|
|
134
186
|
/**
|
|
@@ -159,15 +211,10 @@ class GeminiClientService {
|
|
|
159
211
|
const parts: GeminiContent["parts"] = [{ text: prompt }];
|
|
160
212
|
|
|
161
213
|
for (const image of images) {
|
|
162
|
-
// Remove data URL prefix if present
|
|
163
|
-
const base64Data = image.base64.includes(",")
|
|
164
|
-
? image.base64.split(",")[1]
|
|
165
|
-
: image.base64;
|
|
166
|
-
|
|
167
214
|
parts.push({
|
|
168
215
|
inlineData: {
|
|
169
216
|
mimeType: image.mimeType,
|
|
170
|
-
data:
|
|
217
|
+
data: extractBase64Data(image.base64),
|
|
171
218
|
},
|
|
172
219
|
});
|
|
173
220
|
}
|
|
@@ -287,6 +334,33 @@ class GeminiClientService {
|
|
|
287
334
|
return fullText;
|
|
288
335
|
}
|
|
289
336
|
|
|
337
|
+
private async executeWithRetry<T>(
|
|
338
|
+
operation: () => Promise<T>,
|
|
339
|
+
retryCount = 0,
|
|
340
|
+
): Promise<T> {
|
|
341
|
+
const maxRetries = this.config?.maxRetries ?? 3;
|
|
342
|
+
const baseDelay = this.config?.baseDelay ?? 1000;
|
|
343
|
+
const maxDelay = this.config?.maxDelay ?? 10000;
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
return await operation();
|
|
347
|
+
} catch (error) {
|
|
348
|
+
if (!isRetryableError(error) || retryCount >= maxRetries) {
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const delay = Math.min(baseDelay * Math.pow(2, retryCount), maxDelay);
|
|
353
|
+
|
|
354
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
355
|
+
// eslint-disable-next-line no-console
|
|
356
|
+
console.log(`[Gemini] Retry ${retryCount + 1}/${maxRetries} after ${delay}ms`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
await sleep(delay);
|
|
360
|
+
return this.executeWithRetry(operation, retryCount + 1);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
290
364
|
private extractTextFromResponse(response: GeminiResponse): string {
|
|
291
365
|
const candidate = response.candidates?.[0];
|
|
292
366
|
|
|
@@ -13,6 +13,14 @@ import { geminiClientService } from "./gemini-client.service";
|
|
|
13
13
|
|
|
14
14
|
declare const __DEV__: boolean;
|
|
15
15
|
|
|
16
|
+
function extractBase64Data(base64String: string): string {
|
|
17
|
+
if (!base64String.includes(",")) {
|
|
18
|
+
return base64String;
|
|
19
|
+
}
|
|
20
|
+
const parts = base64String.split(",");
|
|
21
|
+
return parts[1] ?? parts[0] ?? base64String;
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
export interface AIProviderConfig {
|
|
17
25
|
apiKey: string;
|
|
18
26
|
maxRetries?: number;
|
|
@@ -157,7 +165,26 @@ class GeminiProviderService {
|
|
|
157
165
|
model: string,
|
|
158
166
|
input: Record<string, unknown>,
|
|
159
167
|
): Promise<T> {
|
|
160
|
-
|
|
168
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
169
|
+
// eslint-disable-next-line no-console
|
|
170
|
+
console.log("[GeminiProvider] Run started:", {
|
|
171
|
+
model,
|
|
172
|
+
hasPrompt: !!input.prompt,
|
|
173
|
+
outputFormat: input.outputFormat,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const result = await this.executeGeneration<T>(model, input);
|
|
178
|
+
|
|
179
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
180
|
+
// eslint-disable-next-line no-console
|
|
181
|
+
console.log("[GeminiProvider] Run completed:", {
|
|
182
|
+
model,
|
|
183
|
+
hasResult: !!result,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return result;
|
|
161
188
|
}
|
|
162
189
|
|
|
163
190
|
/**
|
|
@@ -225,17 +252,25 @@ class GeminiProviderService {
|
|
|
225
252
|
model: string,
|
|
226
253
|
input: Record<string, unknown>,
|
|
227
254
|
): Promise<T> {
|
|
228
|
-
|
|
229
|
-
|
|
255
|
+
const isImageGeneration = input.generateImage === true || input.type === "image";
|
|
256
|
+
|
|
257
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
258
|
+
// eslint-disable-next-line no-console
|
|
259
|
+
console.log("[GeminiProvider] Execute generation:", {
|
|
260
|
+
model,
|
|
261
|
+
isImageGeneration,
|
|
262
|
+
promptLength: String(input.prompt || "").length,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (isImageGeneration) {
|
|
230
267
|
const prompt = String(input.prompt || "");
|
|
231
268
|
const images = input.images as GeminiImageInput[] | undefined;
|
|
232
269
|
const result = await geminiClientService.generateImage(prompt, images);
|
|
233
270
|
return result as T;
|
|
234
271
|
}
|
|
235
272
|
|
|
236
|
-
// Regular content generation
|
|
237
273
|
const contents = this.buildContents(input);
|
|
238
|
-
|
|
239
274
|
const response = await geminiClientService.generateContent(
|
|
240
275
|
model,
|
|
241
276
|
contents,
|
|
@@ -262,13 +297,10 @@ class GeminiProviderService {
|
|
|
262
297
|
// Handle multiple images
|
|
263
298
|
if (Array.isArray(input.images)) {
|
|
264
299
|
for (const img of input.images as GeminiImageInput[]) {
|
|
265
|
-
const base64Data = img.base64.includes(",")
|
|
266
|
-
? img.base64.split(",")[1]
|
|
267
|
-
: img.base64;
|
|
268
300
|
parts.push({
|
|
269
301
|
inlineData: {
|
|
270
302
|
mimeType: img.mimeType,
|
|
271
|
-
data:
|
|
303
|
+
data: extractBase64Data(img.base64),
|
|
272
304
|
},
|
|
273
305
|
});
|
|
274
306
|
}
|
|
@@ -322,11 +354,17 @@ class GeminiProviderService {
|
|
|
322
354
|
const imagePart = parts.find((p) => p.inlineData);
|
|
323
355
|
const imageData = imagePart?.inlineData;
|
|
324
356
|
|
|
325
|
-
if (
|
|
326
|
-
|
|
357
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
358
|
+
// eslint-disable-next-line no-console
|
|
359
|
+
console.log("[GeminiProvider] Format response:", {
|
|
360
|
+
hasText: !!text,
|
|
361
|
+
textLength: text?.length ?? 0,
|
|
362
|
+
hasImage: !!imageData,
|
|
363
|
+
outputFormat: input.outputFormat,
|
|
364
|
+
});
|
|
327
365
|
}
|
|
328
366
|
|
|
329
|
-
//
|
|
367
|
+
// Build result object - always return { text } for consistency
|
|
330
368
|
const result: Record<string, unknown> = {
|
|
331
369
|
text,
|
|
332
370
|
response,
|