@umituz/react-native-ai-gemini-provider 2.0.16 → 2.0.17

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": "2.0.16",
3
+ "version": "2.0.17",
4
4
  "description": "Google Gemini AI text generation provider for React Native applications",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
package/src/index.ts CHANGED
@@ -30,7 +30,6 @@ export {
30
30
  geminiStructuredTextService,
31
31
  geminiStreamingService,
32
32
  geminiProviderService,
33
- createGeminiProvider,
34
33
  GeminiProvider,
35
34
  } from "./infrastructure/services";
36
35
 
@@ -42,6 +41,7 @@ export {
42
41
  isGeminiErrorRetryable,
43
42
  categorizeGeminiError,
44
43
  createGeminiError,
44
+ extractTextFromResponse,
45
45
  measureAsync,
46
46
  measureSync,
47
47
  debounce,
@@ -71,7 +71,7 @@ export type {
71
71
  RequestContext,
72
72
  RequestInterceptor,
73
73
  InterceptorErrorStrategy,
74
- } from "./infrastructure/interceptors/RequestInterceptors";
74
+ } from "./infrastructure/interceptors";
75
75
 
76
76
  export type {
77
77
  ResponseContext,
@@ -1,4 +1,6 @@
1
1
 
2
+ import { telemetryHooks } from "../telemetry";
3
+
2
4
  export interface RequestContext {
3
5
  model: string;
4
6
  feature?: string;
@@ -47,6 +49,9 @@ class RequestInterceptors {
47
49
  try {
48
50
  result = await interceptor(result);
49
51
  } catch (error) {
52
+ // Log to telemetry
53
+ telemetryHooks.logError(context.model, error instanceof Error ? error : new Error(String(error)), context.feature);
54
+
50
55
  switch (this.errorStrategy) {
51
56
  case "fail":
52
57
  throw new Error(`Request interceptor failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -54,7 +59,7 @@ class RequestInterceptors {
54
59
  // Skip this interceptor and continue with previous result
55
60
  break;
56
61
  case "log":
57
- // Silently ignore but continue
62
+ // Error already logged, continue with previous result
58
63
  break;
59
64
  }
60
65
  }
@@ -1,4 +1,7 @@
1
1
 
2
+ import type { InterceptorErrorStrategy } from "./RequestInterceptors";
3
+ import { telemetryHooks } from "../telemetry";
4
+
2
5
  export interface ResponseContext<T = unknown> {
3
6
  model: string;
4
7
  feature?: string;
@@ -11,8 +14,6 @@ export type ResponseInterceptor<T = unknown> = (
11
14
  context: ResponseContext<T>,
12
15
  ) => ResponseContext<T> | Promise<ResponseContext<T>>;
13
16
 
14
- export type InterceptorErrorStrategy = "fail" | "skip" | "log";
15
-
16
17
  class ResponseInterceptors {
17
18
  private interceptors: Array<ResponseInterceptor<unknown>> = [];
18
19
  private errorStrategy: InterceptorErrorStrategy = "fail";
@@ -52,6 +53,9 @@ class ResponseInterceptors {
52
53
  try {
53
54
  result = await interceptor(result);
54
55
  } catch (error) {
56
+ // Log to telemetry
57
+ telemetryHooks.logError(context.model, error instanceof Error ? error : new Error(String(error)), context.feature);
58
+
55
59
  switch (this.errorStrategy) {
56
60
  case "fail":
57
61
  throw new Error(`Response interceptor failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -59,7 +63,7 @@ class ResponseInterceptors {
59
63
  // Skip this interceptor and continue with previous result
60
64
  break;
61
65
  case "log":
62
- // Silently ignore but continue
66
+ // Error already logged, continue with previous result
63
67
  break;
64
68
  }
65
69
  }
@@ -9,6 +9,7 @@ export { responseInterceptors } from "./ResponseInterceptors";
9
9
  export type {
10
10
  RequestContext,
11
11
  RequestInterceptor,
12
+ InterceptorErrorStrategy,
12
13
  } from "./RequestInterceptors";
13
14
 
14
15
  export type {
@@ -1,5 +1,5 @@
1
1
  import { GoogleGenerativeAI, type GenerativeModel } from "@google/generative-ai";
2
- import { DEFAULT_MODELS, GEMINI_MODELS } from "../../domain/entities";
2
+ import { DEFAULT_MODELS } from "../../domain/entities";
3
3
  import type { GeminiConfig } from "../../domain/entities";
4
4
 
5
5
  const DEFAULT_CONFIG: Partial<GeminiConfig> = {
@@ -40,14 +40,16 @@ class GeminiClientCoreService {
40
40
  }
41
41
 
42
42
  /**
43
- * Validate model name against known models
43
+ * Validate model name format (allows any valid model string)
44
44
  */
45
45
  private validateModel(modelName: string): void {
46
- const knownModels = Object.values(GEMINI_MODELS.TEXT);
47
- const isValid = knownModels.some((model) => model === modelName);
46
+ if (!modelName || typeof modelName !== "string" || modelName.trim().length === 0) {
47
+ throw new Error(`Invalid model name: "${modelName}". Model name must be a non-empty string.`);
48
+ }
48
49
 
49
- if (!isValid) {
50
- throw new Error(`Unknown model: "${modelName}". Known models: ${knownModels.join(", ")}`);
50
+ // Check for valid model format (starts with gemini-)
51
+ if (!modelName.startsWith("gemini-")) {
52
+ throw new Error(`Invalid model name: "${modelName}". Gemini models should start with "gemini-".`);
51
53
  }
52
54
  }
53
55
 
@@ -60,7 +62,7 @@ class GeminiClientCoreService {
60
62
 
61
63
  const effectiveModel = modelName || this.config?.textModel || DEFAULT_MODELS.TEXT;
62
64
 
63
- // Validate model name
65
+ // Validate model name format (not against hardcoded list)
64
66
  this.validateModel(effectiveModel);
65
67
 
66
68
  return this.client.getGenerativeModel({ model: effectiveModel });
@@ -1,7 +1,6 @@
1
1
 
2
2
  import type { GeminiConfig } from "../../domain/entities";
3
3
  import { geminiClientCoreService } from "./gemini-client-core.service";
4
- import { geminiTextGenerationService } from "./gemini-text-generation.service";
5
4
  import { geminiStructuredTextService } from "./gemini-structured-text.service";
6
5
 
7
6
  export type GeminiProviderConfig = GeminiConfig;
@@ -25,15 +24,6 @@ export class GeminiProvider {
25
24
  geminiClientCoreService.reset();
26
25
  }
27
26
 
28
- /**
29
- * Generate text from prompt
30
- */
31
- async generateText(prompt: string, model: string): Promise<string> {
32
- const contents = [{ parts: [{ text: prompt }], role: "user" as const }];
33
- const response = await geminiTextGenerationService.generateContent(model, contents);
34
- return this.extractTextFromResponse(response);
35
- }
36
-
37
27
  /**
38
28
  * Generate structured JSON response
39
29
  */
@@ -44,28 +34,6 @@ export class GeminiProvider {
44
34
  ): Promise<T> {
45
35
  return geminiStructuredTextService.generateStructuredText<T>(model, prompt, schema);
46
36
  }
47
-
48
- /**
49
- * Extract text from Gemini response
50
- */
51
- private extractTextFromResponse(response: unknown): string {
52
- const resp = response as {
53
- candidates?: Array<{
54
- content: {
55
- parts: Array<{ text?: string }>;
56
- };
57
- }>;
58
- };
59
-
60
- return resp.candidates?.[0]?.content.parts
61
- .filter((p): p is { text: string } => "text" in p && typeof p.text === "string")
62
- .map((p) => p.text)
63
- .join("") || "";
64
- }
65
37
  }
66
38
 
67
39
  export const geminiProviderService = new GeminiProvider();
68
-
69
- export function createGeminiProvider(): GeminiProvider {
70
- return new GeminiProvider();
71
- }
@@ -14,6 +14,7 @@ class GeminiStreamingService {
14
14
  contents: GeminiContent[],
15
15
  onChunk: (text: string) => void,
16
16
  generationConfig?: GeminiGenerationConfig,
17
+ signal?: AbortSignal,
17
18
  ): Promise<string> {
18
19
  const genModel = geminiClientCoreService.getModel(model);
19
20
 
@@ -22,10 +23,14 @@ class GeminiStreamingService {
22
23
  parts: content.parts.map((part) => ({ text: part.text })),
23
24
  }));
24
25
 
25
- const result = await genModel.generateContentStream({
26
+ const requestOptions = {
26
27
  contents: sdkContents as Parameters<typeof genModel.generateContentStream>[0] extends { contents: infer C } ? C : never,
27
28
  generationConfig,
28
- });
29
+ };
30
+
31
+ const result = signal
32
+ ? await genModel.generateContentStream(requestOptions, { signal })
33
+ : await genModel.generateContentStream(requestOptions);
29
34
 
30
35
  let fullText = "";
31
36
 
@@ -4,6 +4,7 @@ import type { GenerationConfig } from "@google/generative-ai";
4
4
  import type {
5
5
  GeminiContent,
6
6
  GeminiGenerationConfig,
7
+ GeminiResponse,
7
8
  } from "../../domain/entities";
8
9
 
9
10
 
@@ -16,6 +17,7 @@ class GeminiStructuredTextService {
16
17
  prompt: string,
17
18
  schema: Record<string, unknown>,
18
19
  config?: Omit<GeminiGenerationConfig, "responseMimeType" | "responseSchema">,
20
+ signal?: AbortSignal,
19
21
  ): Promise<T> {
20
22
  // Validate schema structure before passing to SDK
21
23
  if (!schema || typeof schema !== "object" || Object.keys(schema).length === 0) {
@@ -37,6 +39,7 @@ class GeminiStructuredTextService {
37
39
  model,
38
40
  contents,
39
41
  generationConfig,
42
+ signal,
40
43
  );
41
44
 
42
45
  return this.parseJSONResponse<T>(response);
@@ -45,24 +48,32 @@ class GeminiStructuredTextService {
45
48
  /**
46
49
  * Parse JSON response from Gemini
47
50
  */
48
- private parseJSONResponse<T>(response: unknown): T {
49
- const candidates = (response as { candidates?: Array<{ content: { parts: Array<{ text?: string }> } }> }).candidates;
51
+ private parseJSONResponse<T>(response: GeminiResponse): T {
52
+ const candidates = response.candidates;
53
+
54
+ if (!candidates || candidates.length === 0) {
55
+ throw new Error("No candidates in response");
56
+ }
50
57
 
51
58
  let text = "";
52
59
 
53
- if (candidates?.[0]?.content?.parts) {
60
+ if (candidates[0]?.content?.parts) {
54
61
  text = candidates[0].content.parts
55
- .map((part) => part.text || "")
62
+ .map((part) => "text" in part ? (part.text || "") : "")
56
63
  .join("");
57
64
  }
58
65
 
66
+ if (!text || text.trim().length === 0) {
67
+ throw new Error("Empty response received from Gemini");
68
+ }
69
+
59
70
  // Clean and parse JSON (remove markdown code blocks if present)
60
71
  const cleanedText = text.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
61
72
 
62
73
  try {
63
74
  return JSON.parse(cleanedText) as T;
64
75
  } catch (error) {
65
- throw new Error(`Failed to parse structured response: ${error instanceof Error ? error.message : String(error)}`);
76
+ throw new Error(`Failed to parse structured response: ${error instanceof Error ? error.message : String(error)}. Cleaned text: ${cleanedText.substring(0, 200)}...`);
66
77
  }
67
78
  }
68
79
  }
@@ -6,6 +6,8 @@ import type {
6
6
  GeminiGenerationConfig,
7
7
  GeminiResponse,
8
8
  GeminiPart,
9
+ GeminiFinishReason,
10
+ GeminiSafetyRating,
9
11
  } from "../../domain/entities";
10
12
 
11
13
  class GeminiTextGenerationService {
@@ -16,6 +18,7 @@ class GeminiTextGenerationService {
16
18
  model: string,
17
19
  contents: GeminiContent[],
18
20
  generationConfig?: GeminiGenerationConfig,
21
+ signal?: AbortSignal,
19
22
  ): Promise<GeminiResponse> {
20
23
  const genModel = geminiClientCoreService.getModel(model);
21
24
 
@@ -24,12 +27,20 @@ class GeminiTextGenerationService {
24
27
  parts: content.parts,
25
28
  }));
26
29
 
27
- const result = await genModel.generateContent({
30
+ const requestOptions = {
28
31
  contents: sdkContents as Parameters<typeof genModel.generateContent>[0] extends { contents: infer C } ? C : never,
29
32
  generationConfig,
30
- });
33
+ };
34
+
35
+ const result = signal
36
+ ? await genModel.generateContent(requestOptions, { signal })
37
+ : await genModel.generateContent(requestOptions);
31
38
 
32
- const response = (result as { response: GeminiResponse }).response;
39
+ const response = result.response;
40
+
41
+ if (!response) {
42
+ throw new Error("No response received from Gemini API");
43
+ }
33
44
 
34
45
  return {
35
46
  candidates: response.candidates?.map((candidate) => {
@@ -41,14 +52,33 @@ class GeminiTextGenerationService {
41
52
  // Ignore unsupported part types (inlineData, etc.)
42
53
  }
43
54
 
55
+ // Map SDK finish reason to our domain type
56
+ const finishReason: GeminiFinishReason | undefined = candidate.finishReason
57
+ ? (candidate.finishReason as GeminiFinishReason)
58
+ : undefined;
59
+
60
+ // Map safety ratings
61
+ const safetyRatings: GeminiSafetyRating[] | undefined = candidate.safetyRatings
62
+ ? candidate.safetyRatings.map((rating) => ({
63
+ category: rating.category as GeminiSafetyRating["category"],
64
+ probability: rating.probability as GeminiSafetyRating["probability"],
65
+ }))
66
+ : undefined;
67
+
44
68
  return {
45
69
  content: {
46
70
  parts: transformedParts,
47
- role: (candidate.content.role || "model"),
71
+ role: (candidate.content.role || "model") as "user" | "model",
48
72
  },
49
- finishReason: candidate.finishReason,
73
+ finishReason,
74
+ safetyRatings,
50
75
  };
51
76
  }),
77
+ usageMetadata: response.usageMetadata ? {
78
+ promptTokenCount: response.usageMetadata.promptTokenCount,
79
+ candidatesTokenCount: response.usageMetadata.candidatesTokenCount,
80
+ totalTokenCount: response.usageMetadata.totalTokenCount,
81
+ } : undefined,
52
82
  };
53
83
  }
54
84
 
@@ -59,18 +89,15 @@ class GeminiTextGenerationService {
59
89
  model: string,
60
90
  prompt: string,
61
91
  config?: GeminiGenerationConfig,
92
+ signal?: AbortSignal,
62
93
  ): Promise<string> {
63
94
  const contents: GeminiContent[] = [
64
95
  { parts: [{ text: prompt }], role: "user" },
65
96
  ];
66
97
 
67
- const response = await this.generateContent(model, contents, config);
98
+ const response = await this.generateContent(model, contents, config, signal);
68
99
  return extractTextFromResponse(response);
69
100
  }
70
-
71
- /**
72
- * Generate content with images (multimodal)
73
- */
74
101
  }
75
102
 
76
103
  export const geminiTextGenerationService = new GeminiTextGenerationService();
@@ -12,7 +12,6 @@ export { geminiStreamingService } from "./gemini-streaming.service";
12
12
  // Provider
13
13
  export {
14
14
  geminiProviderService,
15
- createGeminiProvider,
16
15
  GeminiProvider,
17
16
  } from "./gemini-provider";
18
17
  export type { GeminiProviderConfig } from "./gemini-provider";
@@ -1,4 +1,3 @@
1
-
2
1
  export interface AsyncStateCallbacks {
3
2
  onSuccess?: (result: string) => void;
4
3
  onError?: (error: string) => void;
@@ -3,12 +3,12 @@ import type { GeminiResponse } from "../../domain/entities";
3
3
 
4
4
 
5
5
  export function extractTextFromResponse(response: GeminiResponse): string {
6
- const candidate = response.candidates?.[0];
7
-
8
- if (!candidate) {
6
+ if (!response.candidates || response.candidates.length === 0) {
9
7
  throw new Error("No response candidates");
10
8
  }
11
9
 
10
+ const candidate = response.candidates[0];
11
+
12
12
  // Handle all finish reasons appropriately
13
13
  switch (candidate.finishReason) {
14
14
  case "SAFETY":
@@ -19,6 +19,7 @@ export function extractTextFromResponse(response: GeminiResponse): string {
19
19
  case "FINISH_REASON_UNSPECIFIED":
20
20
  case "OTHER":
21
21
  case "STOP":
22
+ case undefined:
22
23
  // Continue to extract text
23
24
  break;
24
25
  }
@@ -1,5 +1,6 @@
1
- export * from "./async-state.util";
2
- export * from "./error-mapper.util";
3
- export * from "./gemini-data-transformer.util";
4
- export * from "./performance.util";
5
- export * from "./rate-limiter.util";
1
+ export { mapGeminiError, isGeminiErrorRetryable, categorizeGeminiError, createGeminiError } from "./error-mapper.util";
2
+ export { extractTextFromResponse } from "./gemini-data-transformer.util";
3
+ export { measureAsync, measureSync, debounce, throttle, PerformanceTimer } from "./performance.util";
4
+ export { RateLimiter } from "./rate-limiter.util";
5
+ export type { PerformanceMetrics } from "./performance.util";
6
+ export type { RateLimiterOptions } from "./rate-limiter.util";
@@ -10,7 +10,6 @@ export class RateLimiter {
10
10
  private lastRequest = 0;
11
11
  private minInterval: number;
12
12
  private maxQueueSize: number;
13
- private processQueuePromise: Promise<void> | null = null;
14
13
 
15
14
  constructor(options: RateLimiterOptions = {}) {
16
15
  this.minInterval = options.minInterval ?? 100; // 100ms minimum interval
@@ -32,36 +31,37 @@ export class RateLimiter {
32
31
  }
33
32
  });
34
33
 
35
- // Ensure queue processing is running (wait for it to avoid race condition)
36
- if (!this.processQueuePromise) {
37
- this.processQueuePromise = this.processQueue();
38
- this.processQueuePromise.then(() => {
39
- this.processQueuePromise = null;
40
- }).catch(() => {
41
- this.processQueuePromise = null;
42
- });
43
- }
34
+ // Start queue processing if not already running
35
+ this.processQueue().catch(() => {
36
+ // Individual task errors are handled above, ignore queue processing errors
37
+ });
44
38
  });
45
39
  }
46
40
 
47
- private async processQueue() {
48
- if (this.processing || this.queue.length === 0) return;
41
+ private async processQueue(): Promise<void> {
42
+ // Only one processQueue can run at a time
43
+ if (this.processing) {
44
+ return;
45
+ }
46
+
49
47
  this.processing = true;
50
48
 
51
- while (this.queue.length > 0) {
52
- const elapsed = Date.now() - this.lastRequest;
53
- if (elapsed < this.minInterval) {
54
- await new Promise((r) => setTimeout(r, this.minInterval - elapsed));
55
- }
49
+ try {
50
+ while (this.queue.length > 0) {
51
+ const elapsed = Date.now() - this.lastRequest;
52
+ if (elapsed < this.minInterval) {
53
+ await new Promise((r) => setTimeout(r, this.minInterval - elapsed));
54
+ }
56
55
 
57
- const task = this.queue.shift();
58
- if (task) {
59
- this.lastRequest = Date.now();
60
- await task();
56
+ const task = this.queue.shift();
57
+ if (task) {
58
+ this.lastRequest = Date.now();
59
+ await task();
60
+ }
61
61
  }
62
+ } finally {
63
+ this.processing = false;
62
64
  }
63
-
64
- this.processing = false;
65
65
  }
66
66
 
67
67
  getQueueSize(): number {
@@ -3,7 +3,7 @@ import { useState, useCallback, useRef, useMemo, useEffect } from "react";
3
3
  import type { GeminiGenerationConfig } from "../../domain/entities";
4
4
  import { DEFAULT_MODELS } from "../../domain/entities";
5
5
  import { geminiTextGenerationService, geminiStructuredTextService } from "../../infrastructure/services";
6
- import { executeWithState } from "../../infrastructure/utils";
6
+ import { executeWithState } from "../../infrastructure/utils/async-state.util";
7
7
 
8
8
  export interface UseGeminiOptions {
9
9
  model?: string;
@@ -41,6 +41,11 @@ export function useGemini(options: UseGeminiOptions = {}): UseGeminiReturn {
41
41
  const model = options.model ?? DEFAULT_MODELS.TEXT;
42
42
 
43
43
  const generate = useCallback(async (prompt: string) => {
44
+ // Abort previous operation if still running
45
+ if (abortControllerRef.current) {
46
+ abortControllerRef.current.abort();
47
+ }
48
+
44
49
  // Create new abort controller for this operation
45
50
  const controller = new AbortController();
46
51
  abortControllerRef.current = controller;
@@ -54,11 +59,12 @@ export function useGemini(options: UseGeminiOptions = {}): UseGeminiReturn {
54
59
  async () => {
55
60
  // Check if this operation is still the latest one
56
61
  if (currentOpId !== operationIdRef.current) {
62
+ controller.abort();
57
63
  throw new Error("Operation cancelled by newer request");
58
64
  }
59
- return geminiTextGenerationService.generateText(model, prompt, options.generationConfig);
65
+ return geminiTextGenerationService.generateText(model, prompt, options.generationConfig, controller.signal);
60
66
  },
61
- (text) => {
67
+ (text: string) => {
62
68
  // Only update if this is still the latest operation
63
69
  if (currentOpId === operationIdRef.current) {
64
70
  setResult(text);
@@ -75,6 +81,11 @@ export function useGemini(options: UseGeminiOptions = {}): UseGeminiReturn {
75
81
  }, [model, options.generationConfig, setters, callbacks, options.onSuccess]);
76
82
 
77
83
  const generateJSON = useCallback(async <T>(prompt: string, schema?: Record<string, unknown>): Promise<T | null> => {
84
+ // Abort previous operation if still running
85
+ if (abortControllerRef.current) {
86
+ abortControllerRef.current.abort();
87
+ }
88
+
78
89
  // Create new abort controller for this operation
79
90
  const controller = new AbortController();
80
91
  abortControllerRef.current = controller;
@@ -88,14 +99,15 @@ export function useGemini(options: UseGeminiOptions = {}): UseGeminiReturn {
88
99
  async () => {
89
100
  // Check if this operation is still the latest one
90
101
  if (currentOpId !== operationIdRef.current) {
102
+ controller.abort();
91
103
  throw new Error("Operation cancelled by newer request");
92
104
  }
93
105
 
94
106
  if (schema) {
95
- return geminiStructuredTextService.generateStructuredText<T>(model, prompt, schema, options.generationConfig);
107
+ return geminiStructuredTextService.generateStructuredText<T>(model, prompt, schema, options.generationConfig, controller.signal);
96
108
  }
97
109
 
98
- const text = await geminiTextGenerationService.generateText(model, prompt, { ...options.generationConfig, responseMimeType: "application/json" });
110
+ const text = await geminiTextGenerationService.generateText(model, prompt, { ...options.generationConfig, responseMimeType: "application/json" }, controller.signal);
99
111
  const cleanedText = cleanJsonResponse(text);
100
112
 
101
113
  try {
@@ -104,7 +116,7 @@ export function useGemini(options: UseGeminiOptions = {}): UseGeminiReturn {
104
116
  throw new Error(`Failed to parse JSON response: ${parseError instanceof Error ? parseError.message : String(parseError)}. Response: ${cleanedText.substring(0, 200)}...`);
105
117
  }
106
118
  },
107
- (parsed) => {
119
+ (parsed: unknown) => {
108
120
  // Only update if this is still the latest operation
109
121
  if (currentOpId === operationIdRef.current) {
110
122
  setJsonResult(parsed);
@@ -14,6 +14,7 @@ export interface ProviderFactoryOptions extends ProviderConfigInput {
14
14
 
15
15
  class ProviderFactory {
16
16
  private currentConfig: ResolvedProviderConfig | null = null;
17
+ private currentOptions: ProviderFactoryOptions | null = null;
17
18
 
18
19
  /**
19
20
  * Initialize provider with configuration
@@ -27,6 +28,7 @@ class ProviderFactory {
27
28
  }
28
29
 
29
30
  this.currentConfig = config;
31
+ this.currentOptions = options;
30
32
 
31
33
  // Initialize Gemini client with resolved config
32
34
  const geminiConfig: GeminiConfig = {
@@ -57,25 +59,33 @@ class ProviderFactory {
57
59
  * Note: Changing apiKey requires full re-initialization
58
60
  */
59
61
  updateConfig(updates: Partial<ProviderConfigInput>): void {
60
- if (!this.currentConfig) {
62
+ if (!this.currentConfig || !this.currentOptions) {
61
63
  throw new Error("Provider not initialized. Call initialize() first.");
62
64
  }
63
65
 
64
66
  // If API key is changing, we need to re-initialize
65
67
  if (updates.apiKey && updates.apiKey !== this.currentConfig.apiKey) {
66
- const newInput: ProviderConfigInput = {
68
+ const newInput: ProviderFactoryOptions = {
67
69
  apiKey: updates.apiKey,
68
- preferences: updates.preferences || {},
70
+ preferences: updates.preferences || this.currentOptions.preferences,
71
+ strategy: this.currentOptions.strategy,
69
72
  };
70
73
  this.initialize(newInput);
71
74
  return;
72
75
  }
73
76
 
74
77
  // For other updates, merge with current config
75
- this.currentConfig = {
76
- ...this.currentConfig,
78
+ const mergedPreferences = {
79
+ ...this.currentOptions.preferences,
77
80
  ...updates.preferences,
78
- timeout: updates.preferences?.timeout ?? this.currentConfig.timeout,
81
+ };
82
+
83
+ this.currentOptions.preferences = mergedPreferences;
84
+
85
+ this.currentConfig = {
86
+ apiKey: this.currentConfig.apiKey,
87
+ textModel: this.currentConfig.textModel,
88
+ timeout: mergedPreferences.timeout ?? this.currentConfig.timeout,
79
89
  };
80
90
  }
81
91
  }