@umituz/react-native-ai-gemini-provider 1.14.24 → 1.14.26

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.14.24",
3
+ "version": "1.14.26",
4
4
  "description": "Google Gemini AI provider for React Native applications",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -37,3 +37,48 @@ export interface GeminiApiError {
37
37
  }>;
38
38
  };
39
39
  }
40
+
41
+ /**
42
+ * Custom error class for Gemini API errors
43
+ */
44
+ export class GeminiError extends Error {
45
+ readonly type: GeminiErrorType;
46
+ readonly retryable: boolean;
47
+ readonly statusCode?: number;
48
+ readonly originalError?: unknown;
49
+
50
+ constructor(info: GeminiErrorInfo) {
51
+ super(info.messageKey);
52
+ this.name = "GeminiError";
53
+ this.type = info.type;
54
+ this.retryable = info.retryable;
55
+ this.statusCode = info.statusCode;
56
+ this.originalError = info.originalError;
57
+
58
+ // Maintains proper stack trace (only available on V8)
59
+ if (Error.captureStackTrace) {
60
+ Error.captureStackTrace(this, GeminiError);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Check if error is retryable
66
+ */
67
+ isRetryable(): boolean {
68
+ return this.retryable;
69
+ }
70
+
71
+ /**
72
+ * Get error type
73
+ */
74
+ getErrorType(): GeminiErrorType {
75
+ return this.type;
76
+ }
77
+
78
+ /**
79
+ * Create GeminiError from unknown error
80
+ */
81
+ static fromError(_error: unknown, info: GeminiErrorInfo): GeminiError {
82
+ return new GeminiError(info);
83
+ }
84
+ }
package/src/index.ts CHANGED
@@ -51,6 +51,8 @@ export type {
51
51
  GeminiApiError,
52
52
  } from "./domain/entities";
53
53
 
54
+ export { GeminiError } from "./domain/entities";
55
+
54
56
  // Model Constants
55
57
  export {
56
58
  GEMINI_MODELS,
@@ -90,6 +92,7 @@ export {
90
92
  geminiVideoGenerationService,
91
93
  geminiProviderService,
92
94
  createGeminiProvider,
95
+ featureModelSelector,
93
96
  } from "./infrastructure/services";
94
97
 
95
98
  export type {
@@ -100,6 +103,10 @@ export type {
100
103
  JobStatus,
101
104
  SubscribeOptions,
102
105
  AIJobStatusType,
106
+ GenerationInput,
107
+ GenerationResult,
108
+ ExecutionOptions,
109
+ RetryOptions,
103
110
  } from "./infrastructure/services";
104
111
 
105
112
  // =============================================================================
@@ -110,6 +117,17 @@ export {
110
117
  mapGeminiError,
111
118
  isGeminiErrorRetryable,
112
119
  categorizeGeminiError,
120
+ createGeminiError,
121
+ // Model validation
122
+ isValidModel,
123
+ validateModel,
124
+ getSafeModel,
125
+ isTextModel,
126
+ isImageModel,
127
+ isImageEditModel,
128
+ isVideoGenerationModel,
129
+ getModelCategory,
130
+ getAllValidModels,
113
131
  // Input builders
114
132
  buildSingleImageInput,
115
133
  buildDualImageInput,
@@ -148,6 +166,17 @@ export type {
148
166
  UseGeminiReturn,
149
167
  } from "./presentation/hooks";
150
168
 
169
+ // =============================================================================
170
+ // TELEMETRY - Monitoring and Observability
171
+ // =============================================================================
172
+
173
+ export { telemetryHooks } from "./infrastructure/telemetry";
174
+
175
+ export type {
176
+ TelemetryEvent,
177
+ TelemetryListener,
178
+ } from "./infrastructure/telemetry";
179
+
151
180
  // =============================================================================
152
181
  // PROVIDER CONFIGURATION - Tier-based Setup
153
182
  // =============================================================================
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Feature Model Selector
3
3
  * Returns the appropriate model ID for a given feature
4
+ * Supports runtime model overrides for flexibility
4
5
  */
5
6
 
6
7
  import type {
@@ -12,19 +13,64 @@ import {
12
13
  GEMINI_VIDEO_FEATURE_MODELS,
13
14
  } from "../../domain/constants/feature-models.constants";
14
15
 
16
+ declare const __DEV__: boolean;
17
+
18
+ type ModelOverrideMap = Partial<Record<ImageFeatureType | VideoFeatureType, string>>;
19
+
15
20
  class FeatureModelSelector {
21
+ private overrides: ModelOverrideMap = {};
22
+
23
+ /**
24
+ * Set model override for a specific feature
25
+ * This allows runtime configuration without modifying constants
26
+ */
27
+ setModelOverride(feature: ImageFeatureType | VideoFeatureType, model: string): void {
28
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
29
+ // eslint-disable-next-line no-console
30
+ console.log("[FeatureModelSelector] Model override set:", { feature, model });
31
+ }
32
+ this.overrides[feature] = model;
33
+ }
34
+
35
+ /**
36
+ * Clear all model overrides
37
+ */
38
+ clearOverrides(): void {
39
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
40
+ // eslint-disable-next-line no-console
41
+ console.log("[FeatureModelSelector] All model overrides cleared");
42
+ }
43
+ this.overrides = {};
44
+ }
45
+
16
46
  /**
17
47
  * Get model ID for an IMAGE feature
48
+ * Returns override if set, otherwise returns default model
18
49
  */
19
50
  getImageFeatureModel(feature: ImageFeatureType): string {
20
- return GEMINI_IMAGE_FEATURE_MODELS[feature];
51
+ return this.overrides[feature] ?? GEMINI_IMAGE_FEATURE_MODELS[feature];
21
52
  }
22
53
 
23
54
  /**
24
55
  * Get model ID for a VIDEO feature
56
+ * Returns override if set, otherwise returns default model
25
57
  */
26
58
  getVideoFeatureModel(feature: VideoFeatureType): string {
27
- return GEMINI_VIDEO_FEATURE_MODELS[feature];
59
+ return this.overrides[feature] ?? GEMINI_VIDEO_FEATURE_MODELS[feature];
60
+ }
61
+
62
+ /**
63
+ * Check if a feature has a custom override
64
+ */
65
+ hasOverride(feature: ImageFeatureType | VideoFeatureType): boolean {
66
+ return feature in this.overrides;
67
+ }
68
+
69
+ /**
70
+ * Get all current overrides
71
+ */
72
+ getOverrides(): ModelOverrideMap {
73
+ return { ...this.overrides };
28
74
  }
29
75
  }
30
76
 
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Gemini Retry Service
3
- * Handles retry logic with exponential backoff
3
+ * Handles retry logic with exponential backoff and jitter
4
+ * Jitter helps prevent thundering herd problem in distributed systems
4
5
  */
5
6
 
6
7
  import { geminiClientCoreService } from "./gemini-client-core.service";
@@ -26,19 +27,46 @@ function isRetryableError(error: unknown): boolean {
26
27
  return RETRYABLE_ERROR_PATTERNS.some((pattern) => message.includes(pattern));
27
28
  }
28
29
 
30
+ /**
31
+ * Add random jitter to delay to prevent synchronized retries
32
+ * Uses full jitter strategy: random between 0 and base_delay * 2^attempt
33
+ */
34
+ function calculateDelayWithJitter(
35
+ baseDelay: number,
36
+ retryCount: number,
37
+ maxDelay: number,
38
+ ): number {
39
+ const exponentialDelay = baseDelay * Math.pow(2, retryCount);
40
+ const cappedDelay = Math.min(exponentialDelay, maxDelay);
41
+ const jitter = Math.random() * cappedDelay;
42
+ return Math.floor(jitter);
43
+ }
44
+
29
45
  function sleep(ms: number): Promise<void> {
30
46
  return new Promise((resolve) => setTimeout(resolve, ms));
31
47
  }
32
48
 
49
+ export interface RetryOptions {
50
+ maxRetries?: number;
51
+ baseDelay?: number;
52
+ maxDelay?: number;
53
+ enableJitter?: boolean;
54
+ }
55
+
33
56
  class GeminiRetryService {
57
+ /**
58
+ * Execute operation with retry logic
59
+ */
34
60
  async executeWithRetry<T>(
35
61
  operation: () => Promise<T>,
36
62
  retryCount = 0,
63
+ options?: RetryOptions,
37
64
  ): Promise<T> {
38
65
  const config = geminiClientCoreService.getConfig();
39
- const maxRetries = config?.maxRetries ?? 3;
40
- const baseDelay = config?.baseDelay ?? 1000;
41
- const maxDelay = config?.maxDelay ?? 10000;
66
+ const maxRetries = options?.maxRetries ?? config?.maxRetries ?? 3;
67
+ const baseDelay = options?.baseDelay ?? config?.baseDelay ?? 1000;
68
+ const maxDelay = options?.maxDelay ?? config?.maxDelay ?? 10000;
69
+ const enableJitter = options?.enableJitter ?? true;
42
70
 
43
71
  try {
44
72
  return await operation();
@@ -47,17 +75,28 @@ class GeminiRetryService {
47
75
  throw error;
48
76
  }
49
77
 
50
- const delay = Math.min(baseDelay * Math.pow(2, retryCount), maxDelay);
78
+ const delay = enableJitter
79
+ ? calculateDelayWithJitter(baseDelay, retryCount, maxDelay)
80
+ : Math.min(baseDelay * Math.pow(2, retryCount), maxDelay);
51
81
 
52
82
  if (typeof __DEV__ !== "undefined" && __DEV__) {
53
83
  // eslint-disable-next-line no-console
54
- console.log(`[Gemini] Retry ${retryCount + 1}/${maxRetries} after ${delay}ms`);
84
+ console.log(`[Gemini] Retry ${retryCount + 1}/${maxRetries} after ${delay}ms`, {
85
+ jitter: enableJitter,
86
+ });
55
87
  }
56
88
 
57
89
  await sleep(delay);
58
- return this.executeWithRetry(operation, retryCount + 1);
90
+ return this.executeWithRetry(operation, retryCount + 1, options);
59
91
  }
60
92
  }
93
+
94
+ /**
95
+ * Check if an error is retryable
96
+ */
97
+ isRetryableError(error: unknown): boolean {
98
+ return isRetryableError(error);
99
+ }
61
100
  }
62
101
 
63
102
  export const geminiRetryService = new GeminiRetryService();
@@ -5,7 +5,9 @@
5
5
 
6
6
  import type {
7
7
  GeminiImageInput,
8
+ GeminiImageGenerationResult,
8
9
  VideoGenerationInput,
10
+ VideoGenerationResult,
9
11
  VideoGenerationProgress,
10
12
  } from "../../domain/entities";
11
13
  import { geminiTextGenerationService } from "./gemini-text-generation.service";
@@ -20,13 +22,29 @@ export interface ExecutionOptions {
20
22
  onProgress?: (progress: number) => void;
21
23
  }
22
24
 
25
+ export type GenerationInput = {
26
+ type?: "text" | "image" | "video";
27
+ generateImage?: boolean;
28
+ prompt?: string;
29
+ images?: GeminiImageInput[];
30
+ generationConfig?: unknown;
31
+ image?: string;
32
+ negativePrompt?: string;
33
+ aspect_ratio?: string;
34
+ };
35
+
36
+ export type GenerationResult =
37
+ | string
38
+ | GeminiImageGenerationResult
39
+ | VideoGenerationResult;
40
+
23
41
  export class GenerationExecutor {
24
42
  private contentBuilder = new ContentBuilder();
25
43
  private responseFormatter = new ResponseFormatter();
26
44
 
27
- async executeGeneration<T>(
45
+ async executeGeneration<T = GenerationResult>(
28
46
  model: string,
29
- input: Record<string, unknown>,
47
+ input: GenerationInput,
30
48
  options?: ExecutionOptions,
31
49
  ): Promise<T> {
32
50
  if (typeof __DEV__ !== "undefined" && __DEV__) {
@@ -43,14 +61,13 @@ export class GenerationExecutor {
43
61
  }
44
62
 
45
63
  if (isVideoGeneration) {
46
- return this.executeVideoGeneration<T>(input, options);
64
+ return this.executeVideoGeneration(input, options) as T;
47
65
  }
48
66
 
49
67
  if (isImageGeneration) {
50
- const prompt = String(input.prompt || "");
51
- const images = input.images as GeminiImageInput[] | undefined;
52
- const result = await geminiImageGenerationService.generateImage(prompt, images);
53
- return result as T;
68
+ const prompt = String(input.prompt ?? "");
69
+ const images = input.images;
70
+ return geminiImageGenerationService.generateImage(prompt, images) as T;
54
71
  }
55
72
 
56
73
  const contents = this.contentBuilder.buildContents(input);
@@ -73,21 +90,21 @@ export class GenerationExecutor {
73
90
  /**
74
91
  * Execute video generation using Veo API
75
92
  */
76
- private async executeVideoGeneration<T>(
77
- input: Record<string, unknown>,
93
+ private async executeVideoGeneration(
94
+ input: GenerationInput,
78
95
  options?: ExecutionOptions,
79
- ): Promise<T> {
96
+ ): Promise<VideoGenerationResult> {
80
97
  if (typeof __DEV__ !== "undefined" && __DEV__) {
81
98
  // eslint-disable-next-line no-console
82
99
  console.log("[GenerationExecutor] executeVideoGeneration() called");
83
100
  }
84
101
 
85
102
  const videoInput: VideoGenerationInput = {
86
- prompt: String(input.prompt || ""),
87
- image: input.image as string | undefined,
88
- negativePrompt: input.negativePrompt as string | undefined,
103
+ prompt: String(input.prompt ?? ""),
104
+ image: input.image,
105
+ negativePrompt: input.negativePrompt,
89
106
  options: {
90
- aspectRatio: this.normalizeAspectRatio(input.aspect_ratio as string),
107
+ aspectRatio: this.normalizeAspectRatio(input.aspect_ratio),
91
108
  },
92
109
  };
93
110
 
@@ -109,10 +126,9 @@ export class GenerationExecutor {
109
126
  }
110
127
 
111
128
  return {
112
- video: { url: result.videoUrl },
113
129
  videoUrl: result.videoUrl,
114
130
  metadata: result.metadata,
115
- } as T;
131
+ };
116
132
  }
117
133
 
118
134
  /**
@@ -15,6 +15,7 @@ export { geminiVideoGenerationService } from "./gemini-video-generation.service"
15
15
  export { providerInitializer } from "./provider-initializer";
16
16
  export { jobProcessor } from "./job-processor";
17
17
  export { generationExecutor } from "./generation-executor";
18
+ export { featureModelSelector } from "./feature-model-selector";
18
19
 
19
20
  // Public provider API
20
21
  export {
@@ -26,6 +27,16 @@ export {
26
27
  export type { GeminiProviderConfig } from "./gemini-provider";
27
28
  export type { GeminiProviderConfig as AIProviderConfig } from "./provider-initializer";
28
29
 
30
+ // Generation executor types
31
+ export type {
32
+ GenerationInput,
33
+ GenerationResult,
34
+ ExecutionOptions,
35
+ } from "./generation-executor";
36
+
37
+ // Retry service types
38
+ export type { RetryOptions } from "./gemini-retry.service";
39
+
29
40
  // Re-export types from generation-content for convenience
30
41
  export type {
31
42
  IAIProvider,
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Telemetry Hooks
3
+ * Allows applications to monitor and log AI operations
4
+ */
5
+
6
+ declare const __DEV__: boolean;
7
+
8
+ export interface TelemetryEvent {
9
+ type: "request" | "response" | "error" | "retry";
10
+ timestamp: number;
11
+ model?: string;
12
+ feature?: string;
13
+ duration?: number;
14
+ metadata?: Record<string, unknown>;
15
+ }
16
+
17
+ export type TelemetryListener = (event: TelemetryEvent) => void;
18
+
19
+ class TelemetryHooks {
20
+ private listeners: TelemetryListener[] = [];
21
+
22
+ /**
23
+ * Register a telemetry listener
24
+ */
25
+ subscribe(listener: TelemetryListener): () => void {
26
+ this.listeners.push(listener);
27
+
28
+ return () => {
29
+ const index = this.listeners.indexOf(listener);
30
+ if (index > -1) {
31
+ this.listeners.splice(index, 1);
32
+ }
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Emit a telemetry event to all listeners
38
+ */
39
+ emit(event: TelemetryEvent): void {
40
+ for (const listener of this.listeners) {
41
+ try {
42
+ listener(event);
43
+ } catch (error) {
44
+ // Prevent telemetry errors from breaking the app
45
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
46
+ // eslint-disable-next-line no-console
47
+ console.error("[Telemetry] Listener error:", error);
48
+ }
49
+ }
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Log request start
55
+ */
56
+ logRequest(model: string, feature?: string): number {
57
+ const timestamp = Date.now();
58
+ this.emit({
59
+ type: "request",
60
+ timestamp,
61
+ model,
62
+ feature,
63
+ });
64
+ return timestamp;
65
+ }
66
+
67
+ /**
68
+ * Log response received
69
+ */
70
+ logResponse(model: string, startTime: number, feature?: string, metadata?: Record<string, unknown>): void {
71
+ this.emit({
72
+ type: "response",
73
+ timestamp: Date.now(),
74
+ model,
75
+ feature,
76
+ duration: Date.now() - startTime,
77
+ metadata,
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Log error
83
+ */
84
+ logError(model: string, error: Error, feature?: string): void {
85
+ this.emit({
86
+ type: "error",
87
+ timestamp: Date.now(),
88
+ model,
89
+ feature,
90
+ metadata: {
91
+ error: error.message,
92
+ errorType: error.name,
93
+ },
94
+ });
95
+ }
96
+
97
+ /**
98
+ * Log retry attempt
99
+ */
100
+ logRetry(model: string, attempt: number, feature?: string): void {
101
+ this.emit({
102
+ type: "retry",
103
+ timestamp: Date.now(),
104
+ model,
105
+ feature,
106
+ metadata: { attempt },
107
+ });
108
+ }
109
+
110
+ /**
111
+ * Clear all listeners
112
+ */
113
+ clear(): void {
114
+ this.listeners = [];
115
+ }
116
+
117
+ /**
118
+ * Get current listener count
119
+ */
120
+ getListenerCount(): number {
121
+ return this.listeners.length;
122
+ }
123
+ }
124
+
125
+ export const telemetryHooks = new TelemetryHooks();
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Telemetry Module
3
+ */
4
+
5
+ export { telemetryHooks } from "./TelemetryHooks";
6
+ export type { TelemetryEvent, TelemetryListener } from "./TelemetryHooks";
@@ -7,6 +7,7 @@ import {
7
7
  GeminiErrorType,
8
8
  type GeminiErrorInfo,
9
9
  type GeminiApiError,
10
+ GeminiError,
10
11
  } from "../../domain/entities";
11
12
 
12
13
  const ERROR_PATTERNS: Array<{
@@ -112,3 +113,11 @@ export function isGeminiErrorRetryable(error: unknown): boolean {
112
113
  export function categorizeGeminiError(error: unknown): GeminiErrorType {
113
114
  return mapGeminiError(error).type;
114
115
  }
116
+
117
+ /**
118
+ * Create a GeminiError instance from an unknown error
119
+ */
120
+ export function createGeminiError(error: unknown): GeminiError {
121
+ const errorInfo = mapGeminiError(error);
122
+ return GeminiError.fromError(error, errorInfo);
123
+ }
@@ -6,6 +6,7 @@ export {
6
6
  mapGeminiError,
7
7
  isGeminiErrorRetryable,
8
8
  categorizeGeminiError,
9
+ createGeminiError,
9
10
  } from "./error-mapper.util";
10
11
 
11
12
  export {
@@ -20,6 +21,18 @@ export {
20
21
  } from "./image-preparer.util";
21
22
  export type { PreparedImage } from "./image-preparer.util";
22
23
 
24
+ export {
25
+ isValidModel,
26
+ validateModel,
27
+ getSafeModel,
28
+ isTextModel,
29
+ isImageModel,
30
+ isImageEditModel,
31
+ isVideoGenerationModel,
32
+ getModelCategory,
33
+ getAllValidModels,
34
+ } from "./model-validation.util";
35
+
23
36
  // Input builders
24
37
  export {
25
38
  buildSingleImageInput,
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Model Validation Utilities
3
+ * Validates model IDs and configurations
4
+ */
5
+
6
+ import { GEMINI_MODELS, DEFAULT_MODELS } from "../../domain/entities";
7
+
8
+ declare const __DEV__: boolean;
9
+
10
+ /**
11
+ * Known valid model IDs
12
+ */
13
+ const VALID_MODELS = new Set<string>(
14
+ Object.values(GEMINI_MODELS).flatMap((category) => Object.values(category)),
15
+ );
16
+
17
+ /**
18
+ * Check if a model ID is valid
19
+ */
20
+ export function isValidModel(model: string): boolean {
21
+ return VALID_MODELS.has(model);
22
+ }
23
+
24
+ /**
25
+ * Validate model ID and throw if invalid
26
+ */
27
+ export function validateModel(model: string): void {
28
+ if (!model) {
29
+ throw new Error("Model ID cannot be empty");
30
+ }
31
+
32
+ if (!isValidModel(model)) {
33
+ throw new Error(
34
+ `Invalid model ID: ${model}. Valid models: ${Array.from(VALID_MODELS).join(", ")}`,
35
+ );
36
+ }
37
+
38
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
39
+ // eslint-disable-next-line no-console
40
+ console.log("[ModelValidation] Model validated:", model);
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Get a safe model ID (fallback to default if invalid)
46
+ */
47
+ export function getSafeModel(model: string | undefined, defaultType: keyof typeof DEFAULT_MODELS): string {
48
+ if (!model) {
49
+ return DEFAULT_MODELS[defaultType];
50
+ }
51
+
52
+ if (!isValidModel(model)) {
53
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
54
+ // eslint-disable-next-line no-console
55
+ console.warn(`[ModelValidation] Invalid model "${model}", falling back to ${DEFAULT_MODELS[defaultType]}`);
56
+ }
57
+ return DEFAULT_MODELS[defaultType];
58
+ }
59
+
60
+ return model;
61
+ }
62
+
63
+ /**
64
+ * Check if model is a text generation model
65
+ */
66
+ export function isTextModel(model: string): boolean {
67
+ return Object.values(GEMINI_MODELS.TEXT).includes(model as (typeof GEMINI_MODELS.TEXT)[keyof typeof GEMINI_MODELS.TEXT]);
68
+ }
69
+
70
+ /**
71
+ * Check if model is an image generation model
72
+ */
73
+ export function isImageModel(model: string): boolean {
74
+ return Object.values(GEMINI_MODELS.TEXT_TO_IMAGE).includes(model as (typeof GEMINI_MODELS.TEXT_TO_IMAGE)[keyof typeof GEMINI_MODELS.TEXT_TO_IMAGE]);
75
+ }
76
+
77
+ /**
78
+ * Check if model is an image editing model
79
+ */
80
+ export function isImageEditModel(model: string): boolean {
81
+ return Object.values(GEMINI_MODELS.IMAGE_EDIT).includes(model as (typeof GEMINI_MODELS.IMAGE_EDIT)[keyof typeof GEMINI_MODELS.IMAGE_EDIT]);
82
+ }
83
+
84
+ /**
85
+ * Check if model is a video generation model
86
+ */
87
+ export function isVideoGenerationModel(model: string): boolean {
88
+ return Object.values(GEMINI_MODELS.VIDEO_GENERATION).includes(model as (typeof GEMINI_MODELS.VIDEO_GENERATION)[keyof typeof GEMINI_MODELS.VIDEO_GENERATION]);
89
+ }
90
+
91
+ /**
92
+ * Get model category
93
+ */
94
+ export function getModelCategory(model: string): string | null {
95
+ if (isTextModel(model)) return "text";
96
+ if (isImageModel(model)) return "text-to-image";
97
+ if (isImageEditModel(model)) return "image-edit";
98
+ if (isVideoGenerationModel(model)) return "video-generation";
99
+ return null;
100
+ }
101
+
102
+ /**
103
+ * Get all valid model IDs
104
+ */
105
+ export function getAllValidModels(): readonly string[] {
106
+ return Array.from(VALID_MODELS);
107
+ }