@umituz/react-native-ai-gemini-provider 1.14.25 → 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/index.ts +22 -0
- package/src/infrastructure/services/gemini-retry.service.ts +46 -7
- package/src/infrastructure/services/index.ts +3 -0
- package/src/infrastructure/telemetry/TelemetryHooks.ts +125 -0
- package/src/infrastructure/telemetry/index.ts +6 -0
- package/src/infrastructure/utils/index.ts +12 -0
- package/src/infrastructure/utils/model-validation.util.ts +107 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -106,6 +106,7 @@ export type {
|
|
|
106
106
|
GenerationInput,
|
|
107
107
|
GenerationResult,
|
|
108
108
|
ExecutionOptions,
|
|
109
|
+
RetryOptions,
|
|
109
110
|
} from "./infrastructure/services";
|
|
110
111
|
|
|
111
112
|
// =============================================================================
|
|
@@ -117,6 +118,16 @@ export {
|
|
|
117
118
|
isGeminiErrorRetryable,
|
|
118
119
|
categorizeGeminiError,
|
|
119
120
|
createGeminiError,
|
|
121
|
+
// Model validation
|
|
122
|
+
isValidModel,
|
|
123
|
+
validateModel,
|
|
124
|
+
getSafeModel,
|
|
125
|
+
isTextModel,
|
|
126
|
+
isImageModel,
|
|
127
|
+
isImageEditModel,
|
|
128
|
+
isVideoGenerationModel,
|
|
129
|
+
getModelCategory,
|
|
130
|
+
getAllValidModels,
|
|
120
131
|
// Input builders
|
|
121
132
|
buildSingleImageInput,
|
|
122
133
|
buildDualImageInput,
|
|
@@ -155,6 +166,17 @@ export type {
|
|
|
155
166
|
UseGeminiReturn,
|
|
156
167
|
} from "./presentation/hooks";
|
|
157
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
|
+
|
|
158
180
|
// =============================================================================
|
|
159
181
|
// PROVIDER CONFIGURATION - Tier-based Setup
|
|
160
182
|
// =============================================================================
|
|
@@ -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();
|
|
@@ -34,6 +34,9 @@ export type {
|
|
|
34
34
|
ExecutionOptions,
|
|
35
35
|
} from "./generation-executor";
|
|
36
36
|
|
|
37
|
+
// Retry service types
|
|
38
|
+
export type { RetryOptions } from "./gemini-retry.service";
|
|
39
|
+
|
|
37
40
|
// Re-export types from generation-content for convenience
|
|
38
41
|
export type {
|
|
39
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();
|
|
@@ -21,6 +21,18 @@ export {
|
|
|
21
21
|
} from "./image-preparer.util";
|
|
22
22
|
export type { PreparedImage } from "./image-preparer.util";
|
|
23
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
|
+
|
|
24
36
|
// Input builders
|
|
25
37
|
export {
|
|
26
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
|
+
}
|