@umituz/react-native-ai-gemini-provider 1.9.3 → 1.10.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-gemini-provider",
3
- "version": "1.9.3",
3
+ "version": "1.10.1",
4
4
  "description": "Google Gemini AI provider for React Native applications",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -10,9 +10,12 @@ export interface GeminiConfig {
10
10
  baseDelay?: number;
11
11
  maxDelay?: number;
12
12
  defaultTimeoutMs?: number;
13
- defaultModel?: string;
14
- /** Model used for image generation (default: gemini-2.0-flash-exp) */
15
- imageModel?: string;
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-3-pro-image-preview is the highest quality for identity preservation
24
+ // gemini-2.5-flash-image is the most cost-effective (500 images/day free tier)
25
25
  IMAGE_EDIT: {
26
- DEFAULT: "gemini-3-pro-image-preview",
27
- FAST: "gemini-2.5-flash-image",
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.FLASH,
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
 
package/src/index.ts CHANGED
@@ -93,3 +93,24 @@ export type {
93
93
  UseGeminiOptions,
94
94
  UseGeminiReturn,
95
95
  } from "./presentation/hooks";
96
+
97
+ // =============================================================================
98
+ // PROVIDER CONFIGURATION - Tier-based Setup
99
+ // =============================================================================
100
+
101
+ export {
102
+ providerFactory,
103
+ resolveProviderConfig,
104
+ getCostOptimizedConfig,
105
+ getQualityOptimizedConfig,
106
+ } from "./providers";
107
+
108
+ export type {
109
+ SubscriptionTier,
110
+ QualityPreference,
111
+ ProviderPreferences,
112
+ ProviderConfigInput,
113
+ ResolvedProviderConfig,
114
+ OptimizationStrategy,
115
+ ProviderFactoryOptions,
116
+ } from "./providers";
@@ -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
- defaultModel: DEFAULT_MODELS.TEXT,
18
- imageModel: DEFAULT_MODELS.TEXT_TO_IMAGE,
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
- defaultModel: config.defaultModel,
32
- imageModel: config.imageModel,
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
- defaultModel: this.config.defaultModel,
44
- imageModel: this.config.imageModel,
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?.defaultModel || "gemini-1.5-flash";
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?.imageModel || DEFAULT_MODELS.TEXT_TO_IMAGE;
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
- } from "./gemini-provider.service";
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.service";
23
+ } from "./gemini-provider";
24
+
25
+ export type { JobSubmission, JobStatus } from "../job/JobManager";
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Provider Configuration
3
+ * Centralized configuration for AI provider with tier-based settings
4
+ */
5
+
6
+ export type SubscriptionTier = "free" | "premium";
7
+
8
+ export type QualityPreference = "fast" | "balanced" | "high";
9
+
10
+ export interface ProviderPreferences {
11
+ /** Quality preference (fast = Flash, balanced = Flash, high = Pro) */
12
+ quality?: QualityPreference;
13
+ /** Maximum retry attempts for failed requests */
14
+ maxRetries?: number;
15
+ /** Base delay for retry backoff (ms) */
16
+ baseDelay?: number;
17
+ /** Maximum delay for retry backoff (ms) */
18
+ maxDelay?: number;
19
+ /** Request timeout (ms) */
20
+ timeout?: number;
21
+ }
22
+
23
+ export interface ProviderConfigInput {
24
+ /** API key for authentication */
25
+ apiKey: string;
26
+ /** Subscription tier (determines default models and limits) */
27
+ subscriptionTier: SubscriptionTier;
28
+ /** Optional user preferences (overrides tier defaults) */
29
+ preferences?: ProviderPreferences;
30
+ }
31
+
32
+ export interface ResolvedProviderConfig {
33
+ apiKey: string;
34
+ subscriptionTier: SubscriptionTier;
35
+ textModel: string;
36
+ imageGenerationModel: string;
37
+ imageEditModel: string;
38
+ maxRetries: number;
39
+ baseDelay: number;
40
+ maxDelay: number;
41
+ timeout: number;
42
+ }
43
+
44
+ /**
45
+ * Default configurations per subscription tier
46
+ */
47
+ const TIER_DEFAULTS: Record<
48
+ SubscriptionTier,
49
+ Omit<ResolvedProviderConfig, "apiKey" | "subscriptionTier">
50
+ > = {
51
+ free: {
52
+ textModel: "gemini-2.5-flash",
53
+ imageGenerationModel: "imagen-4.0-generate-001",
54
+ imageEditModel: "gemini-2.5-flash-image", // Fast model for free tier
55
+ maxRetries: 1, // Limited retries for free tier
56
+ baseDelay: 1000,
57
+ maxDelay: 5000,
58
+ timeout: 30000, // 30 seconds
59
+ },
60
+ premium: {
61
+ textModel: "gemini-2.5-flash",
62
+ imageGenerationModel: "imagen-4.0-generate-001",
63
+ imageEditModel: "gemini-3-pro-image-preview", // High quality for premium
64
+ maxRetries: 2, // More retries for premium
65
+ baseDelay: 1000,
66
+ maxDelay: 10000,
67
+ timeout: 60000, // 60 seconds
68
+ },
69
+ };
70
+
71
+ /**
72
+ * Quality preference to model mapping
73
+ */
74
+ const QUALITY_TO_MODEL: Record<QualityPreference, string> = {
75
+ fast: "gemini-2.5-flash-image",
76
+ balanced: "gemini-2.5-flash-image",
77
+ high: "gemini-3-pro-image-preview",
78
+ };
79
+
80
+ /**
81
+ * Resolve provider configuration based on tier and preferences
82
+ */
83
+ export function resolveProviderConfig(
84
+ input: ProviderConfigInput,
85
+ ): ResolvedProviderConfig {
86
+ const tierDefaults = TIER_DEFAULTS[input.subscriptionTier];
87
+ const preferences = input.preferences || {};
88
+
89
+ // Override image edit model if quality preference is provided
90
+ let imageEditModel = tierDefaults.imageEditModel;
91
+ if (preferences.quality) {
92
+ imageEditModel = QUALITY_TO_MODEL[preferences.quality];
93
+ }
94
+
95
+ return {
96
+ apiKey: input.apiKey,
97
+ subscriptionTier: input.subscriptionTier,
98
+ textModel: tierDefaults.textModel,
99
+ imageGenerationModel: tierDefaults.imageGenerationModel,
100
+ imageEditModel,
101
+ maxRetries: preferences.maxRetries ?? tierDefaults.maxRetries,
102
+ baseDelay: preferences.baseDelay ?? tierDefaults.baseDelay,
103
+ maxDelay: preferences.maxDelay ?? tierDefaults.maxDelay,
104
+ timeout: preferences.timeout ?? tierDefaults.timeout,
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Get cost-optimized config (always use fastest/cheapest models)
110
+ * Useful for development or cost-sensitive scenarios
111
+ */
112
+ export function getCostOptimizedConfig(
113
+ input: ProviderConfigInput,
114
+ ): ResolvedProviderConfig {
115
+ const resolved = resolveProviderConfig(input);
116
+ return {
117
+ ...resolved,
118
+ imageEditModel: "gemini-2.5-flash-image", // Always use fast model
119
+ maxRetries: 0, // No retries to minimize cost
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Get quality-optimized config (always use best models)
125
+ * Useful for production or quality-critical scenarios
126
+ */
127
+ export function getQualityOptimizedConfig(
128
+ input: ProviderConfigInput,
129
+ ): ResolvedProviderConfig {
130
+ const resolved = resolveProviderConfig(input);
131
+ return {
132
+ ...resolved,
133
+ imageEditModel: "gemini-3-pro-image-preview", // Always use pro model
134
+ maxRetries: 2, // More retries for reliability
135
+ timeout: 60000, // Longer timeout
136
+ };
137
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Provider Factory
3
+ * Creates and configures AI provider instances with tier-based settings
4
+ */
5
+
6
+ import { geminiClientCoreService } from "../infrastructure/services/gemini-client-core.service";
7
+ import type { GeminiConfig } from "../domain/entities";
8
+ import type {
9
+ ProviderConfigInput,
10
+ ResolvedProviderConfig,
11
+ } from "./ProviderConfig";
12
+ import {
13
+ resolveProviderConfig,
14
+ getCostOptimizedConfig,
15
+ getQualityOptimizedConfig,
16
+ } from "./ProviderConfig";
17
+
18
+ declare const __DEV__: boolean;
19
+
20
+ export type OptimizationStrategy = "cost" | "quality" | "balanced";
21
+
22
+ export interface ProviderFactoryOptions extends ProviderConfigInput {
23
+ /** Optimization strategy (overrides tier defaults) */
24
+ strategy?: OptimizationStrategy;
25
+ }
26
+
27
+ class ProviderFactory {
28
+ private currentConfig: ResolvedProviderConfig | null = null;
29
+
30
+ /**
31
+ * Initialize provider with tier-based configuration
32
+ */
33
+ initialize(options: ProviderFactoryOptions): void {
34
+ let config: ResolvedProviderConfig;
35
+
36
+ // Apply optimization strategy
37
+ switch (options.strategy) {
38
+ case "cost":
39
+ config = getCostOptimizedConfig(options);
40
+ break;
41
+ case "quality":
42
+ config = getQualityOptimizedConfig(options);
43
+ break;
44
+ case "balanced":
45
+ default:
46
+ config = resolveProviderConfig(options);
47
+ break;
48
+ }
49
+
50
+ this.currentConfig = config;
51
+
52
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
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
+ }
61
+
62
+ // Initialize Gemini client with resolved config
63
+ const geminiConfig: GeminiConfig = {
64
+ apiKey: config.apiKey,
65
+ imageEditModel: config.imageEditModel,
66
+ maxRetries: config.maxRetries,
67
+ baseDelay: config.baseDelay,
68
+ maxDelay: config.maxDelay,
69
+ defaultTimeoutMs: config.timeout,
70
+ textModel: config.textModel,
71
+ };
72
+
73
+ geminiClientCoreService.initialize(geminiConfig);
74
+
75
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
76
+ console.log("[ProviderFactory] Provider initialized successfully");
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Get current resolved configuration
82
+ */
83
+ getConfig(): ResolvedProviderConfig | null {
84
+ return this.currentConfig;
85
+ }
86
+
87
+ /**
88
+ * Check if provider is initialized
89
+ */
90
+ isInitialized(): boolean {
91
+ return this.currentConfig !== null;
92
+ }
93
+
94
+ /**
95
+ * Update configuration without re-initializing
96
+ * Useful for switching models or adjusting settings
97
+ */
98
+ updateConfig(updates: Partial<ProviderConfigInput>): void {
99
+ if (!this.currentConfig) {
100
+ throw new Error(
101
+ "Provider not initialized. Call initialize() first.",
102
+ );
103
+ }
104
+
105
+ const newInput: ProviderConfigInput = {
106
+ apiKey: updates.apiKey || this.currentConfig.apiKey,
107
+ subscriptionTier:
108
+ updates.subscriptionTier || this.currentConfig.subscriptionTier,
109
+ preferences: {
110
+ ...updates.preferences,
111
+ },
112
+ };
113
+
114
+ this.initialize(newInput);
115
+ }
116
+ }
117
+
118
+ export const providerFactory = new ProviderFactory();
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Provider Configuration & Factory
3
+ * Centralized configuration system for tier-based AI provider setup
4
+ */
5
+
6
+ export {
7
+ resolveProviderConfig,
8
+ getCostOptimizedConfig,
9
+ getQualityOptimizedConfig,
10
+ } from "./ProviderConfig";
11
+
12
+ export type {
13
+ SubscriptionTier,
14
+ QualityPreference,
15
+ ProviderPreferences,
16
+ ProviderConfigInput,
17
+ ResolvedProviderConfig,
18
+ } from "./ProviderConfig";
19
+
20
+ export { providerFactory } from "./ProviderFactory";
21
+
22
+ export type {
23
+ OptimizationStrategy,
24
+ ProviderFactoryOptions,
25
+ } from "./ProviderFactory";
@@ -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
- }