@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-gemini-provider",
3
- "version": "1.0.7",
3
+ "version": "1.2.0",
4
4
  "description": "Google Gemini AI provider for React Native applications",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -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
- const result = await genModel.generateContent({
102
- contents: sdkContents as Parameters<typeof genModel.generateContent>[0] extends { contents: infer C } ? C : never,
103
- generationConfig,
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
- const response = result.response;
107
-
108
- return {
109
- candidates: response.candidates?.map((candidate) => ({
110
- content: {
111
- parts: candidate.content.parts
112
- .map((part): GeminiPart | null => {
113
- if ("text" in part && part.text !== undefined) {
114
- return { text: part.text };
115
- }
116
- if ("inlineData" in part && part.inlineData) {
117
- return {
118
- inlineData: {
119
- mimeType: part.inlineData.mimeType,
120
- data: part.inlineData.data,
121
- },
122
- };
123
- }
124
- return null;
125
- })
126
- .filter((p): p is GeminiPart => p !== null),
127
- role: (candidate.content.role || "model") as "user" | "model",
128
- },
129
- finishReason: candidate.finishReason as GeminiFinishReason | undefined,
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: base64Data,
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
- return this.executeGeneration<T>(model, input);
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
- // Check if this is an image generation request
229
- if (input.generateImage === true || input.type === "image") {
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: base64Data,
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 (input.outputFormat === "text") {
326
- return text as T;
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
- // Return full result with image data if available
367
+ // Build result object - always return { text } for consistency
330
368
  const result: Record<string, unknown> = {
331
369
  text,
332
370
  response,