@umituz/react-native-ai-gemini-provider 2.0.23 → 2.0.24

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.23",
3
+ "version": "2.0.24",
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",
@@ -72,8 +72,7 @@
72
72
  "react-native-gesture-handler": "^2.30.0",
73
73
  "react-native-safe-area-context": "^5.6.2",
74
74
  "react-native-svg": "^15.15.1",
75
- "typescript": "^5.3.0",
76
- "zustand": "^5.0.9"
75
+ "typescript": "^5.3.0"
77
76
  },
78
77
  "publishConfig": {
79
78
  "access": "public"
@@ -1,29 +1,57 @@
1
+ /**
2
+ * Categories of errors that can occur with Gemini API
3
+ */
1
4
  export enum GeminiErrorType {
5
+ /** Network connectivity issues */
2
6
  NETWORK = "NETWORK",
7
+ /** API rate limit exceeded */
3
8
  RATE_LIMIT = "RATE_LIMIT",
9
+ /** Authentication/authorization failures */
4
10
  AUTHENTICATION = "AUTHENTICATION",
11
+ /** Invalid input data or parameters */
5
12
  VALIDATION = "VALIDATION",
13
+ /** Content blocked by safety filters */
6
14
  SAFETY = "SAFETY",
15
+ /** Server-side errors */
7
16
  SERVER = "SERVER",
17
+ /** Request timeout */
8
18
  TIMEOUT = "TIMEOUT",
19
+ /** API quota exceeded */
9
20
  QUOTA_EXCEEDED = "QUOTA_EXCEEDED",
21
+ /** Requested model not found */
10
22
  MODEL_NOT_FOUND = "MODEL_NOT_FOUND",
23
+ /** Unknown/unclassified error */
11
24
  UNKNOWN = "UNKNOWN",
12
25
  }
13
26
 
27
+ /**
28
+ * Detailed error information for Gemini API errors
29
+ */
14
30
  export interface GeminiErrorInfo {
31
+ /** Category of the error */
15
32
  type: GeminiErrorType;
33
+ /** Message key for i18n translation */
16
34
  messageKey: string;
35
+ /** Whether the request can be retried */
17
36
  retryable: boolean;
37
+ /** Original error that caused this error */
18
38
  originalError?: unknown;
39
+ /** HTTP status code if applicable */
19
40
  statusCode?: number;
20
41
  }
21
42
 
43
+ /**
44
+ * Structure of Gemini API error responses
45
+ */
22
46
  export interface GeminiApiError {
23
47
  error?: {
48
+ /** Error code */
24
49
  code?: number;
50
+ /** Error message */
25
51
  message?: string;
52
+ /** Error status */
26
53
  status?: string;
54
+ /** Additional error details */
27
55
  details?: Array<{
28
56
  "@type"?: string;
29
57
  reason?: string;
@@ -33,12 +61,24 @@ export interface GeminiApiError {
33
61
  };
34
62
  }
35
63
 
64
+ /**
65
+ * Custom error class for Gemini API errors
66
+ * Provides structured error information and retry capability
67
+ */
36
68
  export class GeminiError extends Error {
69
+ /** Error category */
37
70
  readonly type: GeminiErrorType;
71
+ /** Whether the operation can be retried */
38
72
  readonly retryable: boolean;
73
+ /** HTTP status code if applicable */
39
74
  readonly statusCode?: number;
75
+ /** Original error that caused this error */
40
76
  readonly originalError?: unknown;
41
77
 
78
+ /**
79
+ * Create a new GeminiError
80
+ * @param info - Error information
81
+ */
42
82
  constructor(info: GeminiErrorInfo) {
43
83
  super(info.messageKey);
44
84
  this.name = "GeminiError";
@@ -53,14 +93,28 @@ export class GeminiError extends Error {
53
93
  }
54
94
  }
55
95
 
96
+ /**
97
+ * Check if this error is retryable
98
+ * @returns true if the operation can be retried
99
+ */
56
100
  isRetryable(): boolean {
57
101
  return this.retryable;
58
102
  }
59
103
 
104
+ /**
105
+ * Get the error type
106
+ * @returns The error category
107
+ */
60
108
  getErrorType(): GeminiErrorType {
61
109
  return this.type;
62
110
  }
63
111
 
112
+ /**
113
+ * Create a GeminiError from an unknown error
114
+ * @param error - The original error
115
+ * @param info - Error information
116
+ * @returns A new GeminiError instance
117
+ */
64
118
  static fromError(error: unknown, info: GeminiErrorInfo): GeminiError {
65
119
  const geminiError = new GeminiError(info);
66
120
 
@@ -1,46 +1,85 @@
1
1
  import type { GenerationConfig } from "@google/generative-ai";
2
2
 
3
+ /**
4
+ * Configuration for Gemini AI client initialization
5
+ */
3
6
  export interface GeminiConfig {
7
+ /** API key for authentication */
4
8
  apiKey: string;
9
+ /** Optional base URL for API requests */
5
10
  baseUrl?: string;
11
+ /** Default timeout in milliseconds */
6
12
  defaultTimeoutMs?: number;
13
+ /** Default model to use for text generation */
7
14
  textModel?: string;
8
15
  }
9
16
 
17
+ /**
18
+ * Generation configuration for AI requests
19
+ * Extends the SDK's GenerationConfig with proper schema typing
20
+ */
10
21
  export type GeminiGenerationConfig = Omit<GenerationConfig, "responseSchema"> & {
11
22
  responseSchema?: GenerationConfig["responseSchema"];
12
23
  };
13
24
 
25
+ /**
26
+ * Harm categories for content safety filtering
27
+ */
14
28
  export type GeminiHarmCategory =
15
29
  | "HARM_CATEGORY_HARASSMENT"
16
30
  | "HARM_CATEGORY_HATE_SPEECH"
17
31
  | "HARM_CATEGORY_SEXUALLY_EXPLICIT"
18
32
  | "HARM_CATEGORY_DANGEROUS_CONTENT";
19
33
 
34
+ /**
35
+ * Threshold levels for blocking harmful content
36
+ */
20
37
  export type GeminiHarmBlockThreshold =
21
38
  | "BLOCK_NONE"
22
39
  | "BLOCK_LOW_AND_ABOVE"
23
40
  | "BLOCK_MEDIUM_AND_ABOVE"
24
41
  | "BLOCK_ONLY_HIGH";
25
42
 
43
+ /**
44
+ * Content structure for Gemini API requests
45
+ */
26
46
  export interface GeminiContent {
47
+ /** Array of content parts (text, images, etc.) */
27
48
  parts: GeminiPart[];
49
+ /** Role of the content creator (user or model) */
28
50
  role?: "user" | "model";
29
51
  }
30
52
 
53
+ /**
54
+ * Individual content part
55
+ */
31
56
  export type GeminiPart = { text: string };
32
57
 
58
+ /**
59
+ * Response structure from Gemini API
60
+ */
33
61
  export interface GeminiResponse {
62
+ /** Array of response candidates */
34
63
  candidates?: GeminiCandidate[];
64
+ /** Token usage information */
35
65
  usageMetadata?: GeminiUsageMetadata;
36
66
  }
37
67
 
68
+ /**
69
+ * Individual response candidate
70
+ */
38
71
  export interface GeminiCandidate {
72
+ /** Generated content */
39
73
  content: GeminiContent;
74
+ /** Reason for generation completion */
40
75
  finishReason?: GeminiFinishReason;
76
+ /** Safety ratings for the content */
41
77
  safetyRatings?: GeminiSafetyRating[];
42
78
  }
43
79
 
80
+ /**
81
+ * Reasons why generation finished
82
+ */
44
83
  export type GeminiFinishReason =
45
84
  | "FINISH_REASON_UNSPECIFIED"
46
85
  | "STOP"
@@ -49,15 +88,26 @@ export type GeminiFinishReason =
49
88
  | "RECITATION"
50
89
  | "OTHER";
51
90
 
91
+ /**
92
+ * Safety rating for generated content
93
+ */
52
94
  export interface GeminiSafetyRating {
95
+ /** Category of safety check */
53
96
  category: GeminiHarmCategory;
97
+ /** Probability of content being unsafe */
54
98
  probability: "NEGLIGIBLE" | "LOW" | "MEDIUM" | "HIGH";
99
+ /** Whether the content was blocked */
55
100
  blocked?: boolean;
56
101
  }
57
102
 
103
+ /**
104
+ * Token usage metadata for the request
105
+ */
58
106
  export interface GeminiUsageMetadata {
107
+ /** Number of tokens in the prompt */
59
108
  promptTokenCount?: number;
109
+ /** Number of tokens in the response candidates */
60
110
  candidatesTokenCount?: number;
111
+ /** Total number of tokens used */
61
112
  totalTokenCount?: number;
62
113
  }
63
-
@@ -1,13 +1,26 @@
1
+ /**
2
+ * Available Gemini AI models
3
+ */
1
4
  export const GEMINI_MODELS = {
5
+ /** Text generation models */
2
6
  TEXT: {
7
+ /** Lightweight flash model for fast text generation */
3
8
  FLASH_LITE: "gemini-2.5-flash-lite",
4
9
  },
5
10
  } as const;
6
11
 
12
+ /**
13
+ * Default models to use for each category
14
+ */
7
15
  export const DEFAULT_MODELS = {
16
+ /** Default model for text generation */
8
17
  TEXT: GEMINI_MODELS.TEXT.FLASH_LITE,
9
18
  } as const;
10
19
 
20
+ /**
21
+ * Pricing information for Gemini models
22
+ * Prices are per 1M tokens (USD)
23
+ */
11
24
  export const MODEL_PRICING = {
12
25
  [GEMINI_MODELS.TEXT.FLASH_LITE]: { input: 0.10, output: 0.40, freePerDay: 1000 },
13
26
  } as const;
package/src/index.ts CHANGED
@@ -37,22 +37,70 @@ export type { GeminiProviderConfig } from "./infrastructure/services";
37
37
 
38
38
  // Utils
39
39
  export {
40
+ // Error handling
40
41
  mapGeminiError,
41
42
  isGeminiErrorRetryable,
42
43
  categorizeGeminiError,
43
44
  createGeminiError,
45
+ // Data transformation
44
46
  extractTextFromResponse,
47
+ cleanJsonText,
48
+ parseJsonResponse,
49
+ safeParseJson,
50
+ extractJsonFromText,
51
+ toSdkContent,
52
+ createTextContent,
53
+ transformCandidate,
54
+ transformResponse,
55
+ extractTextFromParts,
56
+ // Performance
45
57
  measureAsync,
46
58
  measureSync,
47
59
  debounce,
48
60
  throttle,
49
61
  PerformanceTimer,
62
+ // Rate limiting
50
63
  RateLimiter,
64
+ // Retry logic
65
+ retryWithBackoff,
66
+ retryIf,
67
+ retryWithFixedDelay,
68
+ shouldRetryNetworkError,
69
+ createRetryPredicate,
70
+ // Validation
71
+ validateModelName,
72
+ validateApiKey,
73
+ validateSchema,
74
+ validatePrompt,
75
+ validateTimeout,
76
+ isValidObject,
77
+ validateRequiredFields,
78
+ // Environment
79
+ getRequiredEnv,
80
+ getOptionalEnv,
81
+ getEnvNumber,
82
+ getEnvBoolean,
83
+ loadGeminiEnv,
84
+ getApiKeyFromEnv,
85
+ isDevelopment,
86
+ isDebugEnabled,
87
+ validateEnv,
88
+ getGeminiConfigFromEnv,
89
+ // Async state management
90
+ executeWithState,
91
+ createDebouncedAsync,
92
+ createMemoizedAsync,
51
93
  } from "./infrastructure/utils";
52
94
 
53
95
  export type {
54
96
  PerformanceMetrics,
55
97
  RateLimiterOptions,
98
+ RetryOptions,
99
+ RetryResult,
100
+ EnvConfig,
101
+ AsyncStateCallbacks,
102
+ AsyncStateSetters,
103
+ AsyncStateConfig,
56
104
  } from "./infrastructure/utils";
57
105
 
58
106
  // Hooks
@@ -1,6 +1,7 @@
1
1
  import { GoogleGenerativeAI, type GenerativeModel } from "@google/generative-ai";
2
2
  import { DEFAULT_MODELS } from "../../domain/entities";
3
3
  import type { GeminiConfig } from "../../domain/entities";
4
+ import { validateModelName, validateApiKey } from "../utils/validation.util";
4
5
 
5
6
  const DEFAULT_CONFIG: Partial<GeminiConfig> = {
6
7
  textModel: DEFAULT_MODELS.TEXT,
@@ -11,11 +12,19 @@ class GeminiClientCoreService {
11
12
  private config: GeminiConfig | null = null;
12
13
  private initialized = false;
13
14
 
15
+ /**
16
+ * Initialize the Gemini client with configuration
17
+ *
18
+ * @throws {Error} If already initialized or API key is invalid
19
+ */
14
20
  initialize(config: GeminiConfig): void {
15
21
  if (this.initialized) {
16
22
  throw new Error("Gemini client already initialized. Call reset() before re-initializing with new config.");
17
23
  }
18
24
 
25
+ // Validate API key
26
+ validateApiKey(config.apiKey);
27
+
19
28
  this.client = new GoogleGenerativeAI(config.apiKey);
20
29
  this.config = { ...DEFAULT_CONFIG, ...config };
21
30
  this.initialized = true;
@@ -39,19 +48,6 @@ class GeminiClientCoreService {
39
48
  }
40
49
  }
41
50
 
42
- /**
43
- * Validate model name format (allows any valid model string)
44
- */
45
- private validateModel(modelName: string): void {
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
- }
49
-
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-".`);
53
- }
54
- }
55
51
 
56
52
  getModel(modelName?: string): GenerativeModel {
57
53
  this.validateInitialization();
@@ -62,8 +58,8 @@ class GeminiClientCoreService {
62
58
 
63
59
  const effectiveModel = modelName || this.config?.textModel || DEFAULT_MODELS.TEXT;
64
60
 
65
- // Validate model name format (not against hardcoded list)
66
- this.validateModel(effectiveModel);
61
+ // Validate model name format
62
+ validateModelName(effectiveModel);
67
63
 
68
64
  return this.client.getGenerativeModel({ model: effectiveModel });
69
65
  }
@@ -2,6 +2,7 @@
2
2
  import type { GeminiConfig } from "../../domain/entities";
3
3
  import { geminiClientCoreService } from "./gemini-client-core.service";
4
4
  import { geminiStructuredTextService } from "./gemini-structured-text.service";
5
+ import { validatePrompt } from "../utils/validation.util";
5
6
 
6
7
  export type GeminiProviderConfig = GeminiConfig;
7
8
 
@@ -9,6 +10,11 @@ export class GeminiProvider {
9
10
  readonly providerId = "gemini";
10
11
  readonly providerName = "Google Gemini";
11
12
 
13
+ /**
14
+ * Initialize the Gemini provider
15
+ *
16
+ * @throws {Error} If already initialized or configuration is invalid
17
+ */
12
18
  initialize(config: GeminiProviderConfig): void {
13
19
  if (geminiClientCoreService.isInitialized()) {
14
20
  throw new Error("Provider already initialized. Call reset() before re-initializing with new config.");
@@ -16,22 +22,39 @@ export class GeminiProvider {
16
22
  geminiClientCoreService.initialize(config);
17
23
  }
18
24
 
25
+ /**
26
+ * Check if provider is initialized
27
+ */
19
28
  isInitialized(): boolean {
20
29
  return geminiClientCoreService.isInitialized();
21
30
  }
22
31
 
32
+ /**
33
+ * Reset the provider to uninitialized state
34
+ */
23
35
  reset(): void {
24
36
  geminiClientCoreService.reset();
25
37
  }
26
38
 
27
39
  /**
28
40
  * Generate structured JSON response
41
+ *
42
+ * @throws {GeminiError} For API-specific errors
43
+ * @throws {Error} For validation or network errors
29
44
  */
30
45
  async generateStructuredText<T>(
31
46
  prompt: string,
32
47
  schema: Record<string, unknown>,
33
48
  model: string,
34
49
  ): Promise<T> {
50
+ // Validate inputs
51
+ validatePrompt(prompt);
52
+
53
+ // Check if initialized
54
+ if (!this.isInitialized()) {
55
+ throw new Error("Provider not initialized. Call initialize() first.");
56
+ }
57
+
35
58
  return geminiStructuredTextService.generateStructuredText<T>(model, prompt, schema);
36
59
  }
37
60
  }
@@ -1,5 +1,8 @@
1
1
 
2
2
  import { geminiClientCoreService } from "./gemini-client-core.service";
3
+ import { toSdkContent } from "../utils/content-mapper.util";
4
+ import { createGeminiError } from "../utils/error-mapper.util";
5
+ import { telemetryHooks } from "../telemetry";
3
6
  import type {
4
7
  GeminiContent,
5
8
  GeminiGenerationConfig,
@@ -8,6 +11,18 @@ import type {
8
11
  class GeminiStreamingService {
9
12
  /**
10
13
  * Stream content generation
14
+ *
15
+ * @throws {GeminiError} For API-specific errors
16
+ * @throws {Error} For validation or network errors
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * const fullText = await streamContent(
21
+ * "gemini-2.5-flash-lite",
22
+ * [{ parts: [{ text: "Hello" }], role: "user" }],
23
+ * (chunk) => console.log(chunk)
24
+ * );
25
+ * ```
11
26
  */
12
27
  async streamContent(
13
28
  model: string,
@@ -16,33 +31,76 @@ class GeminiStreamingService {
16
31
  generationConfig?: GeminiGenerationConfig,
17
32
  signal?: AbortSignal,
18
33
  ): Promise<string> {
19
- const genModel = geminiClientCoreService.getModel(model);
34
+ // Validate input
35
+ if (!contents || contents.length === 0) {
36
+ throw new Error("Contents array cannot be empty");
37
+ }
38
+
39
+ if (typeof onChunk !== "function") {
40
+ throw new Error("onChunk must be a function");
41
+ }
20
42
 
21
- const sdkContents = contents.map((content) => ({
22
- role: content.role || "user",
23
- parts: content.parts.map((part) => ({ text: part.text })),
24
- }));
43
+ // Check for early abort
44
+ if (signal?.aborted) {
45
+ throw new Error("Stream generation was aborted");
46
+ }
47
+
48
+ try {
49
+ const genModel = geminiClientCoreService.getModel(model);
50
+ const sdkContents = toSdkContent(contents);
25
51
 
26
- const requestOptions = {
27
- contents: sdkContents as Parameters<typeof genModel.generateContentStream>[0] extends { contents: infer C } ? C : never,
28
- generationConfig,
29
- };
52
+ const requestOptions = {
53
+ contents: sdkContents as Parameters<typeof genModel.generateContentStream>[0] extends { contents: infer C } ? C : never,
54
+ generationConfig,
55
+ };
30
56
 
31
- const result = signal
32
- ? await genModel.generateContentStream(requestOptions, { signal })
33
- : await genModel.generateContentStream(requestOptions);
57
+ const result = signal
58
+ ? await genModel.generateContentStream(requestOptions, { signal })
59
+ : await genModel.generateContentStream(requestOptions);
34
60
 
35
- let fullText = "";
61
+ let fullText = "";
36
62
 
37
- for await (const chunk of result.stream) {
38
- const chunkText = chunk.text();
39
- if (chunkText) {
40
- fullText += chunkText;
41
- onChunk(chunkText);
63
+ for await (const chunk of result.stream) {
64
+ try {
65
+ const chunkText = chunk.text();
66
+ if (chunkText) {
67
+ fullText += chunkText;
68
+ // Safely call onChunk - errors in callback won't break the stream
69
+ try {
70
+ onChunk(chunkText);
71
+ } catch (callbackError) {
72
+ try {
73
+ telemetryHooks.logError(model, callbackError instanceof Error ? callbackError : new Error(String(callbackError)), "stream-callback");
74
+ } catch {
75
+ // Silently ignore telemetry errors to prevent breaking the stream
76
+ }
77
+ }
78
+ }
79
+ } catch (chunkError) {
80
+ // Log chunk error via telemetry, but don't let telemetry errors break the stream
81
+ try {
82
+ telemetryHooks.logError(model, chunkError instanceof Error ? chunkError : new Error(String(chunkError)), "stream-chunk");
83
+ } catch {
84
+ // Silently ignore telemetry errors
85
+ }
86
+ }
42
87
  }
43
- }
44
88
 
45
- return fullText;
89
+ return fullText;
90
+ } catch (error) {
91
+ // Re-throw as GeminiError if it's an API error
92
+ if (error instanceof Error && error.name === "GeminiError") {
93
+ throw error;
94
+ }
95
+
96
+ // Check for abort error
97
+ if (error instanceof Error && error.name === "AbortError") {
98
+ throw new Error("Stream generation was aborted");
99
+ }
100
+
101
+ // Wrap other errors
102
+ throw createGeminiError(error);
103
+ }
46
104
  }
47
105
  }
48
106
 
@@ -1,5 +1,8 @@
1
1
 
2
2
  import { geminiTextGenerationService } from "./gemini-text-generation.service";
3
+ import { parseJsonResponse } from "../utils/json-parser.util";
4
+ import { extractTextFromParts } from "../utils/content-mapper.util";
5
+ import { validateSchema } from "../utils/validation.util";
3
6
  import type { GenerationConfig } from "@google/generative-ai";
4
7
  import type {
5
8
  GeminiContent,
@@ -19,15 +22,11 @@ class GeminiStructuredTextService {
19
22
  config?: Omit<GeminiGenerationConfig, "responseMimeType" | "responseSchema">,
20
23
  signal?: AbortSignal,
21
24
  ): Promise<T> {
22
- // Validate schema structure before passing to SDK
23
- if (!schema || typeof schema !== "object" || Object.keys(schema).length === 0) {
24
- throw new Error("Schema must be a non-empty object");
25
- }
25
+ validateSchema(schema);
26
26
 
27
27
  const generationConfig: GeminiGenerationConfig = {
28
28
  ...config,
29
29
  responseMimeType: "application/json",
30
- // Pass schema directly - Google SDK will validate it
31
30
  responseSchema: schema as GenerationConfig["responseSchema"],
32
31
  };
33
32
 
@@ -55,26 +54,13 @@ class GeminiStructuredTextService {
55
54
  throw new Error("No candidates in response");
56
55
  }
57
56
 
58
- let text = "";
59
-
60
- if (candidates[0]?.content?.parts) {
61
- text = candidates[0].content.parts
62
- .map((part) => "text" in part ? (part.text || "") : "")
63
- .join("");
64
- }
57
+ const text = extractTextFromParts(candidates[0].content.parts);
65
58
 
66
59
  if (!text || text.trim().length === 0) {
67
60
  throw new Error("Empty response received from Gemini");
68
61
  }
69
62
 
70
- // Clean and parse JSON (remove markdown code blocks if present)
71
- const cleanedText = text.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
72
-
73
- try {
74
- return JSON.parse(cleanedText) as T;
75
- } catch (error) {
76
- throw new Error(`Failed to parse structured response: ${error instanceof Error ? error.message : String(error)}. Cleaned text: ${cleanedText.substring(0, 200)}...`);
77
- }
63
+ return parseJsonResponse<T>(text);
78
64
  }
79
65
  }
80
66