@umituz/react-native-ai-gemini-provider 3.0.29 → 3.0.31

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/dist/index.d.ts CHANGED
@@ -5,12 +5,11 @@
5
5
  export type { GeminiConfig, GeminiGenerationConfig, GeminiHarmCategory, GeminiHarmBlockThreshold, GeminiContent, GeminiPart, GeminiInlineDataPart, GeminiMessagePart, GeminiSafetySetting, GeminiModelOptions, GeminiChatConfig, GeminiResponse, GeminiCandidate, GeminiFinishReason, GeminiSafetyRating, GeminiUsageMetadata, GeminiErrorInfo, GeminiApiError, } from "./domain/entities";
6
6
  export { GeminiErrorType, GeminiError, GEMINI_MODELS, DEFAULT_MODELS } from "./domain/entities";
7
7
  export { geminiClient } from "./infrastructure/services/GeminiClient";
8
- export { createChatSession, sendChatMessage, buildChatHistory, trimChatHistory, resolveAudioMimeType, resolveImageMimeType, type ChatSendResult, type ChatHistoryMessage, type SendChatMessageOptions, } from "./infrastructure/services/ChatSession";
8
+ export { createChatSession, sendChatMessage, buildChatHistory, trimChatHistory, type ChatSendResult, type ChatHistoryMessage, type SendChatMessageOptions, } from "./infrastructure/services/ChatSession";
9
9
  export { textGeneration } from "./infrastructure/services/TextGeneration";
10
10
  export { structuredText } from "./infrastructure/services/StructuredText";
11
11
  export { streaming } from "./infrastructure/services/Streaming";
12
- export { geminiProvider, GeminiProvider } from "./infrastructure/services/GeminiProvider";
13
- export { useGemini } from "./presentation/hooks";
14
- export type { UseGeminiOptions, UseGeminiReturn } from "./presentation/hooks";
15
- export { ConfigBuilder, providerFactory } from "./providers";
16
- export type { ProviderConfig, ProviderFactoryOptions, } from "./providers";
12
+ export { useGemini } from "./presentation/hooks/useGemini";
13
+ export type { UseGeminiOptions, UseGeminiReturn } from "./presentation/hooks/useGemini";
14
+ export { ConfigBuilder, providerFactory } from "./providers/ProviderFactory";
15
+ export type { ProviderConfig, ProviderFactoryOptions, } from "./providers/ProviderFactory";
@@ -1,5 +1,6 @@
1
1
  import type { GeminiContent, GeminiGenerationConfig } from "../../domain/entities";
2
2
  import type { GenerativeModel } from "@google/generative-ai";
3
+ import type { Part } from "@google/generative-ai";
3
4
  export interface BaseRequestOptions {
4
5
  model: string;
5
6
  contents: GeminiContent[];
@@ -11,23 +12,17 @@ export declare abstract class BaseGeminiService {
11
12
  genModel: GenerativeModel;
12
13
  sdkContents: Array<{
13
14
  role: string;
14
- parts: Array<{
15
- text: string;
16
- }>;
15
+ parts: Part[];
17
16
  }>;
18
17
  };
19
18
  protected handleError(error: unknown, abortMessage: string): never;
20
19
  protected createRequestOptions(sdkContents: Array<{
21
20
  role: string;
22
- parts: Array<{
23
- text: string;
24
- }>;
21
+ parts: Part[];
25
22
  }>, generationConfig?: GeminiGenerationConfig): {
26
23
  contents: {
27
24
  role: string;
28
- parts: Array<{
29
- text: string;
30
- }>;
25
+ parts: Part[];
31
26
  }[];
32
27
  generationConfig: import("@google/generative-ai").GenerationConfig | undefined;
33
28
  };
@@ -29,10 +29,6 @@ export interface SendChatMessageOptions {
29
29
  /** Min messages to always keep regardless of budget (default 4) */
30
30
  historyMinMessages?: number;
31
31
  }
32
- /** Resolve MIME type for an audio file extension */
33
- export declare function resolveAudioMimeType(extension: string): string;
34
- /** Resolve MIME type for an image file extension */
35
- export declare function resolveImageMimeType(extension: string): string;
36
32
  /**
37
33
  * Converts chat messages (user/assistant/system) to Gemini SDK content format.
38
34
  * Skips system messages, merges consecutive same-role messages.
@@ -1,13 +1,10 @@
1
- import { GoogleGenerativeAI, type GenerativeModel } from "@google/generative-ai";
1
+ import { type GenerativeModel } from "@google/generative-ai";
2
2
  import type { GeminiConfig, GeminiModelOptions } from "../../domain/entities";
3
3
  declare class GeminiClient {
4
4
  private client;
5
5
  private config;
6
6
  private initialized;
7
7
  initialize(config: GeminiConfig): void;
8
- isInitialized(): boolean;
9
- getConfig(): GeminiConfig | null;
10
- getClient(): GoogleGenerativeAI | null;
11
8
  /**
12
9
  * Returns a GenerativeModel configured with optional safety settings and system instruction.
13
10
  * When no safety settings are provided, defaults to BLOCK_NONE for all categories.
@@ -1,7 +1,6 @@
1
1
  import type { GeminiGenerationConfig } from "../../domain/entities";
2
2
  declare class StructuredTextService {
3
3
  generateStructuredText<T>(model: string, prompt: string, schema: Record<string, unknown>, config?: Omit<GeminiGenerationConfig, "responseMimeType" | "responseSchema">, signal?: AbortSignal): Promise<T>;
4
- private parseJSONResponse;
5
4
  }
6
5
  export declare const structuredText: StructuredTextService;
7
6
  export {};
@@ -1,9 +1,8 @@
1
1
  export interface TelemetryEvent {
2
- type: "request" | "response" | "error" | "retry";
2
+ type: "error";
3
3
  timestamp: number;
4
4
  model?: string;
5
5
  feature?: string;
6
- duration?: number;
7
6
  metadata?: Record<string, unknown>;
8
7
  }
9
8
  export type TelemetryListener = (event: TelemetryEvent) => void;
@@ -14,44 +13,29 @@ declare class TelemetryHooks {
14
13
  private listenerFailureCounts;
15
14
  /**
16
15
  * Register a telemetry listener
16
+ *
17
+ * @returns Unsubscribe function - IMPORTANT: Call this when done listening to prevent memory leaks
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * const unsubscribe = telemetryHooks.subscribe((event) => console.log(event));
22
+ * // ... later when done
23
+ * unsubscribe(); // Prevents memory leak
24
+ * ```
17
25
  */
18
26
  subscribe(listener: TelemetryListener): () => void;
19
27
  /**
20
28
  * Emit a telemetry event to all listeners
21
29
  */
22
30
  emit(event: TelemetryEvent): void;
23
- /**
24
- * Log request start
25
- */
26
- logRequest(model: string, feature?: string): number;
27
- /**
28
- * Log response received
29
- */
30
- logResponse(model: string, startTime: number, feature?: string, metadata?: Record<string, unknown>): void;
31
31
  /**
32
32
  * Log error
33
33
  */
34
34
  logError(model: string, error: Error, feature?: string): void;
35
- /**
36
- * Log retry attempt
37
- */
38
- logRetry(model: string, attempt: number, feature?: string): void;
39
35
  /**
40
36
  * Remove a specific listener
41
37
  */
42
38
  unsubscribe(listener: TelemetryListener): void;
43
- /**
44
- * Reset failure counts for all listeners (clear blacklist)
45
- */
46
- resetFailures(): void;
47
- /**
48
- * Clear all listeners
49
- */
50
- clear(): void;
51
- /**
52
- * Get current listener count
53
- */
54
- getListenerCount(): number;
55
39
  }
56
40
  export declare const telemetryHooks: TelemetryHooks;
57
41
  export {};
@@ -23,7 +23,6 @@ export interface AsyncStateSetters<T = string, U = unknown> {
23
23
  */
24
24
  export interface AsyncStateConfig<T = string> {
25
25
  resetState?: boolean;
26
- throwOnError?: boolean;
27
26
  transformResult?: (result: T) => T;
28
27
  }
29
28
  /**
@@ -2,15 +2,15 @@
2
2
  * Content Mapper Utilities
3
3
  * Handles transformation between domain content and SDK format
4
4
  */
5
+ import type { Part } from "@google/generative-ai";
5
6
  import type { GeminiContent, GeminiPart, GeminiResponse } from "../../domain/entities";
6
7
  /**
7
8
  * Convert domain content to SDK format
9
+ * Preserves both text and inlineData parts
8
10
  */
9
11
  export declare function toSdkContent(contents: GeminiContent[]): Array<{
10
12
  role: string;
11
- parts: Array<{
12
- text: string;
13
- }>;
13
+ parts: Part[];
14
14
  }>;
15
15
  /**
16
16
  * Create a simple text content
@@ -7,7 +7,7 @@ export interface UseGeminiOptions {
7
7
  }
8
8
  export interface UseGeminiReturn {
9
9
  generate: (prompt: string) => Promise<void>;
10
- generateJSON: <T>(prompt: string, schema?: Record<string, unknown>) => Promise<T | null>;
10
+ generateJSON: <T>(prompt: string, schema: Record<string, unknown>) => Promise<T | null>;
11
11
  result: string | null;
12
12
  jsonResult: unknown;
13
13
  isGenerating: boolean;
@@ -43,8 +43,4 @@ export declare class ConfigBuilder {
43
43
  * Create a new builder instance
44
44
  */
45
45
  static create(): ConfigBuilder;
46
- /**
47
- * Create from existing config (for updates)
48
- */
49
- static from(config: Partial<ProviderConfig>): ConfigBuilder;
50
46
  }
@@ -1,4 +1,3 @@
1
- import { type ProviderConfig } from "./ConfigBuilder";
2
1
  export { ConfigBuilder } from "./ConfigBuilder";
3
2
  export type { ProviderConfig } from "./ConfigBuilder";
4
3
  export interface ProviderFactoryOptions {
@@ -9,15 +8,10 @@ export interface ProviderFactoryOptions {
9
8
  }
10
9
  declare class ProviderFactory {
11
10
  private currentConfig;
12
- private builder;
13
11
  /**
14
12
  * Initialize provider with configuration
15
13
  */
16
14
  initialize(options: ProviderFactoryOptions): void;
17
- /**
18
- * Get current configuration
19
- */
20
- getConfig(): ProviderConfig | null;
21
15
  /**
22
16
  * Check if provider is initialized
23
17
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-gemini-provider",
3
- "version": "3.0.29",
3
+ "version": "3.0.31",
4
4
  "description": "Google Gemini AI text generation provider for React Native applications",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./dist/index.d.ts",
package/src/index.ts CHANGED
@@ -39,8 +39,6 @@ export {
39
39
  sendChatMessage,
40
40
  buildChatHistory,
41
41
  trimChatHistory,
42
- resolveAudioMimeType,
43
- resolveImageMimeType,
44
42
  type ChatSendResult,
45
43
  type ChatHistoryMessage,
46
44
  type SendChatMessageOptions,
@@ -4,6 +4,7 @@ import { createGeminiError } from "../utils/error-mapper.util";
4
4
  import { GeminiError } from "../../domain/entities";
5
5
  import type { GeminiContent, GeminiGenerationConfig } from "../../domain/entities";
6
6
  import type { GenerativeModel } from "@google/generative-ai";
7
+ import type { Part } from "@google/generative-ai";
7
8
 
8
9
  export interface BaseRequestOptions {
9
10
  model: string;
@@ -15,7 +16,7 @@ export interface BaseRequestOptions {
15
16
  export abstract class BaseGeminiService {
16
17
  protected validateAndPrepare(options: BaseRequestOptions): {
17
18
  genModel: GenerativeModel;
18
- sdkContents: Array<{ role: string; parts: Array<{ text: string }> }>;
19
+ sdkContents: Array<{ role: string; parts: Part[] }>;
19
20
  } {
20
21
  if (!options.contents || options.contents.length === 0) {
21
22
  throw new Error("Contents array cannot be empty");
@@ -44,7 +45,7 @@ export abstract class BaseGeminiService {
44
45
  }
45
46
 
46
47
  protected createRequestOptions(
47
- sdkContents: Array<{ role: string; parts: Array<{ text: string }> }>,
48
+ sdkContents: Array<{ role: string; parts: Part[] }>,
48
49
  generationConfig?: GeminiGenerationConfig
49
50
  ) {
50
51
  return { contents: sdkContents, generationConfig };
@@ -1,6 +1,7 @@
1
1
  import type { ChatSession as SdkChatSession, Part } from "@google/generative-ai";
2
2
  import { geminiClient } from "./GeminiClient";
3
3
  import { DEFAULT_MODELS } from "../../domain/entities";
4
+ import { trimArrayByCharBudget } from "../utils/text-calculations.util";
4
5
  import type {
5
6
  GeminiChatConfig,
6
7
  GeminiGenerationConfig,
@@ -45,41 +46,6 @@ export interface SendChatMessageOptions {
45
46
  historyMinMessages?: number;
46
47
  }
47
48
 
48
- // ─── MIME Utilities ──────────────────────────────────────────────────────────
49
-
50
- const AUDIO_MIME: Record<string, string> = {
51
- mp3: "audio/mpeg",
52
- wav: "audio/wav",
53
- ogg: "audio/ogg",
54
- flac: "audio/flac",
55
- aac: "audio/aac",
56
- mp4: "audio/mp4",
57
- m4a: "audio/mp4",
58
- caf: "audio/mp4",
59
- "3gp": "audio/3gpp",
60
- };
61
-
62
- /** Resolve MIME type for an audio file extension */
63
- export function resolveAudioMimeType(extension: string): string {
64
- return AUDIO_MIME[extension.toLowerCase()] ?? "audio/mp4";
65
- }
66
-
67
- const IMAGE_MIME: Record<string, string> = {
68
- png: "image/png",
69
- jpg: "image/jpeg",
70
- jpeg: "image/jpeg",
71
- webp: "image/webp",
72
- gif: "image/gif",
73
- heic: "image/heic",
74
- heif: "image/heif",
75
- bmp: "image/bmp",
76
- };
77
-
78
- /** Resolve MIME type for an image file extension */
79
- export function resolveImageMimeType(extension: string): string {
80
- return IMAGE_MIME[extension.toLowerCase()] ?? "image/jpeg";
81
- }
82
-
83
49
  // ─── History Utilities ───────────────────────────────────────────────────────
84
50
 
85
51
  /**
@@ -102,10 +68,8 @@ export function buildChatHistory(
102
68
 
103
69
  if (last && last.role === role) {
104
70
  // Merge by extracting all text from existing parts and appending new content
105
- const existingText = last.parts
106
- .map((p) => ("text" in p ? (p.text ?? "") : ""))
107
- .filter(Boolean)
108
- .join("");
71
+ // Optimized: reduce instead of map + filter + join
72
+ const existingText = last.parts.reduce((acc, p) => acc + ("text" in p ? p.text : ""), "");
109
73
  last.parts = [{ text: existingText + "\n" + m.content }];
110
74
  } else {
111
75
  result.push({ role, parts: [{ text: m.content }] });
@@ -124,19 +88,12 @@ export function trimChatHistory(
124
88
  maxChars = 12000,
125
89
  minMessages = 4,
126
90
  ): ChatHistoryMessage[] {
127
- if (history.length <= minMessages) return history;
128
-
129
- let totalChars = 0;
130
- const trimmed: ChatHistoryMessage[] = [];
131
-
132
- for (let i = history.length - 1; i >= 0; i--) {
133
- const chars = history[i].content.length;
134
- if (trimmed.length >= minMessages && totalChars + chars > maxChars) break;
135
- trimmed.unshift(history[i]);
136
- totalChars += chars;
137
- }
138
-
139
- return trimmed;
91
+ return trimArrayByCharBudget(
92
+ history,
93
+ (m) => m.content.length,
94
+ maxChars,
95
+ minMessages,
96
+ );
140
97
  }
141
98
 
142
99
  // ─── Low-level: createChatSession ────────────────────────────────────────────
@@ -164,15 +121,6 @@ export function createChatSession(config: GeminiChatConfig = {}) {
164
121
 
165
122
  return {
166
123
  async send(parts: GeminiMessagePart[]): Promise<ChatSendResult> {
167
- if (__DEV__) {
168
- console.log("[ChatSession.send] >>> SENDING TO GEMINI SDK");
169
- console.log("[ChatSession.send] parts count:", parts.length);
170
- parts.forEach((p, i) => {
171
- if ("text" in p) console.log(`[ChatSession.send] part[${i}] text:`, (p.text ?? "").substring(0, 200));
172
- if ("inlineData" in p) console.log(`[ChatSession.send] part[${i}] inlineData: mime=${(p as GeminiInlineDataPart).inlineData.mimeType}, size=${(p as GeminiInlineDataPart).inlineData.data.length}`);
173
- });
174
- }
175
-
176
124
  const result = await chat.sendMessage(parts as Part[]);
177
125
  if (!result.response) throw new Error("No response from Gemini SDK");
178
126
  const candidate = result.response.candidates?.[0];
@@ -188,15 +136,6 @@ export function createChatSession(config: GeminiChatConfig = {}) {
188
136
  throw new Error("No text content in response");
189
137
  }
190
138
 
191
- if (__DEV__) {
192
- console.log("[ChatSession.send] <<< GEMINI SDK RESPONSE");
193
- console.log("[ChatSession.send] finishReason:", candidate?.finishReason ?? "N/A");
194
- console.log("[ChatSession.send] safetyRatings:", JSON.stringify(candidate?.safetyRatings ?? []));
195
- console.log("[ChatSession.send] response text length:", text.length);
196
- console.log("[ChatSession.send] response text:", text.substring(0, 500));
197
- console.log("[ChatSession.send] response FULL text:", text);
198
- }
199
-
200
139
  return {
201
140
  text,
202
141
  finishReason: candidate?.finishReason,
@@ -228,40 +167,14 @@ export async function sendChatMessage(
228
167
  throw new Error("Message cannot be empty");
229
168
  }
230
169
 
231
- if (__DEV__) {
232
- console.log("═══════════════════════════════════════════════════");
233
- console.log("[sendChatMessage] >>> START");
234
- console.log("[sendChatMessage] model:", opts.model ?? DEFAULT_MODELS.CHAT);
235
- console.log("[sendChatMessage] message:", opts.message.substring(0, 200));
236
- console.log("[sendChatMessage] history count:", opts.history.length);
237
- console.log("[sendChatMessage] systemPrompt length:", opts.systemPrompt?.length ?? 0);
238
- console.log("[sendChatMessage] systemPrompt (first 500):", opts.systemPrompt?.substring(0, 500));
239
- console.log("[sendChatMessage] generationConfig:", JSON.stringify(opts.generationConfig));
240
- console.log("[sendChatMessage] safetySettings:", opts.safetySettings ? JSON.stringify(opts.safetySettings) : "USING DEFAULT (BLOCK_NONE)");
241
- console.log("[sendChatMessage] attachments:", opts.attachments?.length ?? 0);
242
- }
243
-
244
170
  const trimmed = trimChatHistory(
245
171
  opts.history,
246
172
  opts.historyMaxChars,
247
173
  opts.historyMinMessages,
248
174
  );
249
175
 
250
- if (__DEV__) {
251
- console.log("[sendChatMessage] history after trim:", trimmed.length, "messages");
252
- trimmed.forEach((m, i) => console.log(`[sendChatMessage] history[${i}]: role=${m.role}, content=${m.content.substring(0, 100)}`));
253
- }
254
-
255
176
  const geminiHistory = buildChatHistory(trimmed);
256
177
 
257
- if (__DEV__) {
258
- console.log("[sendChatMessage] geminiHistory (SDK format):", geminiHistory.length, "turns");
259
- geminiHistory.forEach((h, i) => {
260
- const text = "text" in h.parts[0] ? (h.parts[0] as { text: string }).text : "[non-text]";
261
- console.log(`[sendChatMessage] geminiHistory[${i}]: role=${h.role}, text=${text.substring(0, 100)}`);
262
- });
263
- }
264
-
265
178
  const session = createChatSession({
266
179
  model: opts.model ?? DEFAULT_MODELS.CHAT,
267
180
  systemInstruction: opts.systemPrompt,
@@ -277,16 +190,7 @@ export async function sendChatMessage(
277
190
 
278
191
  const result = await session.send(parts);
279
192
 
280
- if (__DEV__) {
281
- console.log("[sendChatMessage] finishReason:", result.finishReason);
282
- console.log("[sendChatMessage] response text length:", result.text.length);
283
- console.log("[sendChatMessage] response FULL:", result.text);
284
- console.log("[sendChatMessage] <<< END");
285
- console.log("═══════════════════════════════════════════════════");
286
- }
287
-
288
193
  if (result.finishReason === "SAFETY") {
289
- if (__DEV__) console.warn("[sendChatMessage] ⚠️ SAFETY FILTER TRIGGERED but has text, returning partial");
290
194
  if (result.text.trim()) return result.text;
291
195
  throw new Error("Response blocked by safety filter.");
292
196
  }
@@ -26,28 +26,23 @@ class GeminiClient {
26
26
  private initialized = false;
27
27
 
28
28
  initialize(config: GeminiConfig): void {
29
- if (!config.apiKey || config.apiKey.trim().length < 10) {
29
+ const apiKey = config.apiKey?.trim();
30
+
31
+ if (!apiKey || apiKey.length < 10) {
30
32
  throw new Error("API key is required and must be at least 10 characters");
31
33
  }
32
34
 
35
+ // Basic format validation for Google AI API keys (starts with "AIza")
36
+ if (!apiKey.startsWith("AIza")) {
37
+ throw new Error('Invalid API key format. Google AI API keys should start with "AIza"');
38
+ }
39
+
33
40
  // Allow re-initialization with new config (e.g. API key change)
34
- this.client = new GoogleGenerativeAI(config.apiKey);
35
- this.config = { ...DEFAULT_CONFIG, ...config };
41
+ this.client = new GoogleGenerativeAI(apiKey);
42
+ this.config = { ...DEFAULT_CONFIG, ...config, apiKey };
36
43
  this.initialized = true;
37
44
  }
38
45
 
39
- isInitialized(): boolean {
40
- return this.initialized;
41
- }
42
-
43
- getConfig(): GeminiConfig | null {
44
- return this.config;
45
- }
46
-
47
- getClient(): GoogleGenerativeAI | null {
48
- return this.client;
49
- }
50
-
51
46
  /**
52
47
  * Returns a GenerativeModel configured with optional safety settings and system instruction.
53
48
  * When no safety settings are provided, defaults to BLOCK_NONE for all categories.
@@ -71,18 +66,35 @@ class GeminiClient {
71
66
 
72
67
  // Map package safety settings to SDK format
73
68
  const sdkSafety: SafetySetting[] = opts.safetySettings
74
- ? opts.safetySettings.map((s) => ({
75
- category: s.category as unknown as HarmCategory,
76
- threshold: s.threshold as unknown as HarmBlockThreshold,
77
- }))
69
+ ? opts.safetySettings.map((s) => {
70
+ // Validate safety settings to prevent runtime errors
71
+ const validCategories = [
72
+ HarmCategory.HARM_CATEGORY_HARASSMENT,
73
+ HarmCategory.HARM_CATEGORY_HATE_SPEECH,
74
+ HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
75
+ HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
76
+ ];
77
+
78
+ const validThresholds = [
79
+ HarmBlockThreshold.BLOCK_NONE,
80
+ HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
81
+ HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
82
+ HarmBlockThreshold.BLOCK_ONLY_HIGH,
83
+ ];
84
+
85
+ // Check if category and threshold are valid enum values
86
+ const category = validCategories.includes(s.category as unknown as HarmCategory)
87
+ ? (s.category as unknown as HarmCategory)
88
+ : HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT; // Fallback to safest
89
+
90
+ const threshold = validThresholds.includes(s.threshold as unknown as HarmBlockThreshold)
91
+ ? (s.threshold as unknown as HarmBlockThreshold)
92
+ : HarmBlockThreshold.BLOCK_NONE; // Fallback to most permissive
93
+
94
+ return { category, threshold };
95
+ })
78
96
  : PERMISSIVE_SAFETY;
79
97
 
80
- if (__DEV__) {
81
- console.log("[GeminiClient.getModel] model:", effectiveModel);
82
- console.log("[GeminiClient.getModel] systemInstruction length:", opts.systemInstruction?.length ?? 0);
83
- console.log("[GeminiClient.getModel] safetySettings:", JSON.stringify(sdkSafety.map(s => ({ category: s.category, threshold: s.threshold }))));
84
- }
85
-
86
98
  return this.client.getGenerativeModel({
87
99
  model: effectiveModel,
88
100
  ...(opts.systemInstruction && { systemInstruction: opts.systemInstruction }),
@@ -5,7 +5,6 @@ import type { GenerationConfig } from "@google/generative-ai";
5
5
  import type {
6
6
  GeminiContent,
7
7
  GeminiGenerationConfig,
8
- GeminiResponse,
9
8
  } from "../../domain/entities";
10
9
 
11
10
  class StructuredTextService {
@@ -41,18 +40,12 @@ class StructuredTextService {
41
40
  signal,
42
41
  );
43
42
 
44
- return this.parseJSONResponse<T>(response);
45
- }
46
-
47
- private parseJSONResponse<T>(response: GeminiResponse): T {
48
43
  const candidates = response.candidates;
49
-
50
44
  if (!candidates || candidates.length === 0) {
51
45
  throw new Error("No candidates in response");
52
46
  }
53
47
 
54
48
  const text = extractTextFromParts(candidates[0]?.content?.parts);
55
-
56
49
  if (!text || text.trim().length === 0) {
57
50
  throw new Error("Empty response received from Gemini");
58
51
  }
@@ -1,10 +1,9 @@
1
1
 
2
2
  export interface TelemetryEvent {
3
- type: "request" | "response" | "error" | "retry";
3
+ type: "error";
4
4
  timestamp: number;
5
5
  model?: string;
6
6
  feature?: string;
7
- duration?: number;
8
7
  metadata?: Record<string, unknown>;
9
8
  }
10
9
 
@@ -18,6 +17,15 @@ class TelemetryHooks {
18
17
 
19
18
  /**
20
19
  * Register a telemetry listener
20
+ *
21
+ * @returns Unsubscribe function - IMPORTANT: Call this when done listening to prevent memory leaks
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * const unsubscribe = telemetryHooks.subscribe((event) => console.log(event));
26
+ * // ... later when done
27
+ * unsubscribe(); // Prevents memory leak
28
+ * ```
21
29
  */
22
30
  subscribe(listener: TelemetryListener): () => void {
23
31
  this.listeners.push(listener);
@@ -69,35 +77,6 @@ class TelemetryHooks {
69
77
  }
70
78
  }
71
79
 
72
- /**
73
- * Log request start
74
- */
75
- logRequest(model: string, feature?: string): number {
76
- const timestamp = Date.now();
77
- this.emit({
78
- type: "request",
79
- timestamp,
80
- model,
81
- feature,
82
- });
83
- return timestamp;
84
- }
85
-
86
- /**
87
- * Log response received
88
- */
89
- logResponse(model: string, startTime: number, feature?: string, metadata?: Record<string, unknown>): void {
90
- const now = Date.now();
91
- this.emit({
92
- type: "response",
93
- timestamp: now,
94
- model,
95
- feature,
96
- duration: now - startTime,
97
- metadata,
98
- });
99
- }
100
-
101
80
  /**
102
81
  * Log error
103
82
  */
@@ -114,19 +93,6 @@ class TelemetryHooks {
114
93
  });
115
94
  }
116
95
 
117
- /**
118
- * Log retry attempt
119
- */
120
- logRetry(model: string, attempt: number, feature?: string): void {
121
- this.emit({
122
- type: "retry",
123
- timestamp: Date.now(),
124
- model,
125
- feature,
126
- metadata: { attempt },
127
- });
128
- }
129
-
130
96
  /**
131
97
  * Remove a specific listener
132
98
  */
@@ -139,32 +105,6 @@ class TelemetryHooks {
139
105
  this.failedListeners.delete(listener);
140
106
  this.listenerFailureCounts.delete(listener);
141
107
  }
142
-
143
- /**
144
- * Reset failure counts for all listeners (clear blacklist)
145
- */
146
- resetFailures(): void {
147
- this.failedListeners.clear();
148
- for (const listener of this.listeners) {
149
- this.listenerFailureCounts.set(listener, 0);
150
- }
151
- }
152
-
153
- /**
154
- * Clear all listeners
155
- */
156
- clear(): void {
157
- this.listeners = [];
158
- this.failedListeners.clear();
159
- this.listenerFailureCounts.clear();
160
- }
161
-
162
- /**
163
- * Get current listener count
164
- */
165
- getListenerCount(): number {
166
- return this.listeners.length;
167
- }
168
108
  }
169
109
 
170
110
  export const telemetryHooks = new TelemetryHooks();
@@ -26,7 +26,6 @@ export interface AsyncStateSetters<T = string, U = unknown> {
26
26
  */
27
27
  export interface AsyncStateConfig<T = string> {
28
28
  resetState?: boolean;
29
- throwOnError?: boolean;
30
29
  transformResult?: (result: T) => T;
31
30
  }
32
31
 
@@ -60,7 +59,6 @@ export async function executeWithState<T, U = unknown>(
60
59
  ): Promise<T | null> {
61
60
  const {
62
61
  resetState = true,
63
- throwOnError = false,
64
62
  transformResult,
65
63
  } = config;
66
64
 
@@ -88,10 +86,6 @@ export async function executeWithState<T, U = unknown>(
88
86
  setters.setError(errorMessage);
89
87
  callbacks.onError?.(errorMessage);
90
88
 
91
- if (throwOnError) {
92
- throw err;
93
- }
94
-
95
89
  return null;
96
90
  } finally {
97
91
  setters.setIsLoading(false);
@@ -3,6 +3,7 @@
3
3
  * Handles transformation between domain content and SDK format
4
4
  */
5
5
 
6
+ import type { Part } from "@google/generative-ai";
6
7
  import type {
7
8
  GeminiContent,
8
9
  GeminiPart,
@@ -49,14 +50,21 @@ function isValidProbability(value: string): value is GeminiSafetyRating["probabi
49
50
 
50
51
  /**
51
52
  * Convert domain content to SDK format
53
+ * Preserves both text and inlineData parts
52
54
  */
53
55
  export function toSdkContent(contents: GeminiContent[]): Array<{
54
56
  role: string;
55
- parts: Array<{ text: string }>;
57
+ parts: Part[];
56
58
  }> {
57
59
  return contents.map((content) => ({
58
60
  role: content.role || "user",
59
- parts: content.parts.map((part) => ({ text: part.text })),
61
+ parts: content.parts.map((part) => {
62
+ if ("text" in part) {
63
+ return { text: part.text } as Part;
64
+ }
65
+ // For inlineData and other part types, cast to SDK Part type
66
+ return part as Part;
67
+ }),
60
68
  }));
61
69
  }
62
70
 
@@ -21,12 +21,18 @@ export async function processStream(
21
21
  let fullText = "";
22
22
 
23
23
  for await (const chunk of stream) {
24
- // chunk.text() errors are critical (e.g. safety blocks mid-stream)
25
- // and must propagate, unlike callback errors which are safe to swallow
26
- const chunkText = chunk.text();
27
- if (chunkText) {
28
- fullText += chunkText;
29
- safeCallChunk(onChunk, chunkText, onError);
24
+ try {
25
+ // chunk.text() can throw critical errors (e.g. safety blocks mid-stream)
26
+ // and must propagate, unlike callback errors which are safe to swallow
27
+ const chunkText = chunk.text();
28
+ if (chunkText) {
29
+ fullText += chunkText;
30
+ safeCallChunk(onChunk, chunkText, onError);
31
+ }
32
+ } catch (chunkError) {
33
+ // Log chunk error but re-throw to properly fail the stream
34
+ logError(onError, chunkError, "stream-chunk");
35
+ throw chunkError;
30
36
  }
31
37
  }
32
38
 
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Text Calculation Utilities
3
+ * Reusable functions for text length and character budget calculations
4
+ */
5
+
6
+ /**
7
+ * Calculate total character count in an array of text objects
8
+ * @param items - Array of objects with text content
9
+ * @param getText - Function to extract text from each item
10
+ * @returns Total character count
11
+ */
12
+ export function calculateTotalChars<T>(
13
+ items: readonly T[],
14
+ getItemLength: (item: T) => number
15
+ ): number {
16
+ return items.reduce((sum, item) => sum + getItemLength(item), 0);
17
+ }
18
+
19
+ /**
20
+ * Check if adding an item would exceed the character budget
21
+ * @param currentTotal - Current total character count
22
+ * @param itemLength - Length of item to add
23
+ * @param maxBudget - Maximum allowed budget
24
+ * @returns true if within budget, false otherwise
25
+ */
26
+ export function fitsWithinBudget(
27
+ currentTotal: number,
28
+ itemLength: number,
29
+ maxBudget: number
30
+ ): boolean {
31
+ return currentTotal + itemLength <= maxBudget;
32
+ }
33
+
34
+ /**
35
+ * Trim array to fit within character budget while keeping minimum items
36
+ * Keeps the last `minItems` regardless of budget, then adds as many as possible from the end
37
+ *
38
+ * @param items - Array to trim
39
+ * @param getItemLength - Function to get length of each item
40
+ * @param maxBudget - Maximum total length allowed
41
+ * @param minItems - Minimum number of items to keep (from the end)
42
+ * @returns Trimmed array
43
+ */
44
+ export function trimArrayByCharBudget<T>(
45
+ items: readonly T[],
46
+ getItemLength: (item: T) => number,
47
+ maxBudget: number,
48
+ minItems: number
49
+ ): T[] {
50
+ if (items.length <= minItems) return [...items];
51
+
52
+ // First, guarantee minimum items (from the end)
53
+ const guaranteedMin = items.slice(-minItems);
54
+ const remaining = items.slice(0, -minItems);
55
+
56
+ // Calculate current total
57
+ let totalChars = calculateTotalChars(guaranteedMin, getItemLength);
58
+ const trimmed: T[] = [...guaranteedMin];
59
+
60
+ // Add older items in reverse order until budget is exceeded
61
+ for (let i = remaining.length - 1; i >= 0; i--) {
62
+ const itemLength = getItemLength(remaining[i]);
63
+ if (!fitsWithinBudget(totalChars, itemLength, maxBudget)) break;
64
+
65
+ trimmed.unshift(remaining[i]);
66
+ totalChars += itemLength;
67
+ }
68
+
69
+ return trimmed;
70
+ }
@@ -4,7 +4,6 @@ import { DEFAULT_MODELS } from "../../domain/entities";
4
4
  import { textGeneration } from "../../infrastructure/services/TextGeneration";
5
5
  import { structuredText } from "../../infrastructure/services/StructuredText";
6
6
  import { executeWithState, type AsyncStateSetters } from "../../infrastructure/utils/async/execute-state.util";
7
- import { parseJsonResponse } from "../../infrastructure/utils/json-parser.util";
8
7
  import { useOperationManager } from "./useOperationManager";
9
8
 
10
9
  export interface UseGeminiOptions {
@@ -16,7 +15,7 @@ export interface UseGeminiOptions {
16
15
 
17
16
  export interface UseGeminiReturn {
18
17
  generate: (prompt: string) => Promise<void>;
19
- generateJSON: <T>(prompt: string, schema?: Record<string, unknown>) => Promise<T | null>;
18
+ generateJSON: <T>(prompt: string, schema: Record<string, unknown>) => Promise<T | null>;
20
19
  result: string | null;
21
20
  jsonResult: unknown;
22
21
  isGenerating: boolean;
@@ -65,7 +64,6 @@ export function useGemini(options: UseGeminiOptions = {}): UseGeminiReturn {
65
64
  },
66
65
  (text: string) => {
67
66
  setResult(text);
68
- // onSuccess is called by executeWithState via callbacks - do not call here again
69
67
  }
70
68
  );
71
69
  });
@@ -74,7 +72,7 @@ export function useGemini(options: UseGeminiOptions = {}): UseGeminiReturn {
74
72
  );
75
73
 
76
74
  const generateJSON = useCallback(
77
- async <T>(prompt: string, schema?: Record<string, unknown>): Promise<T | null> => {
75
+ async <T>(prompt: string, schema: Record<string, unknown>): Promise<T | null> => {
78
76
  return executeOperation(async (signal, _operationId) => {
79
77
  const jsonSetters: AsyncStateSetters<unknown, unknown> = {
80
78
  setIsLoading: setIsGenerating,
@@ -95,24 +93,13 @@ export function useGemini(options: UseGeminiOptions = {}): UseGeminiReturn {
95
93
  jsonSetters,
96
94
  jsonCallbacks,
97
95
  async () => {
98
- if (schema) {
99
- return structuredText.generateStructuredText<T>(
100
- model,
101
- prompt,
102
- schema,
103
- options.generationConfig,
104
- signal
105
- );
106
- }
107
-
108
- const text = await textGeneration.generateText(
96
+ return structuredText.generateStructuredText<T>(
109
97
  model,
110
98
  prompt,
111
- { ...options.generationConfig, responseMimeType: "application/json" },
99
+ schema,
100
+ options.generationConfig,
112
101
  signal
113
102
  );
114
-
115
- return parseJsonResponse<T>(text);
116
103
  },
117
104
  (parsed: unknown) => {
118
105
  setJsonResult(parsed);
@@ -109,17 +109,4 @@ export class ConfigBuilder {
109
109
  static create(): ConfigBuilder {
110
110
  return new ConfigBuilder();
111
111
  }
112
-
113
- /**
114
- * Create from existing config (for updates)
115
- */
116
- static from(config: Partial<ProviderConfig>): ConfigBuilder {
117
- const builder = new ConfigBuilder();
118
- if (config.apiKey) builder.withApiKey(config.apiKey);
119
- if (config.textModel) builder.withTextModel(config.textModel);
120
- // Apply strategy first (sets default timeout), then explicit timeout overrides it
121
- if (config.strategy) builder.withStrategy(config.strategy);
122
- if (config.timeout != null && config.timeout > 0) builder.withTimeout(config.timeout);
123
- return builder;
124
- }
125
112
  }
@@ -14,42 +14,33 @@ export interface ProviderFactoryOptions {
14
14
 
15
15
  class ProviderFactory {
16
16
  private currentConfig: ProviderConfig | null = null;
17
- private builder: ConfigBuilder | null = null;
18
17
 
19
18
  /**
20
19
  * Initialize provider with configuration
21
20
  */
22
21
  initialize(options: ProviderFactoryOptions): void {
23
- // Build configuration using builder pattern
24
- this.builder = ConfigBuilder.create()
25
- .withApiKey(options.apiKey);
22
+ // Build configuration using builder pattern (inline, no state needed)
23
+ const builder = ConfigBuilder.create().withApiKey(options.apiKey);
26
24
 
27
25
  if (options.strategy) {
28
- this.builder.withStrategy(options.strategy);
26
+ builder.withStrategy(options.strategy);
29
27
  }
30
28
 
31
29
  if (options.textModel) {
32
- this.builder.withTextModel(options.textModel);
30
+ builder.withTextModel(options.textModel);
33
31
  }
34
32
 
35
33
  if (options.timeout) {
36
- this.builder.withTimeout(options.timeout);
34
+ builder.withTimeout(options.timeout);
37
35
  }
38
36
 
39
- this.currentConfig = this.builder.build();
37
+ this.currentConfig = builder.build();
40
38
 
41
39
  // Initialize Gemini client
42
- const geminiConfig = this.builder.toGeminiConfig();
40
+ const geminiConfig = builder.toGeminiConfig();
43
41
  geminiClient.initialize(geminiConfig);
44
42
  }
45
43
 
46
- /**
47
- * Get current configuration
48
- */
49
- getConfig(): ProviderConfig | null {
50
- return this.currentConfig;
51
- }
52
-
53
44
  /**
54
45
  * Check if provider is initialized
55
46
  */
package/src/global.d.ts DELETED
@@ -1 +0,0 @@
1
- declare const __DEV__: boolean;