@umituz/react-native-ai-gemini-provider 1.10.0 → 1.10.2
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 +8 -4
- package/src/domain/entities/gemini.types.ts +6 -3
- package/src/domain/entities/models.ts +6 -5
- package/src/infrastructure/content/ContentBuilder.ts +58 -0
- package/src/infrastructure/job/JobManager.ts +120 -0
- package/src/infrastructure/response/ResponseFormatter.ts +58 -0
- package/src/infrastructure/services/gemini-client-core.service.ts +10 -7
- package/src/infrastructure/services/gemini-image-edit.service.ts +1 -1
- package/src/infrastructure/services/gemini-image-generation.service.ts +1 -1
- package/src/infrastructure/services/gemini-provider.ts +201 -0
- package/src/infrastructure/services/index.ts +6 -4
- package/src/providers/ProviderFactory.ts +4 -15
- package/src/infrastructure/services/gemini-provider.service.ts +0 -426
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-gemini-provider",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.2",
|
|
4
4
|
"description": "Google Gemini AI provider for React Native applications",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"typecheck": "
|
|
9
|
-
"lint": "
|
|
8
|
+
"typecheck": "tsc --noEmit",
|
|
9
|
+
"lint": "eslint src --ext .ts,.tsx --max-warnings 0",
|
|
10
|
+
"lint:fix": "eslint src --ext .ts,.tsx --fix",
|
|
10
11
|
"version:patch": "npm version patch -m 'chore: release v%s'",
|
|
11
12
|
"version:minor": "npm version minor -m 'chore: release v%s'",
|
|
12
13
|
"version:major": "npm version major -m 'chore: release v%s'"
|
|
@@ -33,10 +34,13 @@
|
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
35
36
|
"@google/generative-ai": "^0.21.0",
|
|
37
|
+
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
|
38
|
+
"@typescript-eslint/parser": "^7.0.0",
|
|
39
|
+
"eslint": "^8.57.0",
|
|
36
40
|
"@types/react": "~19.1.10",
|
|
37
41
|
"react": "19.1.0",
|
|
38
42
|
"react-native": "0.81.5",
|
|
39
|
-
"typescript": "
|
|
43
|
+
"typescript": "^5.3.0"
|
|
40
44
|
},
|
|
41
45
|
"publishConfig": {
|
|
42
46
|
"access": "public"
|
|
@@ -10,9 +10,12 @@ export interface GeminiConfig {
|
|
|
10
10
|
baseDelay?: number;
|
|
11
11
|
maxDelay?: number;
|
|
12
12
|
defaultTimeoutMs?: number;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
/** Model used for text generation (default: gemini-2.5-flash-lite) */
|
|
14
|
+
textModel?: string;
|
|
15
|
+
/** Model used for text-to-image generation (default: imagen-4.0-generate-001) */
|
|
16
|
+
textToImageModel?: string;
|
|
17
|
+
/** Model used for image editing/transformation (default: gemini-2.5-flash-image) */
|
|
18
|
+
imageEditModel?: string;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
export interface GeminiGenerationConfig {
|
|
@@ -21,10 +21,10 @@ export const GEMINI_MODELS = {
|
|
|
21
21
|
},
|
|
22
22
|
|
|
23
23
|
// Image editing models - transforms/edits images with input image + prompt
|
|
24
|
-
// gemini-
|
|
24
|
+
// gemini-2.5-flash-image is the most cost-effective (500 images/day free tier)
|
|
25
25
|
IMAGE_EDIT: {
|
|
26
|
-
DEFAULT: "gemini-
|
|
27
|
-
|
|
26
|
+
DEFAULT: "gemini-2.5-flash-image",
|
|
27
|
+
HIGH_QUALITY: "gemini-3-pro-image-preview",
|
|
28
28
|
LEGACY: "gemini-2.0-flash-preview-image-generation",
|
|
29
29
|
},
|
|
30
30
|
|
|
@@ -36,11 +36,12 @@ export const GEMINI_MODELS = {
|
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Default models for each operation type
|
|
39
|
+
* Optimized for cost-effectiveness while maintaining quality
|
|
39
40
|
*/
|
|
40
41
|
export const DEFAULT_MODELS = {
|
|
41
|
-
TEXT: GEMINI_MODELS.TEXT.
|
|
42
|
+
TEXT: GEMINI_MODELS.TEXT.FLASH_LITE, // Most cost-effective for text
|
|
42
43
|
TEXT_TO_IMAGE: GEMINI_MODELS.TEXT_TO_IMAGE.DEFAULT,
|
|
43
|
-
IMAGE_EDIT: GEMINI_MODELS.IMAGE_EDIT.DEFAULT,
|
|
44
|
+
IMAGE_EDIT: GEMINI_MODELS.IMAGE_EDIT.DEFAULT, // Uses gemini-2.5-flash-image (free tier)
|
|
44
45
|
VIDEO: GEMINI_MODELS.VIDEO.FLASH,
|
|
45
46
|
} as const;
|
|
46
47
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Builder
|
|
3
|
+
* Constructs Gemini API content from various input formats
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { GeminiContent, GeminiImageInput } from "../../domain/entities";
|
|
7
|
+
import { extractBase64Data } from "../utils/gemini-data-transformer.util";
|
|
8
|
+
|
|
9
|
+
export class ContentBuilder {
|
|
10
|
+
buildContents(input: Record<string, unknown>): GeminiContent[] {
|
|
11
|
+
const contents: GeminiContent[] = [];
|
|
12
|
+
|
|
13
|
+
if (typeof input.prompt === "string") {
|
|
14
|
+
const parts: GeminiContent["parts"] = [{ text: input.prompt }];
|
|
15
|
+
|
|
16
|
+
// Handle single image
|
|
17
|
+
if (input.image_url && typeof input.image_url === "string") {
|
|
18
|
+
const imageData = this.parseImageUrl(input.image_url);
|
|
19
|
+
if (imageData) {
|
|
20
|
+
parts.push({ inlineData: imageData });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Handle multiple images
|
|
25
|
+
if (Array.isArray(input.images)) {
|
|
26
|
+
for (const img of input.images as GeminiImageInput[]) {
|
|
27
|
+
parts.push({
|
|
28
|
+
inlineData: {
|
|
29
|
+
mimeType: img.mimeType,
|
|
30
|
+
data: extractBase64Data(img.base64),
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
contents.push({ parts, role: "user" });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (Array.isArray(input.contents)) {
|
|
40
|
+
contents.push(...(input.contents as GeminiContent[]));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return contents;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private parseImageUrl(
|
|
47
|
+
imageUrl: string,
|
|
48
|
+
): { mimeType: string; data: string } | null {
|
|
49
|
+
const base64Match = imageUrl.match(/^data:([^;]+);base64,(.+)$/);
|
|
50
|
+
if (base64Match) {
|
|
51
|
+
return {
|
|
52
|
+
mimeType: base64Match[1],
|
|
53
|
+
data: base64Match[2],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job Manager
|
|
3
|
+
* Handles async job submission, tracking, and status management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
declare const __DEV__: boolean;
|
|
7
|
+
|
|
8
|
+
export interface JobSubmission {
|
|
9
|
+
requestId: string;
|
|
10
|
+
statusUrl?: string;
|
|
11
|
+
responseUrl?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface JobStatus {
|
|
15
|
+
status: "IN_QUEUE" | "IN_PROGRESS" | "COMPLETED" | "FAILED";
|
|
16
|
+
logs?: Array<{ message: string; level: string; timestamp?: string }>;
|
|
17
|
+
queuePosition?: number;
|
|
18
|
+
eta?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface PendingJob {
|
|
22
|
+
model: string;
|
|
23
|
+
input: Record<string, unknown>;
|
|
24
|
+
status: JobStatus["status"];
|
|
25
|
+
result?: unknown;
|
|
26
|
+
error?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class JobManager {
|
|
30
|
+
private pendingJobs: Map<string, PendingJob> = new Map();
|
|
31
|
+
private jobCounter = 0;
|
|
32
|
+
|
|
33
|
+
submitJob(model: string, input: Record<string, unknown>): JobSubmission {
|
|
34
|
+
const requestId = this.generateRequestId();
|
|
35
|
+
|
|
36
|
+
this.pendingJobs.set(requestId, {
|
|
37
|
+
model,
|
|
38
|
+
input,
|
|
39
|
+
status: "IN_QUEUE",
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
43
|
+
// eslint-disable-next-line no-console
|
|
44
|
+
console.log("[JobManager] Job submitted:", { requestId, model });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
requestId,
|
|
49
|
+
statusUrl: undefined,
|
|
50
|
+
responseUrl: undefined,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getJobStatus(requestId: string): JobStatus {
|
|
55
|
+
const job = this.pendingJobs.get(requestId);
|
|
56
|
+
|
|
57
|
+
if (!job) {
|
|
58
|
+
return { status: "FAILED" };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { status: job.status };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
getJobResult<T = unknown>(requestId: string): T {
|
|
65
|
+
const job = this.pendingJobs.get(requestId);
|
|
66
|
+
|
|
67
|
+
if (!job) {
|
|
68
|
+
throw new Error(`Job ${requestId} not found`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (job.status !== "COMPLETED") {
|
|
72
|
+
throw new Error(`Job ${requestId} not completed`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (job.error) {
|
|
76
|
+
throw new Error(job.error);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.pendingJobs.delete(requestId);
|
|
80
|
+
|
|
81
|
+
return job.result as T;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
updateJobStatus(requestId: string, status: JobStatus["status"]): void {
|
|
85
|
+
const job = this.pendingJobs.get(requestId);
|
|
86
|
+
if (job) {
|
|
87
|
+
job.status = status;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
setJobResult(requestId: string, result: unknown): void {
|
|
92
|
+
const job = this.pendingJobs.get(requestId);
|
|
93
|
+
if (job) {
|
|
94
|
+
job.result = result;
|
|
95
|
+
job.status = "COMPLETED";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
setJobError(requestId: string, error: string): void {
|
|
100
|
+
const job = this.pendingJobs.get(requestId);
|
|
101
|
+
if (job) {
|
|
102
|
+
job.error = error;
|
|
103
|
+
job.status = "FAILED";
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getJob(requestId: string): PendingJob | undefined {
|
|
108
|
+
return this.pendingJobs.get(requestId);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
clear(): void {
|
|
112
|
+
this.pendingJobs.clear();
|
|
113
|
+
this.jobCounter = 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private generateRequestId(): string {
|
|
117
|
+
this.jobCounter++;
|
|
118
|
+
return `gemini-${Date.now()}-${this.jobCounter}`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response Formatter
|
|
3
|
+
* Formats Gemini API responses into consistent output structure
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
declare const __DEV__: boolean;
|
|
7
|
+
|
|
8
|
+
export class ResponseFormatter {
|
|
9
|
+
formatResponse<T>(
|
|
10
|
+
response: unknown,
|
|
11
|
+
input: Record<string, unknown>,
|
|
12
|
+
): T {
|
|
13
|
+
const resp = response as {
|
|
14
|
+
candidates?: Array<{
|
|
15
|
+
content: {
|
|
16
|
+
parts: Array<{
|
|
17
|
+
text?: string;
|
|
18
|
+
inlineData?: { mimeType: string; data: string };
|
|
19
|
+
}>;
|
|
20
|
+
};
|
|
21
|
+
}>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const candidate = resp.candidates?.[0];
|
|
25
|
+
const parts = candidate?.content.parts || [];
|
|
26
|
+
|
|
27
|
+
// Extract text
|
|
28
|
+
const text = parts.find((p) => p.text)?.text;
|
|
29
|
+
|
|
30
|
+
// Extract image if present
|
|
31
|
+
const imagePart = parts.find((p) => p.inlineData);
|
|
32
|
+
const imageData = imagePart?.inlineData;
|
|
33
|
+
|
|
34
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
35
|
+
// eslint-disable-next-line no-console
|
|
36
|
+
console.log("[ResponseFormatter] Formatting response:", {
|
|
37
|
+
hasText: !!text,
|
|
38
|
+
textLength: text?.length ?? 0,
|
|
39
|
+
hasImage: !!imageData,
|
|
40
|
+
outputFormat: input.outputFormat,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Build result object - always return { text } for consistency
|
|
45
|
+
const result: Record<string, unknown> = {
|
|
46
|
+
text,
|
|
47
|
+
response,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
if (imageData) {
|
|
51
|
+
result.imageUrl = `data:${imageData.mimeType};base64,${imageData.data}`;
|
|
52
|
+
result.imageBase64 = imageData.data;
|
|
53
|
+
result.mimeType = imageData.mimeType;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return result as T;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -14,8 +14,9 @@ const DEFAULT_CONFIG: Partial<GeminiConfig> = {
|
|
|
14
14
|
baseDelay: 1000,
|
|
15
15
|
maxDelay: 10000,
|
|
16
16
|
defaultTimeoutMs: 60000,
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
textModel: DEFAULT_MODELS.TEXT,
|
|
18
|
+
textToImageModel: DEFAULT_MODELS.TEXT_TO_IMAGE,
|
|
19
|
+
imageEditModel: DEFAULT_MODELS.IMAGE_EDIT,
|
|
19
20
|
};
|
|
20
21
|
|
|
21
22
|
class GeminiClientCoreService {
|
|
@@ -28,8 +29,9 @@ class GeminiClientCoreService {
|
|
|
28
29
|
// eslint-disable-next-line no-console
|
|
29
30
|
console.log("[GeminiClient] initialize() called", {
|
|
30
31
|
hasApiKey: !!config.apiKey,
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
textModel: config.textModel,
|
|
33
|
+
textToImageModel: config.textToImageModel,
|
|
34
|
+
imageEditModel: config.imageEditModel,
|
|
33
35
|
});
|
|
34
36
|
}
|
|
35
37
|
|
|
@@ -40,8 +42,9 @@ class GeminiClientCoreService {
|
|
|
40
42
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
41
43
|
// eslint-disable-next-line no-console
|
|
42
44
|
console.log("[GeminiClient] initialized successfully", {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
textModel: this.config.textModel,
|
|
46
|
+
textToImageModel: this.config.textToImageModel,
|
|
47
|
+
imageEditModel: this.config.imageEditModel,
|
|
45
48
|
maxRetries: this.config.maxRetries,
|
|
46
49
|
});
|
|
47
50
|
}
|
|
@@ -69,7 +72,7 @@ class GeminiClientCoreService {
|
|
|
69
72
|
|
|
70
73
|
getModel(modelName?: string): GenerativeModel {
|
|
71
74
|
this.validateInitialization();
|
|
72
|
-
const effectiveModel = modelName || this.config?.
|
|
75
|
+
const effectiveModel = modelName || this.config?.textModel || DEFAULT_MODELS.TEXT;
|
|
73
76
|
return this.client!.getGenerativeModel({ model: effectiveModel });
|
|
74
77
|
}
|
|
75
78
|
|
|
@@ -37,7 +37,7 @@ class GeminiImageEditService {
|
|
|
37
37
|
geminiClientCoreService.validateInitialization();
|
|
38
38
|
|
|
39
39
|
const config = geminiClientCoreService.getConfig();
|
|
40
|
-
const editModel = DEFAULT_MODELS.IMAGE_EDIT;
|
|
40
|
+
const editModel = config?.imageEditModel || DEFAULT_MODELS.IMAGE_EDIT;
|
|
41
41
|
const apiKey = config?.apiKey;
|
|
42
42
|
|
|
43
43
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
@@ -34,7 +34,7 @@ class GeminiImageGenerationService {
|
|
|
34
34
|
geminiClientCoreService.validateInitialization();
|
|
35
35
|
|
|
36
36
|
const config = geminiClientCoreService.getConfig();
|
|
37
|
-
const imageModel = config?.
|
|
37
|
+
const imageModel = config?.textToImageModel || DEFAULT_MODELS.TEXT_TO_IMAGE;
|
|
38
38
|
const apiKey = config?.apiKey;
|
|
39
39
|
|
|
40
40
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini Provider
|
|
3
|
+
* Main AI provider implementation for Google Gemini
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
GeminiConfig,
|
|
8
|
+
GeminiImageInput,
|
|
9
|
+
GeminiImageGenerationResult,
|
|
10
|
+
} from "../../domain/entities";
|
|
11
|
+
import { geminiClientCoreService } from "./gemini-client-core.service";
|
|
12
|
+
import { geminiTextGenerationService } from "./gemini-text-generation.service";
|
|
13
|
+
import { geminiImageGenerationService } from "./gemini-image-generation.service";
|
|
14
|
+
import { geminiImageEditService } from "./gemini-image-edit.service";
|
|
15
|
+
import { JobManager } from "../job/JobManager";
|
|
16
|
+
import { ContentBuilder } from "../content/ContentBuilder";
|
|
17
|
+
import { ResponseFormatter } from "../response/ResponseFormatter";
|
|
18
|
+
import type { JobSubmission, JobStatus } from "../job/JobManager";
|
|
19
|
+
|
|
20
|
+
declare const __DEV__: boolean;
|
|
21
|
+
|
|
22
|
+
export interface AIProviderConfig {
|
|
23
|
+
apiKey: string;
|
|
24
|
+
maxRetries?: number;
|
|
25
|
+
baseDelay?: number;
|
|
26
|
+
maxDelay?: number;
|
|
27
|
+
defaultTimeoutMs?: number;
|
|
28
|
+
textModel?: string;
|
|
29
|
+
textToImageModel?: string;
|
|
30
|
+
imageEditModel?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SubscribeOptions<T = unknown> {
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
onQueueUpdate?: (status: JobStatus) => void;
|
|
36
|
+
onProgress?: (progress: number) => void;
|
|
37
|
+
onResult?: (result: T) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class GeminiProvider {
|
|
41
|
+
readonly providerId = "gemini";
|
|
42
|
+
readonly providerName = "Google Gemini";
|
|
43
|
+
|
|
44
|
+
private jobManager = new JobManager();
|
|
45
|
+
private contentBuilder = new ContentBuilder();
|
|
46
|
+
private responseFormatter = new ResponseFormatter();
|
|
47
|
+
|
|
48
|
+
initialize(config: AIProviderConfig): void {
|
|
49
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
50
|
+
// eslint-disable-next-line no-console
|
|
51
|
+
console.log("[GeminiProvider] Initializing...");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const geminiConfig: GeminiConfig = {
|
|
55
|
+
apiKey: config.apiKey,
|
|
56
|
+
maxRetries: config.maxRetries,
|
|
57
|
+
baseDelay: config.baseDelay,
|
|
58
|
+
maxDelay: config.maxDelay,
|
|
59
|
+
defaultTimeoutMs: config.defaultTimeoutMs,
|
|
60
|
+
textModel: config.textModel,
|
|
61
|
+
textToImageModel: config.textToImageModel,
|
|
62
|
+
imageEditModel: config.imageEditModel,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
geminiClientCoreService.initialize(geminiConfig);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
isInitialized(): boolean {
|
|
69
|
+
return geminiClientCoreService.isInitialized();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
submitJob(model: string, input: Record<string, unknown>): Promise<JobSubmission> {
|
|
73
|
+
const submission = this.jobManager.submitJob(model, input);
|
|
74
|
+
|
|
75
|
+
this.processJobAsync(submission.requestId).catch((error) => {
|
|
76
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
77
|
+
// eslint-disable-next-line no-console
|
|
78
|
+
console.error("[GeminiProvider] Job failed:", error);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return Promise.resolve(submission);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getJobStatus(_model: string, requestId: string): Promise<JobStatus> {
|
|
86
|
+
const status = this.jobManager.getJobStatus(requestId);
|
|
87
|
+
return Promise.resolve(status);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getJobResult<T = unknown>(_model: string, requestId: string): Promise<T> {
|
|
91
|
+
try {
|
|
92
|
+
const result = this.jobManager.getJobResult<T>(requestId);
|
|
93
|
+
return Promise.resolve(result);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
return Promise.reject(error);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async subscribe<T = unknown>(
|
|
100
|
+
model: string,
|
|
101
|
+
input: Record<string, unknown>,
|
|
102
|
+
options?: SubscribeOptions<T>,
|
|
103
|
+
): Promise<T> {
|
|
104
|
+
options?.onQueueUpdate?.({ status: "IN_QUEUE" });
|
|
105
|
+
options?.onProgress?.(10);
|
|
106
|
+
|
|
107
|
+
const result = await this.executeGeneration<T>(model, input);
|
|
108
|
+
|
|
109
|
+
options?.onProgress?.(100);
|
|
110
|
+
options?.onQueueUpdate?.({ status: "COMPLETED" });
|
|
111
|
+
options?.onResult?.(result);
|
|
112
|
+
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async run<T = unknown>(
|
|
117
|
+
model: string,
|
|
118
|
+
input: Record<string, unknown>,
|
|
119
|
+
): Promise<T> {
|
|
120
|
+
return this.executeGeneration<T>(model, input);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async generateImage(prompt: string): Promise<GeminiImageGenerationResult> {
|
|
124
|
+
return geminiImageGenerationService.generateImage(prompt);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async editImage(
|
|
128
|
+
prompt: string,
|
|
129
|
+
images: GeminiImageInput[],
|
|
130
|
+
): Promise<GeminiImageGenerationResult> {
|
|
131
|
+
return geminiImageEditService.editImage(prompt, images);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async generateWithImages(
|
|
135
|
+
model: string,
|
|
136
|
+
prompt: string,
|
|
137
|
+
images: GeminiImageInput[],
|
|
138
|
+
): Promise<{ text: string; response: unknown }> {
|
|
139
|
+
const response = await geminiTextGenerationService.generateWithImages(
|
|
140
|
+
model,
|
|
141
|
+
prompt,
|
|
142
|
+
images,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const text = response.candidates?.[0]?.content.parts
|
|
146
|
+
.filter((p): p is { text: string } => "text" in p)
|
|
147
|
+
.map((p) => p.text)
|
|
148
|
+
.join("") || "";
|
|
149
|
+
|
|
150
|
+
return { text, response };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
reset(): void {
|
|
154
|
+
geminiClientCoreService.reset();
|
|
155
|
+
this.jobManager.clear();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private async processJobAsync(requestId: string): Promise<void> {
|
|
159
|
+
const job = this.jobManager.getJob(requestId);
|
|
160
|
+
if (!job) return;
|
|
161
|
+
|
|
162
|
+
this.jobManager.updateJobStatus(requestId, "IN_PROGRESS");
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const result = await this.executeGeneration(job.model, job.input);
|
|
166
|
+
this.jobManager.setJobResult(requestId, result);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
169
|
+
this.jobManager.setJobError(requestId, errorMessage);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private async executeGeneration<T>(
|
|
174
|
+
model: string,
|
|
175
|
+
input: Record<string, unknown>,
|
|
176
|
+
): Promise<T> {
|
|
177
|
+
const isImageGeneration = input.generateImage === true || input.type === "image";
|
|
178
|
+
|
|
179
|
+
if (isImageGeneration) {
|
|
180
|
+
const prompt = String(input.prompt || "");
|
|
181
|
+
const images = input.images as GeminiImageInput[] | undefined;
|
|
182
|
+
const result = await geminiImageGenerationService.generateImage(prompt, images);
|
|
183
|
+
return result as T;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const contents = this.contentBuilder.buildContents(input);
|
|
187
|
+
const response = await geminiTextGenerationService.generateContent(
|
|
188
|
+
model,
|
|
189
|
+
contents,
|
|
190
|
+
input.generationConfig as undefined,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
return this.responseFormatter.formatResponse<T>(response, input);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export const geminiProviderService = new GeminiProvider();
|
|
198
|
+
|
|
199
|
+
export function createGeminiProvider(): GeminiProvider {
|
|
200
|
+
return new GeminiProvider();
|
|
201
|
+
}
|
|
@@ -14,10 +14,12 @@ export { geminiStreamingService } from "./gemini-streaming.service";
|
|
|
14
14
|
export {
|
|
15
15
|
geminiProviderService,
|
|
16
16
|
createGeminiProvider,
|
|
17
|
-
|
|
17
|
+
GeminiProvider,
|
|
18
|
+
} from "./gemini-provider";
|
|
19
|
+
|
|
18
20
|
export type {
|
|
19
21
|
AIProviderConfig,
|
|
20
|
-
JobSubmission,
|
|
21
|
-
JobStatus,
|
|
22
22
|
SubscribeOptions,
|
|
23
|
-
} from "./gemini-provider
|
|
23
|
+
} from "./gemini-provider";
|
|
24
|
+
|
|
25
|
+
export type { JobSubmission, JobStatus } from "../job/JobManager";
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
getQualityOptimizedConfig,
|
|
16
16
|
} from "./ProviderConfig";
|
|
17
17
|
|
|
18
|
-
declare const __DEV__: boolean;
|
|
19
18
|
|
|
20
19
|
export type OptimizationStrategy = "cost" | "quality" | "balanced";
|
|
21
20
|
|
|
@@ -49,32 +48,22 @@ class ProviderFactory {
|
|
|
49
48
|
|
|
50
49
|
this.currentConfig = config;
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
console.log("[ProviderFactory] Initializing with config:", {
|
|
54
|
-
tier: config.subscriptionTier,
|
|
55
|
-
strategy: options.strategy || "balanced",
|
|
56
|
-
textModel: config.textModel,
|
|
57
|
-
imageEditModel: config.imageEditModel,
|
|
58
|
-
maxRetries: config.maxRetries,
|
|
59
|
-
});
|
|
60
|
-
}
|
|
51
|
+
this.currentConfig = config;
|
|
61
52
|
|
|
62
53
|
// Initialize Gemini client with resolved config
|
|
63
54
|
const geminiConfig: GeminiConfig = {
|
|
64
55
|
apiKey: config.apiKey,
|
|
65
|
-
|
|
56
|
+
imageEditModel: config.imageEditModel,
|
|
66
57
|
maxRetries: config.maxRetries,
|
|
67
58
|
baseDelay: config.baseDelay,
|
|
68
59
|
maxDelay: config.maxDelay,
|
|
69
60
|
defaultTimeoutMs: config.timeout,
|
|
70
|
-
|
|
61
|
+
textModel: config.textModel,
|
|
71
62
|
};
|
|
72
63
|
|
|
73
64
|
geminiClientCoreService.initialize(geminiConfig);
|
|
74
65
|
|
|
75
|
-
|
|
76
|
-
console.log("[ProviderFactory] Provider initialized successfully");
|
|
77
|
-
}
|
|
66
|
+
geminiClientCoreService.initialize(geminiConfig);
|
|
78
67
|
}
|
|
79
68
|
|
|
80
69
|
/**
|
|
@@ -1,426 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Gemini Provider Service
|
|
3
|
-
* IAIProvider implementation for Google Gemini
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type {
|
|
7
|
-
GeminiConfig,
|
|
8
|
-
GeminiContent,
|
|
9
|
-
GeminiImageInput,
|
|
10
|
-
GeminiImageGenerationResult,
|
|
11
|
-
} from "../../domain/entities";
|
|
12
|
-
import { geminiClientCoreService } from "./gemini-client-core.service";
|
|
13
|
-
import { geminiTextGenerationService } from "./gemini-text-generation.service";
|
|
14
|
-
import { geminiImageGenerationService } from "./gemini-image-generation.service";
|
|
15
|
-
import { geminiImageEditService } from "./gemini-image-edit.service";
|
|
16
|
-
import { extractBase64Data } from "../utils/gemini-data-transformer.util";
|
|
17
|
-
|
|
18
|
-
declare const __DEV__: boolean;
|
|
19
|
-
|
|
20
|
-
export interface AIProviderConfig {
|
|
21
|
-
apiKey: string;
|
|
22
|
-
maxRetries?: number;
|
|
23
|
-
baseDelay?: number;
|
|
24
|
-
maxDelay?: number;
|
|
25
|
-
defaultTimeoutMs?: number;
|
|
26
|
-
/** Model used for image generation */
|
|
27
|
-
imageModel?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface JobSubmission {
|
|
31
|
-
requestId: string;
|
|
32
|
-
statusUrl?: string;
|
|
33
|
-
responseUrl?: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface JobStatus {
|
|
37
|
-
status: "IN_QUEUE" | "IN_PROGRESS" | "COMPLETED" | "FAILED";
|
|
38
|
-
logs?: Array<{ message: string; level: string; timestamp?: string }>;
|
|
39
|
-
queuePosition?: number;
|
|
40
|
-
eta?: number;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface SubscribeOptions<T = unknown> {
|
|
44
|
-
timeoutMs?: number;
|
|
45
|
-
onQueueUpdate?: (status: JobStatus) => void;
|
|
46
|
-
onProgress?: (progress: number) => void;
|
|
47
|
-
onResult?: (result: T) => void;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
interface PendingJob {
|
|
51
|
-
model: string;
|
|
52
|
-
input: Record<string, unknown>;
|
|
53
|
-
status: JobStatus["status"];
|
|
54
|
-
result?: unknown;
|
|
55
|
-
error?: string;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
class GeminiProviderService {
|
|
59
|
-
readonly providerId = "gemini";
|
|
60
|
-
readonly providerName = "Google Gemini";
|
|
61
|
-
|
|
62
|
-
private pendingJobs: Map<string, PendingJob> = new Map();
|
|
63
|
-
private jobCounter = 0;
|
|
64
|
-
|
|
65
|
-
initialize(config: AIProviderConfig): void {
|
|
66
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
67
|
-
// eslint-disable-next-line no-console
|
|
68
|
-
console.log("[GeminiProvider] initialize() called", {
|
|
69
|
-
hasApiKey: !!config.apiKey,
|
|
70
|
-
imageModel: config.imageModel,
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const geminiConfig: GeminiConfig = {
|
|
75
|
-
apiKey: config.apiKey,
|
|
76
|
-
maxRetries: config.maxRetries,
|
|
77
|
-
baseDelay: config.baseDelay,
|
|
78
|
-
maxDelay: config.maxDelay,
|
|
79
|
-
defaultTimeoutMs: config.defaultTimeoutMs,
|
|
80
|
-
imageModel: config.imageModel,
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
geminiClientCoreService.initialize(geminiConfig);
|
|
84
|
-
|
|
85
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
86
|
-
// eslint-disable-next-line no-console
|
|
87
|
-
console.log("[GeminiProvider] initialized successfully");
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
isInitialized(): boolean {
|
|
92
|
-
return geminiClientCoreService.isInitialized();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
submitJob(
|
|
96
|
-
model: string,
|
|
97
|
-
input: Record<string, unknown>,
|
|
98
|
-
): Promise<JobSubmission> {
|
|
99
|
-
const requestId = this.generateRequestId();
|
|
100
|
-
|
|
101
|
-
this.pendingJobs.set(requestId, {
|
|
102
|
-
model,
|
|
103
|
-
input,
|
|
104
|
-
status: "IN_QUEUE",
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
108
|
-
// eslint-disable-next-line no-console
|
|
109
|
-
console.log("[GeminiProvider] Job submitted:", { requestId, model });
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
this.processJobAsync(requestId).catch((error) => {
|
|
113
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
114
|
-
// eslint-disable-next-line no-console
|
|
115
|
-
console.error("[GeminiProvider] Job failed:", error);
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
return Promise.resolve({
|
|
120
|
-
requestId,
|
|
121
|
-
statusUrl: undefined,
|
|
122
|
-
responseUrl: undefined,
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
getJobStatus(_model: string, requestId: string): Promise<JobStatus> {
|
|
127
|
-
const job = this.pendingJobs.get(requestId);
|
|
128
|
-
|
|
129
|
-
if (!job) {
|
|
130
|
-
return Promise.resolve({ status: "FAILED" });
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return Promise.resolve({ status: job.status });
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
getJobResult<T = unknown>(_model: string, requestId: string): Promise<T> {
|
|
137
|
-
const job = this.pendingJobs.get(requestId);
|
|
138
|
-
|
|
139
|
-
if (!job) {
|
|
140
|
-
return Promise.reject(new Error(`Job ${requestId} not found`));
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (job.status !== "COMPLETED") {
|
|
144
|
-
return Promise.reject(new Error(`Job ${requestId} not completed`));
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (job.error) {
|
|
148
|
-
return Promise.reject(new Error(job.error));
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
this.pendingJobs.delete(requestId);
|
|
152
|
-
|
|
153
|
-
return Promise.resolve(job.result as T);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
async subscribe<T = unknown>(
|
|
157
|
-
model: string,
|
|
158
|
-
input: Record<string, unknown>,
|
|
159
|
-
options?: SubscribeOptions<T>,
|
|
160
|
-
): Promise<T> {
|
|
161
|
-
options?.onQueueUpdate?.({ status: "IN_QUEUE" });
|
|
162
|
-
options?.onProgress?.(10);
|
|
163
|
-
|
|
164
|
-
const result = await this.executeGeneration<T>(model, input);
|
|
165
|
-
|
|
166
|
-
options?.onProgress?.(100);
|
|
167
|
-
options?.onQueueUpdate?.({ status: "COMPLETED" });
|
|
168
|
-
options?.onResult?.(result);
|
|
169
|
-
|
|
170
|
-
return result;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
async run<T = unknown>(
|
|
174
|
-
model: string,
|
|
175
|
-
input: Record<string, unknown>,
|
|
176
|
-
): Promise<T> {
|
|
177
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
178
|
-
// eslint-disable-next-line no-console
|
|
179
|
-
console.log("[GeminiProvider] Run started:", {
|
|
180
|
-
model,
|
|
181
|
-
hasPrompt: !!input.prompt,
|
|
182
|
-
outputFormat: input.outputFormat,
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const result = await this.executeGeneration<T>(model, input);
|
|
187
|
-
|
|
188
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
189
|
-
// eslint-disable-next-line no-console
|
|
190
|
-
console.log("[GeminiProvider] Run completed:", {
|
|
191
|
-
model,
|
|
192
|
-
hasResult: !!result,
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return result;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Generate image from text only (Imagen API)
|
|
201
|
-
* Use for text-to-image generation without input images
|
|
202
|
-
*/
|
|
203
|
-
async generateImage(
|
|
204
|
-
prompt: string,
|
|
205
|
-
): Promise<GeminiImageGenerationResult> {
|
|
206
|
-
return geminiImageGenerationService.generateImage(prompt);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Edit/transform image using input image + prompt (Gemini API)
|
|
211
|
-
* Use for image editing, transformation, style transfer
|
|
212
|
-
*/
|
|
213
|
-
async editImage(
|
|
214
|
-
prompt: string,
|
|
215
|
-
images: GeminiImageInput[],
|
|
216
|
-
): Promise<GeminiImageGenerationResult> {
|
|
217
|
-
return geminiImageEditService.editImage(prompt, images);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Generate content with images (multimodal)
|
|
222
|
-
*/
|
|
223
|
-
async generateWithImages(
|
|
224
|
-
model: string,
|
|
225
|
-
prompt: string,
|
|
226
|
-
images: GeminiImageInput[],
|
|
227
|
-
): Promise<{ text: string; response: unknown }> {
|
|
228
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
229
|
-
// eslint-disable-next-line no-console
|
|
230
|
-
console.log("[GeminiProvider] generateWithImages() called", {
|
|
231
|
-
model,
|
|
232
|
-
promptLength: prompt.length,
|
|
233
|
-
imagesCount: images.length,
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const response = await geminiTextGenerationService.generateWithImages(
|
|
238
|
-
model,
|
|
239
|
-
prompt,
|
|
240
|
-
images,
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
const text = response.candidates?.[0]?.content.parts
|
|
244
|
-
.filter((p): p is { text: string } => "text" in p)
|
|
245
|
-
.map((p) => p.text)
|
|
246
|
-
.join("") || "";
|
|
247
|
-
|
|
248
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
249
|
-
// eslint-disable-next-line no-console
|
|
250
|
-
console.log("[GeminiProvider] generateWithImages() completed", {
|
|
251
|
-
hasText: !!text,
|
|
252
|
-
textLength: text.length,
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return { text, response };
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
reset(): void {
|
|
260
|
-
geminiClientCoreService.reset();
|
|
261
|
-
this.pendingJobs.clear();
|
|
262
|
-
this.jobCounter = 0;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
private generateRequestId(): string {
|
|
266
|
-
this.jobCounter++;
|
|
267
|
-
return `gemini-${Date.now()}-${this.jobCounter}`;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
private async processJobAsync(requestId: string): Promise<void> {
|
|
271
|
-
const job = this.pendingJobs.get(requestId);
|
|
272
|
-
|
|
273
|
-
if (!job) return;
|
|
274
|
-
|
|
275
|
-
job.status = "IN_PROGRESS";
|
|
276
|
-
|
|
277
|
-
try {
|
|
278
|
-
const result = await this.executeGeneration(job.model, job.input);
|
|
279
|
-
job.result = result;
|
|
280
|
-
job.status = "COMPLETED";
|
|
281
|
-
} catch (error) {
|
|
282
|
-
job.status = "FAILED";
|
|
283
|
-
job.error = error instanceof Error ? error.message : String(error);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
private async executeGeneration<T>(
|
|
288
|
-
model: string,
|
|
289
|
-
input: Record<string, unknown>,
|
|
290
|
-
): Promise<T> {
|
|
291
|
-
const isImageGeneration = input.generateImage === true || input.type === "image";
|
|
292
|
-
|
|
293
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
294
|
-
// eslint-disable-next-line no-console
|
|
295
|
-
console.log("[GeminiProvider] Execute generation:", {
|
|
296
|
-
model,
|
|
297
|
-
isImageGeneration,
|
|
298
|
-
promptLength: String(input.prompt || "").length,
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if (isImageGeneration) {
|
|
303
|
-
const prompt = String(input.prompt || "");
|
|
304
|
-
const images = input.images as GeminiImageInput[] | undefined;
|
|
305
|
-
const result = await geminiImageGenerationService.generateImage(prompt, images);
|
|
306
|
-
return result as T;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const contents = this.buildContents(input);
|
|
310
|
-
const response = await geminiTextGenerationService.generateContent(
|
|
311
|
-
model,
|
|
312
|
-
contents,
|
|
313
|
-
input.generationConfig as undefined,
|
|
314
|
-
);
|
|
315
|
-
|
|
316
|
-
return this.formatResponse<T>(response, input);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
private buildContents(input: Record<string, unknown>): GeminiContent[] {
|
|
320
|
-
const contents: GeminiContent[] = [];
|
|
321
|
-
|
|
322
|
-
if (typeof input.prompt === "string") {
|
|
323
|
-
const parts: GeminiContent["parts"] = [{ text: input.prompt }];
|
|
324
|
-
|
|
325
|
-
// Handle single image
|
|
326
|
-
if (input.image_url && typeof input.image_url === "string") {
|
|
327
|
-
const imageData = this.parseImageUrl(input.image_url);
|
|
328
|
-
if (imageData) {
|
|
329
|
-
parts.push({ inlineData: imageData });
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Handle multiple images
|
|
334
|
-
if (Array.isArray(input.images)) {
|
|
335
|
-
for (const img of input.images as GeminiImageInput[]) {
|
|
336
|
-
parts.push({
|
|
337
|
-
inlineData: {
|
|
338
|
-
mimeType: img.mimeType,
|
|
339
|
-
data: extractBase64Data(img.base64),
|
|
340
|
-
},
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
contents.push({ parts, role: "user" });
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (Array.isArray(input.contents)) {
|
|
349
|
-
contents.push(...(input.contents as GeminiContent[]));
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
return contents;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
private parseImageUrl(
|
|
356
|
-
imageUrl: string,
|
|
357
|
-
): { mimeType: string; data: string } | null {
|
|
358
|
-
const base64Match = imageUrl.match(/^data:([^;]+);base64,(.+)$/);
|
|
359
|
-
if (base64Match) {
|
|
360
|
-
return {
|
|
361
|
-
mimeType: base64Match[1],
|
|
362
|
-
data: base64Match[2],
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
return null;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
private formatResponse<T>(
|
|
369
|
-
response: unknown,
|
|
370
|
-
input: Record<string, unknown>,
|
|
371
|
-
): T {
|
|
372
|
-
const resp = response as {
|
|
373
|
-
candidates?: Array<{
|
|
374
|
-
content: {
|
|
375
|
-
parts: Array<{
|
|
376
|
-
text?: string;
|
|
377
|
-
inlineData?: { mimeType: string; data: string };
|
|
378
|
-
}>;
|
|
379
|
-
};
|
|
380
|
-
}>;
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
const candidate = resp.candidates?.[0];
|
|
384
|
-
const parts = candidate?.content.parts || [];
|
|
385
|
-
|
|
386
|
-
// Extract text
|
|
387
|
-
const text = parts.find((p) => p.text)?.text;
|
|
388
|
-
|
|
389
|
-
// Extract image if present
|
|
390
|
-
const imagePart = parts.find((p) => p.inlineData);
|
|
391
|
-
const imageData = imagePart?.inlineData;
|
|
392
|
-
|
|
393
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
394
|
-
// eslint-disable-next-line no-console
|
|
395
|
-
console.log("[GeminiProvider] Format response:", {
|
|
396
|
-
hasText: !!text,
|
|
397
|
-
textLength: text?.length ?? 0,
|
|
398
|
-
hasImage: !!imageData,
|
|
399
|
-
outputFormat: input.outputFormat,
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Build result object - always return { text } for consistency
|
|
404
|
-
const result: Record<string, unknown> = {
|
|
405
|
-
text,
|
|
406
|
-
response,
|
|
407
|
-
};
|
|
408
|
-
|
|
409
|
-
if (imageData) {
|
|
410
|
-
result.imageUrl = `data:${imageData.mimeType};base64,${imageData.data}`;
|
|
411
|
-
result.imageBase64 = imageData.data;
|
|
412
|
-
result.mimeType = imageData.mimeType;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
return result as T;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
export const geminiProviderService = new GeminiProviderService();
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Factory function to create a new Gemini provider instance
|
|
423
|
-
*/
|
|
424
|
-
export function createGeminiProvider(): GeminiProviderService {
|
|
425
|
-
return new GeminiProviderService();
|
|
426
|
-
}
|