@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.
- package/package.json +1 -1
- package/src/domain/entities/models.ts +12 -6
- package/src/index.ts +6 -1
- package/src/infrastructure/services/gemini-client-core.service.ts +83 -0
- package/src/infrastructure/services/gemini-image-edit.service.ts +134 -0
- package/src/infrastructure/services/gemini-image-generation.service.ts +113 -0
- package/src/infrastructure/services/gemini-provider.service.ts +13 -17
- package/src/infrastructure/services/gemini-retry.service.ts +63 -0
- package/src/infrastructure/services/gemini-streaming.service.ts +61 -0
- package/src/infrastructure/services/gemini-text-generation.service.ts +164 -0
- package/src/infrastructure/services/index.ts +9 -1
- package/src/infrastructure/utils/gemini-data-transformer.util.ts +42 -0
- package/src/infrastructure/utils/index.ts +5 -0
- package/src/presentation/hooks/use-gemini.ts +4 -4
- package/src/infrastructure/services/gemini-client.service.ts +0 -548
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini Text Generation Service
|
|
3
|
+
* Handles text and multimodal content generation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { geminiClientCoreService } from "./gemini-client-core.service";
|
|
7
|
+
import { geminiRetryService } from "./gemini-retry.service";
|
|
8
|
+
import { extractBase64Data, extractTextFromResponse } from "../utils/gemini-data-transformer.util";
|
|
9
|
+
import type {
|
|
10
|
+
GeminiContent,
|
|
11
|
+
GeminiGenerationConfig,
|
|
12
|
+
GeminiResponse,
|
|
13
|
+
GeminiPart,
|
|
14
|
+
GeminiFinishReason,
|
|
15
|
+
} from "../../domain/entities";
|
|
16
|
+
|
|
17
|
+
declare const __DEV__: boolean;
|
|
18
|
+
|
|
19
|
+
class GeminiTextGenerationService {
|
|
20
|
+
/**
|
|
21
|
+
* Generate content (text, with optional images)
|
|
22
|
+
*/
|
|
23
|
+
async generateContent(
|
|
24
|
+
model: string,
|
|
25
|
+
contents: GeminiContent[],
|
|
26
|
+
generationConfig?: GeminiGenerationConfig,
|
|
27
|
+
): Promise<GeminiResponse> {
|
|
28
|
+
const genModel = geminiClientCoreService.getModel(model);
|
|
29
|
+
|
|
30
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
31
|
+
// eslint-disable-next-line no-console
|
|
32
|
+
console.log("[Gemini] Generate content:", { model });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const sdkContents = contents.map((content) => ({
|
|
36
|
+
role: content.role || "user",
|
|
37
|
+
parts: content.parts.map((part) => {
|
|
38
|
+
if ("text" in part) {
|
|
39
|
+
return { text: part.text };
|
|
40
|
+
}
|
|
41
|
+
if ("inlineData" in part) {
|
|
42
|
+
return {
|
|
43
|
+
inlineData: {
|
|
44
|
+
mimeType: part.inlineData.mimeType,
|
|
45
|
+
data: part.inlineData.data,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return part;
|
|
50
|
+
}),
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const result = await geminiRetryService.executeWithRetry(() =>
|
|
55
|
+
genModel.generateContent({
|
|
56
|
+
contents: sdkContents as Parameters<typeof genModel.generateContent>[0] extends { contents: infer C } ? C : never,
|
|
57
|
+
generationConfig,
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const response = result.response;
|
|
62
|
+
|
|
63
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
64
|
+
// eslint-disable-next-line no-console
|
|
65
|
+
console.log("[Gemini] Content generated:", {
|
|
66
|
+
candidatesCount: response.candidates?.length ?? 0,
|
|
67
|
+
finishReason: response.candidates?.[0]?.finishReason,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
candidates: response.candidates?.map((candidate) => ({
|
|
73
|
+
content: {
|
|
74
|
+
parts: candidate.content.parts
|
|
75
|
+
.map((part): GeminiPart | null => {
|
|
76
|
+
if ("text" in part && part.text !== undefined) {
|
|
77
|
+
return { text: part.text };
|
|
78
|
+
}
|
|
79
|
+
if ("inlineData" in part && part.inlineData) {
|
|
80
|
+
return {
|
|
81
|
+
inlineData: {
|
|
82
|
+
mimeType: part.inlineData.mimeType,
|
|
83
|
+
data: part.inlineData.data,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
})
|
|
89
|
+
.filter((p): p is GeminiPart => p !== null),
|
|
90
|
+
role: (candidate.content.role || "model") as "user" | "model",
|
|
91
|
+
},
|
|
92
|
+
finishReason: candidate.finishReason as GeminiFinishReason | undefined,
|
|
93
|
+
})),
|
|
94
|
+
};
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
97
|
+
// eslint-disable-next-line no-console
|
|
98
|
+
console.error("[Gemini] Content generation failed:", {
|
|
99
|
+
model,
|
|
100
|
+
error: error instanceof Error ? error.message : String(error),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Generate text from prompt
|
|
109
|
+
*/
|
|
110
|
+
async generateText(
|
|
111
|
+
model: string,
|
|
112
|
+
prompt: string,
|
|
113
|
+
config?: GeminiGenerationConfig,
|
|
114
|
+
): Promise<string> {
|
|
115
|
+
const contents: GeminiContent[] = [
|
|
116
|
+
{ parts: [{ text: prompt }], role: "user" },
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
const response = await this.generateContent(model, contents, config);
|
|
120
|
+
return extractTextFromResponse(response);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Generate content with images (multimodal)
|
|
125
|
+
*/
|
|
126
|
+
async generateWithImages(
|
|
127
|
+
model: string,
|
|
128
|
+
prompt: string,
|
|
129
|
+
images: Array<{ base64: string; mimeType: string }>,
|
|
130
|
+
config?: GeminiGenerationConfig,
|
|
131
|
+
): Promise<GeminiResponse> {
|
|
132
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
133
|
+
// eslint-disable-next-line no-console
|
|
134
|
+
console.log("[GeminiClient] generateWithImages() called", {
|
|
135
|
+
model,
|
|
136
|
+
promptLength: prompt.length,
|
|
137
|
+
imagesCount: images.length,
|
|
138
|
+
imageMimeTypes: images.map(i => i.mimeType),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const parts: GeminiContent["parts"] = [{ text: prompt }];
|
|
143
|
+
|
|
144
|
+
for (const image of images) {
|
|
145
|
+
parts.push({
|
|
146
|
+
inlineData: {
|
|
147
|
+
mimeType: image.mimeType,
|
|
148
|
+
data: extractBase64Data(image.base64),
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const contents: GeminiContent[] = [{ parts, role: "user" }];
|
|
154
|
+
|
|
155
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
156
|
+
// eslint-disable-next-line no-console
|
|
157
|
+
console.log("[GeminiClient] generateWithImages() → calling generateContent()");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return this.generateContent(model, contents, config);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export const geminiTextGenerationService = new GeminiTextGenerationService();
|
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
* Infrastructure Services
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// Core services
|
|
6
|
+
export { geminiClientCoreService } from "./gemini-client-core.service";
|
|
7
|
+
export { geminiRetryService } from "./gemini-retry.service";
|
|
8
|
+
export { geminiTextGenerationService } from "./gemini-text-generation.service";
|
|
9
|
+
export { geminiImageGenerationService } from "./gemini-image-generation.service";
|
|
10
|
+
export { geminiImageEditService } from "./gemini-image-edit.service";
|
|
11
|
+
export { geminiStreamingService } from "./gemini-streaming.service";
|
|
12
|
+
|
|
13
|
+
// Public provider API
|
|
6
14
|
export {
|
|
7
15
|
geminiProviderService,
|
|
8
16
|
createGeminiProvider,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini Data Transformer Utility
|
|
3
|
+
* Handles data extraction and response parsing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { GeminiResponse } from "../../domain/entities";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extract base64 data from data URL or return as-is
|
|
10
|
+
*/
|
|
11
|
+
export function extractBase64Data(base64String: string): string {
|
|
12
|
+
if (!base64String.includes(",")) {
|
|
13
|
+
return base64String;
|
|
14
|
+
}
|
|
15
|
+
const parts = base64String.split(",");
|
|
16
|
+
return parts[1] ?? parts[0] ?? base64String;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Extract text from Gemini response
|
|
21
|
+
*/
|
|
22
|
+
export function extractTextFromResponse(response: GeminiResponse): string {
|
|
23
|
+
const candidate = response.candidates?.[0];
|
|
24
|
+
|
|
25
|
+
if (!candidate) {
|
|
26
|
+
throw new Error("No response candidates");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (candidate.finishReason === "SAFETY") {
|
|
30
|
+
throw new Error("Content blocked by safety filters");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const textPart = candidate.content.parts.find(
|
|
34
|
+
(p): p is { text: string } => "text" in p && typeof p.text === "string",
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (!textPart) {
|
|
38
|
+
throw new Error("No text in response");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return textPart.text;
|
|
42
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { useState, useCallback, useRef } from "react";
|
|
7
7
|
import type { GeminiGenerationConfig } from "../../domain/entities";
|
|
8
|
-
import {
|
|
8
|
+
import { geminiTextGenerationService } from "../../infrastructure/services";
|
|
9
9
|
|
|
10
10
|
export interface UseGeminiOptions {
|
|
11
11
|
model?: string;
|
|
@@ -43,7 +43,7 @@ export function useGemini(options: UseGeminiOptions = {}): UseGeminiReturn {
|
|
|
43
43
|
|
|
44
44
|
try {
|
|
45
45
|
const model = options.model ?? "gemini-1.5-flash";
|
|
46
|
-
const text = await
|
|
46
|
+
const text = await geminiTextGenerationService.generateText(
|
|
47
47
|
model,
|
|
48
48
|
prompt,
|
|
49
49
|
options.generationConfig,
|
|
@@ -78,7 +78,7 @@ export function useGemini(options: UseGeminiOptions = {}): UseGeminiReturn {
|
|
|
78
78
|
|
|
79
79
|
try {
|
|
80
80
|
const model = options.model ?? "gemini-1.5-flash";
|
|
81
|
-
const response = await
|
|
81
|
+
const response = await geminiTextGenerationService.generateWithImages(
|
|
82
82
|
model,
|
|
83
83
|
prompt,
|
|
84
84
|
[{ base64: imageBase64, mimeType }],
|
|
@@ -91,7 +91,7 @@ export function useGemini(options: UseGeminiOptions = {}): UseGeminiReturn {
|
|
|
91
91
|
const text =
|
|
92
92
|
response.candidates?.[0]?.content.parts
|
|
93
93
|
.filter((p): p is { text: string } => "text" in p)
|
|
94
|
-
.map((p) => p.text)
|
|
94
|
+
.map((p: { text: string }) => p.text)
|
|
95
95
|
.join("") || "";
|
|
96
96
|
|
|
97
97
|
setResult(text);
|