@umituz/react-native-ai-gemini-provider 1.8.3 → 1.9.1

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,548 +0,0 @@
1
- /**
2
- * Gemini Client Service
3
- * Google Gemini AI client using official SDK
4
- */
5
-
6
- import { GoogleGenerativeAI, type GenerativeModel } from "@google/generative-ai";
7
- import { DEFAULT_MODELS } from "../../domain/entities";
8
- import type {
9
- GeminiConfig,
10
- GeminiContent,
11
- GeminiGenerationConfig,
12
- GeminiResponse,
13
- GeminiImageGenerationResult,
14
- GeminiPart,
15
- GeminiFinishReason,
16
- } from "../../domain/entities";
17
-
18
- declare const __DEV__: boolean;
19
-
20
- const DEFAULT_CONFIG: Partial<GeminiConfig> = {
21
- maxRetries: 3,
22
- baseDelay: 1000,
23
- maxDelay: 10000,
24
- defaultTimeoutMs: 60000,
25
- defaultModel: DEFAULT_MODELS.TEXT,
26
- imageModel: DEFAULT_MODELS.TEXT_TO_IMAGE,
27
- };
28
-
29
- const RETRYABLE_ERROR_PATTERNS = [
30
- "rate limit",
31
- "too many requests",
32
- "429",
33
- "500",
34
- "502",
35
- "503",
36
- "504",
37
- "timeout",
38
- "network",
39
- "econnrefused",
40
- "fetch failed",
41
- ];
42
-
43
- function isRetryableError(error: unknown): boolean {
44
- const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
45
- return RETRYABLE_ERROR_PATTERNS.some((pattern) => message.includes(pattern));
46
- }
47
-
48
- function sleep(ms: number): Promise<void> {
49
- return new Promise((resolve) => setTimeout(resolve, ms));
50
- }
51
-
52
- function extractBase64Data(base64String: string): string {
53
- if (!base64String.includes(",")) {
54
- return base64String;
55
- }
56
- const parts = base64String.split(",");
57
- return parts[1] ?? parts[0] ?? base64String;
58
- }
59
-
60
- class GeminiClientService {
61
- private client: GoogleGenerativeAI | null = null;
62
- private config: GeminiConfig | null = null;
63
- private initialized = false;
64
-
65
- initialize(config: GeminiConfig): void {
66
- if (typeof __DEV__ !== "undefined" && __DEV__) {
67
- // eslint-disable-next-line no-console
68
- console.log("[GeminiClient] initialize() called", {
69
- hasApiKey: !!config.apiKey,
70
- defaultModel: config.defaultModel,
71
- imageModel: config.imageModel,
72
- });
73
- }
74
-
75
- this.client = new GoogleGenerativeAI(config.apiKey);
76
- this.config = { ...DEFAULT_CONFIG, ...config };
77
- this.initialized = true;
78
-
79
- if (typeof __DEV__ !== "undefined" && __DEV__) {
80
- // eslint-disable-next-line no-console
81
- console.log("[GeminiClient] initialized successfully", {
82
- defaultModel: this.config.defaultModel,
83
- imageModel: this.config.imageModel,
84
- maxRetries: this.config.maxRetries,
85
- });
86
- }
87
- }
88
-
89
- isInitialized(): boolean {
90
- return this.initialized;
91
- }
92
-
93
- getConfig(): GeminiConfig | null {
94
- return this.config;
95
- }
96
-
97
- private validateInitialization(): void {
98
- if (!this.client || !this.initialized) {
99
- throw new Error(
100
- "Gemini client not initialized. Call initialize() first.",
101
- );
102
- }
103
- }
104
-
105
- private getModel(modelName?: string): GenerativeModel {
106
- this.validateInitialization();
107
- const effectiveModel = modelName || this.config?.defaultModel || "gemini-1.5-flash";
108
- return this.client!.getGenerativeModel({ model: effectiveModel });
109
- }
110
-
111
- /**
112
- * Generate content (text, with optional images)
113
- */
114
- async generateContent(
115
- model: string,
116
- contents: GeminiContent[],
117
- generationConfig?: GeminiGenerationConfig,
118
- ): Promise<GeminiResponse> {
119
- const genModel = this.getModel(model);
120
-
121
- if (typeof __DEV__ !== "undefined" && __DEV__) {
122
- // eslint-disable-next-line no-console
123
- console.log("[Gemini] Generate content:", { model });
124
- }
125
-
126
- // Convert our content format to SDK format
127
- const sdkContents = contents.map((content) => ({
128
- role: content.role || "user",
129
- parts: content.parts.map((part) => {
130
- if ("text" in part) {
131
- return { text: part.text };
132
- }
133
- if ("inlineData" in part) {
134
- return {
135
- inlineData: {
136
- mimeType: part.inlineData.mimeType,
137
- data: part.inlineData.data,
138
- },
139
- };
140
- }
141
- // fileData parts
142
- return part;
143
- }),
144
- }));
145
-
146
- try {
147
- const result = await this.executeWithRetry(() =>
148
- genModel.generateContent({
149
- contents: sdkContents as Parameters<typeof genModel.generateContent>[0] extends { contents: infer C } ? C : never,
150
- generationConfig,
151
- }),
152
- );
153
-
154
- const response = result.response;
155
-
156
- if (typeof __DEV__ !== "undefined" && __DEV__) {
157
- // eslint-disable-next-line no-console
158
- console.log("[Gemini] Content generated:", {
159
- candidatesCount: response.candidates?.length ?? 0,
160
- finishReason: response.candidates?.[0]?.finishReason,
161
- });
162
- }
163
-
164
- return {
165
- candidates: response.candidates?.map((candidate) => ({
166
- content: {
167
- parts: candidate.content.parts
168
- .map((part): GeminiPart | null => {
169
- if ("text" in part && part.text !== undefined) {
170
- return { text: part.text };
171
- }
172
- if ("inlineData" in part && part.inlineData) {
173
- return {
174
- inlineData: {
175
- mimeType: part.inlineData.mimeType,
176
- data: part.inlineData.data,
177
- },
178
- };
179
- }
180
- return null;
181
- })
182
- .filter((p): p is GeminiPart => p !== null),
183
- role: (candidate.content.role || "model") as "user" | "model",
184
- },
185
- finishReason: candidate.finishReason as GeminiFinishReason | undefined,
186
- })),
187
- };
188
- } catch (error) {
189
- if (typeof __DEV__ !== "undefined" && __DEV__) {
190
- // eslint-disable-next-line no-console
191
- console.error("[Gemini] Content generation failed:", {
192
- model,
193
- error: error instanceof Error ? error.message : String(error),
194
- });
195
- }
196
- throw error;
197
- }
198
- }
199
-
200
- /**
201
- * Generate text from prompt
202
- */
203
- async generateText(
204
- model: string,
205
- prompt: string,
206
- config?: GeminiGenerationConfig,
207
- ): Promise<string> {
208
- const contents: GeminiContent[] = [
209
- { parts: [{ text: prompt }], role: "user" },
210
- ];
211
-
212
- const response = await this.generateContent(model, contents, config);
213
- return this.extractTextFromResponse(response);
214
- }
215
-
216
- /**
217
- * Generate content with images (multimodal)
218
- */
219
- async generateWithImages(
220
- model: string,
221
- prompt: string,
222
- images: Array<{ base64: string; mimeType: string }>,
223
- config?: GeminiGenerationConfig,
224
- ): Promise<GeminiResponse> {
225
- if (typeof __DEV__ !== "undefined" && __DEV__) {
226
- // eslint-disable-next-line no-console
227
- console.log("[GeminiClient] generateWithImages() called", {
228
- model,
229
- promptLength: prompt.length,
230
- imagesCount: images.length,
231
- imageMimeTypes: images.map(i => i.mimeType),
232
- });
233
- }
234
-
235
- const parts: GeminiContent["parts"] = [{ text: prompt }];
236
-
237
- for (const image of images) {
238
- parts.push({
239
- inlineData: {
240
- mimeType: image.mimeType,
241
- data: extractBase64Data(image.base64),
242
- },
243
- });
244
- }
245
-
246
- const contents: GeminiContent[] = [{ parts, role: "user" }];
247
-
248
- if (typeof __DEV__ !== "undefined" && __DEV__) {
249
- // eslint-disable-next-line no-console
250
- console.log("[GeminiClient] generateWithImages() → calling generateContent()");
251
- }
252
-
253
- return this.generateContent(model, contents, config);
254
- }
255
-
256
- /**
257
- * Generate image from prompt using Imagen API
258
- * Uses REST API endpoint: /v1beta/models/{model}:predict
259
- */
260
- async generateImage(
261
- prompt: string,
262
- _images?: Array<{ base64: string; mimeType: string }>,
263
- _config?: GeminiGenerationConfig,
264
- ): Promise<GeminiImageGenerationResult> {
265
- this.validateInitialization();
266
-
267
- const imageModel = this.config?.imageModel || DEFAULT_MODELS.TEXT_TO_IMAGE;
268
- const apiKey = this.config?.apiKey;
269
-
270
- if (typeof __DEV__ !== "undefined" && __DEV__) {
271
- // eslint-disable-next-line no-console
272
- console.log("[GeminiClient] generateImage() called (Imagen API)", {
273
- model: imageModel,
274
- promptLength: prompt.length,
275
- });
276
- }
277
-
278
- const url = `https://generativelanguage.googleapis.com/v1beta/models/${imageModel}:predict`;
279
-
280
- const requestBody = {
281
- instances: [{ prompt }],
282
- parameters: {
283
- sampleCount: 1,
284
- aspectRatio: "1:1",
285
- },
286
- };
287
-
288
- if (typeof __DEV__ !== "undefined" && __DEV__) {
289
- // eslint-disable-next-line no-console
290
- console.log("[GeminiClient] Imagen API request", {
291
- url,
292
- prompt: prompt.substring(0, 100) + "...",
293
- });
294
- }
295
-
296
- const response = await this.executeWithRetry(async () => {
297
- const res = await fetch(url, {
298
- method: "POST",
299
- headers: {
300
- "Content-Type": "application/json",
301
- "x-goog-api-key": apiKey!,
302
- },
303
- body: JSON.stringify(requestBody),
304
- });
305
-
306
- if (!res.ok) {
307
- const errorText = await res.text();
308
- throw new Error(`Imagen API error (${res.status}): ${errorText}`);
309
- }
310
-
311
- return res.json();
312
- });
313
-
314
- const result: GeminiImageGenerationResult = {
315
- text: undefined,
316
- imageUrl: undefined,
317
- imageBase64: undefined,
318
- mimeType: "image/png",
319
- };
320
-
321
- if (response.generatedImages && response.generatedImages.length > 0) {
322
- const generatedImage = response.generatedImages[0];
323
- const imageBytes = generatedImage.image?.imageBytes;
324
-
325
- if (imageBytes) {
326
- result.imageBase64 = imageBytes;
327
- result.imageUrl = `data:image/png;base64,${imageBytes}`;
328
- }
329
- }
330
-
331
- if (typeof __DEV__ !== "undefined" && __DEV__) {
332
- // eslint-disable-next-line no-console
333
- console.log("[GeminiClient] generateImage() completed (Imagen)", {
334
- hasImage: !!result.imageBase64,
335
- imageDataLength: result.imageBase64?.length ?? 0,
336
- });
337
- }
338
-
339
- return result;
340
- }
341
-
342
- /**
343
- * Edit/transform image using Gemini generateContent API
344
- * Takes input image + prompt and generates new image
345
- */
346
- async editImage(
347
- prompt: string,
348
- images: Array<{ base64: string; mimeType: string }>,
349
- ): Promise<GeminiImageGenerationResult> {
350
- this.validateInitialization();
351
-
352
- const editModel = DEFAULT_MODELS.IMAGE_EDIT;
353
- const apiKey = this.config?.apiKey;
354
-
355
- if (typeof __DEV__ !== "undefined" && __DEV__) {
356
- // eslint-disable-next-line no-console
357
- console.log("[GeminiClient] editImage() called", {
358
- model: editModel,
359
- promptLength: prompt.length,
360
- imagesCount: images.length,
361
- });
362
- }
363
-
364
- const url = `https://generativelanguage.googleapis.com/v1beta/models/${editModel}:generateContent`;
365
-
366
- const parts: Array<Record<string, unknown>> = [];
367
-
368
- for (const image of images) {
369
- parts.push({
370
- inlineData: {
371
- mimeType: image.mimeType,
372
- data: extractBase64Data(image.base64),
373
- },
374
- });
375
- }
376
-
377
- parts.push({ text: prompt });
378
-
379
- const requestBody = {
380
- contents: [{ parts }],
381
- generationConfig: {
382
- responseModalities: ["TEXT", "IMAGE"],
383
- },
384
- };
385
-
386
- if (typeof __DEV__ !== "undefined" && __DEV__) {
387
- // eslint-disable-next-line no-console
388
- console.log("[GeminiClient] editImage() request", {
389
- url,
390
- partsCount: parts.length,
391
- });
392
- }
393
-
394
- const response = await this.executeWithRetry(async () => {
395
- const res = await fetch(url, {
396
- method: "POST",
397
- headers: {
398
- "Content-Type": "application/json",
399
- "x-goog-api-key": apiKey!,
400
- },
401
- body: JSON.stringify(requestBody),
402
- });
403
-
404
- if (!res.ok) {
405
- const errorText = await res.text();
406
- throw new Error(`Image edit API error (${res.status}): ${errorText}`);
407
- }
408
-
409
- return res.json();
410
- });
411
-
412
- const result: GeminiImageGenerationResult = {
413
- text: undefined,
414
- imageUrl: undefined,
415
- imageBase64: undefined,
416
- mimeType: "image/png",
417
- };
418
-
419
- const candidate = response.candidates?.[0];
420
- const responseParts = candidate?.content?.parts || [];
421
-
422
- for (const part of responseParts) {
423
- if (part.text) {
424
- result.text = part.text;
425
- }
426
- if (part.inlineData) {
427
- result.imageBase64 = part.inlineData.data;
428
- result.mimeType = part.inlineData.mimeType || "image/png";
429
- result.imageUrl = `data:${result.mimeType};base64,${result.imageBase64}`;
430
- }
431
- }
432
-
433
- if (typeof __DEV__ !== "undefined" && __DEV__) {
434
- // eslint-disable-next-line no-console
435
- console.log("[GeminiClient] editImage() completed", {
436
- hasImage: !!result.imageBase64,
437
- hasText: !!result.text,
438
- imageDataLength: result.imageBase64?.length ?? 0,
439
- });
440
- }
441
-
442
- return result;
443
- }
444
-
445
- /**
446
- * Stream content generation
447
- */
448
- async streamContent(
449
- model: string,
450
- contents: GeminiContent[],
451
- onChunk: (text: string) => void,
452
- generationConfig?: GeminiGenerationConfig,
453
- ): Promise<string> {
454
- const genModel = this.getModel(model);
455
-
456
- const sdkContents = contents.map((content) => ({
457
- role: content.role || "user",
458
- parts: content.parts.map((part) => {
459
- if ("text" in part) {
460
- return { text: part.text };
461
- }
462
- if ("inlineData" in part) {
463
- return {
464
- inlineData: {
465
- mimeType: part.inlineData.mimeType,
466
- data: part.inlineData.data,
467
- },
468
- };
469
- }
470
- return part;
471
- }),
472
- }));
473
-
474
- const result = await genModel.generateContentStream({
475
- contents: sdkContents as Parameters<typeof genModel.generateContentStream>[0] extends { contents: infer C } ? C : never,
476
- generationConfig,
477
- });
478
-
479
- let fullText = "";
480
-
481
- for await (const chunk of result.stream) {
482
- const chunkText = chunk.text();
483
- if (chunkText) {
484
- fullText += chunkText;
485
- onChunk(chunkText);
486
- }
487
- }
488
-
489
- return fullText;
490
- }
491
-
492
- private async executeWithRetry<T>(
493
- operation: () => Promise<T>,
494
- retryCount = 0,
495
- ): Promise<T> {
496
- const maxRetries = this.config?.maxRetries ?? 3;
497
- const baseDelay = this.config?.baseDelay ?? 1000;
498
- const maxDelay = this.config?.maxDelay ?? 10000;
499
-
500
- try {
501
- return await operation();
502
- } catch (error) {
503
- if (!isRetryableError(error) || retryCount >= maxRetries) {
504
- throw error;
505
- }
506
-
507
- const delay = Math.min(baseDelay * Math.pow(2, retryCount), maxDelay);
508
-
509
- if (typeof __DEV__ !== "undefined" && __DEV__) {
510
- // eslint-disable-next-line no-console
511
- console.log(`[Gemini] Retry ${retryCount + 1}/${maxRetries} after ${delay}ms`);
512
- }
513
-
514
- await sleep(delay);
515
- return this.executeWithRetry(operation, retryCount + 1);
516
- }
517
- }
518
-
519
- private extractTextFromResponse(response: GeminiResponse): string {
520
- const candidate = response.candidates?.[0];
521
-
522
- if (!candidate) {
523
- throw new Error("No response candidates");
524
- }
525
-
526
- if (candidate.finishReason === "SAFETY") {
527
- throw new Error("Content blocked by safety filters");
528
- }
529
-
530
- const textPart = candidate.content.parts.find(
531
- (p): p is { text: string } => "text" in p && typeof p.text === "string",
532
- );
533
-
534
- if (!textPart) {
535
- throw new Error("No text in response");
536
- }
537
-
538
- return textPart.text;
539
- }
540
-
541
- reset(): void {
542
- this.client = null;
543
- this.config = null;
544
- this.initialized = false;
545
- }
546
- }
547
-
548
- export const geminiClientService = new GeminiClientService();