@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 +1 -1
- package/src/domain/entities/error.types.ts +45 -0
- package/src/index.ts +29 -0
- package/src/infrastructure/services/feature-model-selector.ts +48 -2
- package/src/infrastructure/services/gemini-retry.service.ts +46 -7
- package/src/infrastructure/services/generation-executor.ts +32 -16
- package/src/infrastructure/services/index.ts +11 -0
- package/src/infrastructure/telemetry/TelemetryHooks.ts +125 -0
- package/src/infrastructure/telemetry/index.ts +6 -0
- package/src/infrastructure/utils/error-mapper.util.ts +9 -0
- package/src/infrastructure/utils/index.ts +13 -0
- package/src/infrastructure/utils/model-validation.util.ts +107 -0
package/package.json
CHANGED
|
@@ -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 =
|
|
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:
|
|
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
|
|
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
|
|
52
|
-
|
|
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
|
|
77
|
-
input:
|
|
93
|
+
private async executeVideoGeneration(
|
|
94
|
+
input: GenerationInput,
|
|
78
95
|
options?: ExecutionOptions,
|
|
79
|
-
): Promise<
|
|
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
|
|
88
|
-
negativePrompt: input.negativePrompt
|
|
103
|
+
prompt: String(input.prompt ?? ""),
|
|
104
|
+
image: input.image,
|
|
105
|
+
negativePrompt: input.negativePrompt,
|
|
89
106
|
options: {
|
|
90
|
-
aspectRatio: this.normalizeAspectRatio(input.aspect_ratio
|
|
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
|
-
}
|
|
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();
|
|
@@ -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
|
+
}
|