@umituz/react-native-ai-gemini-provider 1.14.25 → 1.14.27
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 +52 -61
- package/src/infrastructure/cache/SimpleCache.ts +173 -0
- package/src/infrastructure/cache/index.ts +7 -0
- package/src/infrastructure/interceptors/RequestInterceptors.ts +67 -0
- package/src/infrastructure/interceptors/ResponseInterceptors.ts +72 -0
- package/src/infrastructure/interceptors/index.ts +17 -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 +23 -0
- package/src/infrastructure/utils/model-validation.util.ts +107 -0
- package/src/infrastructure/utils/performance.util.ts +145 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @umituz/react-native-ai-gemini-provider
|
|
3
3
|
* Google Gemini AI provider for React Native applications
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* import {
|
|
7
|
-
* geminiClientService,
|
|
8
|
-
* geminiProviderService,
|
|
9
|
-
* useGemini,
|
|
10
|
-
* mapGeminiError,
|
|
11
|
-
* } from '@umituz/react-native-ai-gemini-provider';
|
|
12
4
|
*/
|
|
13
5
|
|
|
14
|
-
//
|
|
15
|
-
// DOMAIN LAYER - Types
|
|
16
|
-
// =============================================================================
|
|
17
|
-
|
|
6
|
+
// Domain Types
|
|
18
7
|
export type {
|
|
19
8
|
GeminiConfig,
|
|
20
9
|
GeminiGenerationConfig,
|
|
@@ -42,30 +31,14 @@ export type {
|
|
|
42
31
|
VideoResolution,
|
|
43
32
|
VideoOperationStatus,
|
|
44
33
|
VeoOperation,
|
|
45
|
-
} from "./domain/entities";
|
|
46
|
-
|
|
47
|
-
export { GeminiErrorType } from "./domain/entities";
|
|
48
|
-
|
|
49
|
-
export type {
|
|
50
34
|
GeminiErrorInfo,
|
|
51
35
|
GeminiApiError,
|
|
36
|
+
ResponseModality,
|
|
52
37
|
} from "./domain/entities";
|
|
53
38
|
|
|
54
|
-
export { GeminiError } from "./domain/entities";
|
|
55
|
-
|
|
56
|
-
// Model Constants
|
|
57
|
-
export {
|
|
58
|
-
GEMINI_MODELS,
|
|
59
|
-
DEFAULT_MODELS,
|
|
60
|
-
RESPONSE_MODALITIES,
|
|
61
|
-
} from "./domain/entities";
|
|
62
|
-
|
|
63
|
-
export type { ResponseModality } from "./domain/entities";
|
|
64
|
-
|
|
65
|
-
// =============================================================================
|
|
66
|
-
// DOMAIN LAYER - Feature Models
|
|
67
|
-
// =============================================================================
|
|
39
|
+
export { GeminiErrorType, GeminiError, GEMINI_MODELS, DEFAULT_MODELS, RESPONSE_MODALITIES } from "./domain/entities";
|
|
68
40
|
|
|
41
|
+
// Feature Models
|
|
69
42
|
export {
|
|
70
43
|
GEMINI_IMAGE_FEATURE_MODELS,
|
|
71
44
|
GEMINI_VIDEO_FEATURE_MODELS,
|
|
@@ -74,14 +47,9 @@ export {
|
|
|
74
47
|
getAllFeatureModels,
|
|
75
48
|
} from "./domain/constants";
|
|
76
49
|
|
|
77
|
-
export type {
|
|
78
|
-
FeatureModelConfig,
|
|
79
|
-
} from "./domain/constants";
|
|
80
|
-
|
|
81
|
-
// =============================================================================
|
|
82
|
-
// INFRASTRUCTURE LAYER - Services
|
|
83
|
-
// =============================================================================
|
|
50
|
+
export type { FeatureModelConfig } from "./domain/constants";
|
|
84
51
|
|
|
52
|
+
// Services
|
|
85
53
|
export {
|
|
86
54
|
geminiClientCoreService,
|
|
87
55
|
geminiRetryService,
|
|
@@ -106,18 +74,31 @@ export type {
|
|
|
106
74
|
GenerationInput,
|
|
107
75
|
GenerationResult,
|
|
108
76
|
ExecutionOptions,
|
|
77
|
+
RetryOptions,
|
|
109
78
|
} from "./infrastructure/services";
|
|
110
79
|
|
|
111
|
-
//
|
|
112
|
-
// INFRASTRUCTURE LAYER - Utils
|
|
113
|
-
// =============================================================================
|
|
114
|
-
|
|
80
|
+
// Utils
|
|
115
81
|
export {
|
|
116
82
|
mapGeminiError,
|
|
117
83
|
isGeminiErrorRetryable,
|
|
118
84
|
categorizeGeminiError,
|
|
119
85
|
createGeminiError,
|
|
120
|
-
|
|
86
|
+
isValidModel,
|
|
87
|
+
validateModel,
|
|
88
|
+
getSafeModel,
|
|
89
|
+
isTextModel,
|
|
90
|
+
isImageModel,
|
|
91
|
+
isImageEditModel,
|
|
92
|
+
isVideoGenerationModel,
|
|
93
|
+
getModelCategory,
|
|
94
|
+
getAllValidModels,
|
|
95
|
+
measureAsync,
|
|
96
|
+
measureSync,
|
|
97
|
+
debounce,
|
|
98
|
+
throttle,
|
|
99
|
+
PerformanceTimer,
|
|
100
|
+
PerformanceTracker,
|
|
101
|
+
performanceTracker,
|
|
121
102
|
buildSingleImageInput,
|
|
122
103
|
buildDualImageInput,
|
|
123
104
|
buildUpscaleInput,
|
|
@@ -134,31 +115,41 @@ export {
|
|
|
134
115
|
} from "./infrastructure/utils";
|
|
135
116
|
|
|
136
117
|
export type {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
118
|
+
PreparedImage,
|
|
119
|
+
UpscaleOptions,
|
|
120
|
+
PhotoRestoreOptions,
|
|
121
|
+
FaceSwapOptions,
|
|
122
|
+
AnimeSelfieOptions,
|
|
123
|
+
RemoveBackgroundOptions,
|
|
124
|
+
RemoveObjectOptions,
|
|
125
|
+
ReplaceBackgroundOptions,
|
|
126
|
+
VideoFromImageOptions,
|
|
127
|
+
PerformanceMetrics,
|
|
145
128
|
} from "./infrastructure/utils";
|
|
146
129
|
|
|
147
|
-
//
|
|
148
|
-
// PRESENTATION LAYER - Hooks
|
|
149
|
-
// =============================================================================
|
|
150
|
-
|
|
130
|
+
// Hooks
|
|
151
131
|
export { useGemini } from "./presentation/hooks";
|
|
152
132
|
|
|
133
|
+
export type { UseGeminiOptions, UseGeminiReturn } from "./presentation/hooks";
|
|
134
|
+
|
|
135
|
+
// Telemetry
|
|
136
|
+
export { telemetryHooks } from "./infrastructure/telemetry";
|
|
137
|
+
export type { TelemetryEvent, TelemetryListener } from "./infrastructure/telemetry";
|
|
138
|
+
|
|
139
|
+
// Interceptors
|
|
140
|
+
export { requestInterceptors, responseInterceptors } from "./infrastructure/interceptors";
|
|
153
141
|
export type {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
142
|
+
RequestContext,
|
|
143
|
+
RequestInterceptor,
|
|
144
|
+
ResponseContext,
|
|
145
|
+
ResponseInterceptor,
|
|
146
|
+
} from "./infrastructure/interceptors";
|
|
157
147
|
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
148
|
+
// Cache
|
|
149
|
+
export { SimpleCache, modelSelectionCache } from "./infrastructure/cache";
|
|
150
|
+
export type { CacheOptions } from "./infrastructure/cache";
|
|
161
151
|
|
|
152
|
+
// Provider Config
|
|
162
153
|
export {
|
|
163
154
|
providerFactory,
|
|
164
155
|
resolveProviderConfig,
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple LRU Cache
|
|
3
|
+
* Least Recently Used cache for performance optimization
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
declare const __DEV__: boolean;
|
|
7
|
+
|
|
8
|
+
interface CacheEntry<V> {
|
|
9
|
+
value: V;
|
|
10
|
+
expiresAt: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CacheOptions {
|
|
14
|
+
maxSize?: number;
|
|
15
|
+
ttl?: number; // Time to live in milliseconds
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class SimpleCache<K, V> {
|
|
19
|
+
private cache = new Map<K, CacheEntry<V>>();
|
|
20
|
+
private maxSize: number;
|
|
21
|
+
private ttl: number;
|
|
22
|
+
|
|
23
|
+
constructor(options: CacheOptions = {}) {
|
|
24
|
+
this.maxSize = options.maxSize ?? 100;
|
|
25
|
+
this.ttl = options.ttl ?? 5 * 60 * 1000; // 5 minutes default
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Set a value in the cache
|
|
30
|
+
*/
|
|
31
|
+
set(key: K, value: V, customTtl?: number): void {
|
|
32
|
+
// Remove oldest entry if at capacity
|
|
33
|
+
if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
|
|
34
|
+
const firstKey = this.cache.keys().next().value;
|
|
35
|
+
if (firstKey !== undefined) {
|
|
36
|
+
this.cache.delete(firstKey);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ttl = customTtl ?? this.ttl;
|
|
41
|
+
this.cache.set(key, {
|
|
42
|
+
value,
|
|
43
|
+
expiresAt: Date.now() + ttl,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get a value from the cache
|
|
49
|
+
* Returns undefined if not found or expired
|
|
50
|
+
*/
|
|
51
|
+
get(key: K): V | undefined {
|
|
52
|
+
const entry = this.cache.get(key);
|
|
53
|
+
|
|
54
|
+
if (!entry) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check if expired
|
|
59
|
+
if (Date.now() > entry.expiresAt) {
|
|
60
|
+
this.cache.delete(key);
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Move to end (most recently used)
|
|
65
|
+
this.cache.delete(key);
|
|
66
|
+
this.cache.set(key, entry);
|
|
67
|
+
|
|
68
|
+
return entry.value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if a key exists and is not expired
|
|
73
|
+
*/
|
|
74
|
+
has(key: K): boolean {
|
|
75
|
+
return this.get(key) !== undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Delete a specific key
|
|
80
|
+
*/
|
|
81
|
+
delete(key: K): boolean {
|
|
82
|
+
return this.cache.delete(key);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Clear all entries
|
|
87
|
+
*/
|
|
88
|
+
clear(): void {
|
|
89
|
+
this.cache.clear();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get current cache size
|
|
94
|
+
*/
|
|
95
|
+
size(): number {
|
|
96
|
+
// Clean expired entries first
|
|
97
|
+
this.cleanExpired();
|
|
98
|
+
return this.cache.size;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Remove all expired entries
|
|
103
|
+
*/
|
|
104
|
+
private cleanExpired(): void {
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
107
|
+
if (now > entry.expiresAt) {
|
|
108
|
+
this.cache.delete(key);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get all valid keys (not expired)
|
|
115
|
+
*/
|
|
116
|
+
keys(): K[] {
|
|
117
|
+
this.cleanExpired();
|
|
118
|
+
return Array.from(this.cache.keys());
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get cache statistics
|
|
123
|
+
*/
|
|
124
|
+
getStats(): {
|
|
125
|
+
size: number;
|
|
126
|
+
maxSize: number;
|
|
127
|
+
keys: K[];
|
|
128
|
+
} {
|
|
129
|
+
return {
|
|
130
|
+
size: this.size(),
|
|
131
|
+
maxSize: this.maxSize,
|
|
132
|
+
keys: this.keys(),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Global model selection cache
|
|
139
|
+
* Caches model IDs for features to avoid repeated lookups
|
|
140
|
+
*/
|
|
141
|
+
class ModelSelectionCache {
|
|
142
|
+
private cache = new SimpleCache<string, string>({
|
|
143
|
+
maxSize: 50,
|
|
144
|
+
ttl: 10 * 60 * 1000, // 10 minutes
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
set(feature: string, model: string): void {
|
|
148
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
149
|
+
// eslint-disable-next-line no-console
|
|
150
|
+
console.log("[ModelSelectionCache] Caching model:", { feature, model });
|
|
151
|
+
}
|
|
152
|
+
this.cache.set(feature, model);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
get(feature: string): string | undefined {
|
|
156
|
+
const model = this.cache.get(feature);
|
|
157
|
+
if (model && typeof __DEV__ !== "undefined" && __DEV__) {
|
|
158
|
+
// eslint-disable-next-line no-console
|
|
159
|
+
console.log("[ModelSelectionCache] Cache hit:", { feature, model });
|
|
160
|
+
}
|
|
161
|
+
return model;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
clear(): void {
|
|
165
|
+
this.cache.clear();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
getStats(): ReturnType<SimpleCache<string, string>["getStats"]> {
|
|
169
|
+
return this.cache.getStats();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export const modelSelectionCache = new ModelSelectionCache();
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request Interceptors
|
|
3
|
+
* Allows applications to modify requests before they're sent
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface RequestContext {
|
|
7
|
+
model: string;
|
|
8
|
+
feature?: string;
|
|
9
|
+
payload: Record<string, unknown>;
|
|
10
|
+
timestamp: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type RequestInterceptor = (context: RequestContext) => RequestContext | Promise<RequestContext>;
|
|
14
|
+
|
|
15
|
+
class RequestInterceptors {
|
|
16
|
+
private interceptors: RequestInterceptor[] = [];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Register a request interceptor
|
|
20
|
+
* Interceptors are called in order (first registered = first called)
|
|
21
|
+
*/
|
|
22
|
+
use(interceptor: RequestInterceptor): () => void {
|
|
23
|
+
this.interceptors.push(interceptor);
|
|
24
|
+
|
|
25
|
+
// Return unsubscribe function
|
|
26
|
+
return () => {
|
|
27
|
+
const index = this.interceptors.indexOf(interceptor);
|
|
28
|
+
if (index > -1) {
|
|
29
|
+
this.interceptors.splice(index, 1);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Apply all interceptors to a request context
|
|
36
|
+
*/
|
|
37
|
+
async apply(context: RequestContext): Promise<RequestContext> {
|
|
38
|
+
let result = context;
|
|
39
|
+
|
|
40
|
+
for (const interceptor of this.interceptors) {
|
|
41
|
+
try {
|
|
42
|
+
result = await interceptor(result);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// Interceptor error should fail the request
|
|
45
|
+
throw new Error(`Request interceptor failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Clear all interceptors
|
|
54
|
+
*/
|
|
55
|
+
clear(): void {
|
|
56
|
+
this.interceptors = [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get interceptor count
|
|
61
|
+
*/
|
|
62
|
+
count(): number {
|
|
63
|
+
return this.interceptors.length;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const requestInterceptors = new RequestInterceptors();
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response Interceptors
|
|
3
|
+
* Allows applications to modify responses after they're received
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ResponseContext<T = unknown> {
|
|
7
|
+
model: string;
|
|
8
|
+
feature?: string;
|
|
9
|
+
data: T;
|
|
10
|
+
duration: number;
|
|
11
|
+
timestamp: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ResponseInterceptor<T = unknown> = (
|
|
15
|
+
context: ResponseContext<T>,
|
|
16
|
+
) => ResponseContext<T> | Promise<ResponseContext<T>>;
|
|
17
|
+
|
|
18
|
+
class ResponseInterceptors {
|
|
19
|
+
private interceptors: Array<ResponseInterceptor<unknown>> = [];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Register a response interceptor
|
|
23
|
+
* Interceptors are called in reverse order (last registered = first called)
|
|
24
|
+
*/
|
|
25
|
+
use<T = unknown>(interceptor: ResponseInterceptor<T>): () => void {
|
|
26
|
+
this.interceptors.push(interceptor as ResponseInterceptor<unknown>);
|
|
27
|
+
|
|
28
|
+
// Return unsubscribe function
|
|
29
|
+
return () => {
|
|
30
|
+
const index = this.interceptors.indexOf(interceptor as ResponseInterceptor<unknown>);
|
|
31
|
+
if (index > -1) {
|
|
32
|
+
this.interceptors.splice(index, 1);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Apply all interceptors to a response context
|
|
39
|
+
*/
|
|
40
|
+
async apply<T>(context: ResponseContext<T>): Promise<ResponseContext<T>> {
|
|
41
|
+
let result: ResponseContext<unknown> = context;
|
|
42
|
+
|
|
43
|
+
// Apply in reverse order (last added = first processed)
|
|
44
|
+
for (let i = this.interceptors.length - 1; i >= 0; i--) {
|
|
45
|
+
const interceptor = this.interceptors[i];
|
|
46
|
+
try {
|
|
47
|
+
result = await interceptor(result);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
// Interceptor error should fail the response processing
|
|
50
|
+
throw new Error(`Response interceptor failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return result as ResponseContext<T>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Clear all interceptors
|
|
59
|
+
*/
|
|
60
|
+
clear(): void {
|
|
61
|
+
this.interceptors = [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get interceptor count
|
|
66
|
+
*/
|
|
67
|
+
count(): number {
|
|
68
|
+
return this.interceptors.length;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const responseInterceptors = new ResponseInterceptors();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interceptors Module
|
|
3
|
+
* Allows applications to modify requests and responses
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { requestInterceptors } from "./RequestInterceptors";
|
|
7
|
+
export { responseInterceptors } from "./ResponseInterceptors";
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
RequestContext,
|
|
11
|
+
RequestInterceptor,
|
|
12
|
+
} from "./RequestInterceptors";
|
|
13
|
+
|
|
14
|
+
export type {
|
|
15
|
+
ResponseContext,
|
|
16
|
+
ResponseInterceptor,
|
|
17
|
+
} from "./ResponseInterceptors";
|
|
@@ -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,29 @@ 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
|
+
|
|
36
|
+
export {
|
|
37
|
+
measureAsync,
|
|
38
|
+
measureSync,
|
|
39
|
+
debounce,
|
|
40
|
+
throttle,
|
|
41
|
+
PerformanceTimer,
|
|
42
|
+
PerformanceTracker,
|
|
43
|
+
performanceTracker,
|
|
44
|
+
} from "./performance.util";
|
|
45
|
+
export type { PerformanceMetrics } from "./performance.util";
|
|
46
|
+
|
|
24
47
|
// Input builders
|
|
25
48
|
export {
|
|
26
49
|
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
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance Utilities
|
|
3
|
+
* Tools for measuring and optimizing performance
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
declare const __DEV__: boolean;
|
|
7
|
+
|
|
8
|
+
export interface PerformanceMetrics {
|
|
9
|
+
duration: number;
|
|
10
|
+
timestamp: number;
|
|
11
|
+
metadata?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class PerformanceTimer {
|
|
15
|
+
private startTime: number;
|
|
16
|
+
private endTime?: number;
|
|
17
|
+
private metadata?: Record<string, unknown>;
|
|
18
|
+
|
|
19
|
+
constructor(metadata?: Record<string, unknown>) {
|
|
20
|
+
this.startTime = Date.now();
|
|
21
|
+
this.metadata = metadata;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
stop(): number {
|
|
25
|
+
this.endTime = Date.now();
|
|
26
|
+
return this.duration;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get duration(): number {
|
|
30
|
+
const end = this.endTime ?? Date.now();
|
|
31
|
+
return end - this.startTime;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getMetrics(): PerformanceMetrics {
|
|
35
|
+
return { duration: this.duration, timestamp: this.startTime, metadata: this.metadata };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get isRunning(): boolean {
|
|
39
|
+
return this.endTime === undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function measureAsync<T>(
|
|
44
|
+
operation: () => Promise<T>,
|
|
45
|
+
metadata?: Record<string, unknown>,
|
|
46
|
+
): Promise<{ result: T; duration: number }> {
|
|
47
|
+
const timer = new PerformanceTimer(metadata);
|
|
48
|
+
try {
|
|
49
|
+
const result = await operation();
|
|
50
|
+
const duration = timer.stop();
|
|
51
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
52
|
+
// eslint-disable-next-line no-console
|
|
53
|
+
console.log("[Performance] Operation completed:", { duration: `${duration}ms`, metadata });
|
|
54
|
+
}
|
|
55
|
+
return { result, duration };
|
|
56
|
+
} catch (error) {
|
|
57
|
+
timer.stop();
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function measureSync<T>(
|
|
63
|
+
operation: () => T,
|
|
64
|
+
metadata?: Record<string, unknown>,
|
|
65
|
+
): { result: T; duration: number } {
|
|
66
|
+
const timer = new PerformanceTimer(metadata);
|
|
67
|
+
try {
|
|
68
|
+
const result = operation();
|
|
69
|
+
const duration = timer.stop();
|
|
70
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
71
|
+
// eslint-disable-next-line no-console
|
|
72
|
+
console.log("[Performance] Operation completed:", { duration: `${duration}ms`, metadata });
|
|
73
|
+
}
|
|
74
|
+
return { result, duration };
|
|
75
|
+
} catch (error) {
|
|
76
|
+
timer.stop();
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function debounce<T extends (...args: never[]) => unknown>(
|
|
82
|
+
func: T,
|
|
83
|
+
wait: number,
|
|
84
|
+
): (...args: Parameters<T>) => void {
|
|
85
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
86
|
+
return (...args: Parameters<T>) => {
|
|
87
|
+
const later = () => {
|
|
88
|
+
clearTimeout(timeout);
|
|
89
|
+
func(...args);
|
|
90
|
+
};
|
|
91
|
+
clearTimeout(timeout);
|
|
92
|
+
timeout = setTimeout(later, wait);
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function throttle<T extends (...args: never[]) => unknown>(
|
|
97
|
+
func: T,
|
|
98
|
+
limit: number,
|
|
99
|
+
): (...args: Parameters<T>) => void {
|
|
100
|
+
let inThrottle: boolean;
|
|
101
|
+
return (...args: Parameters<T>) => {
|
|
102
|
+
if (!inThrottle) {
|
|
103
|
+
func(...args);
|
|
104
|
+
inThrottle = true;
|
|
105
|
+
setTimeout(() => (inThrottle = false), limit);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export class PerformanceTracker {
|
|
111
|
+
private metrics = new Map<string, number[]>();
|
|
112
|
+
|
|
113
|
+
record(operation: string, duration: number): void {
|
|
114
|
+
if (!this.metrics.has(operation)) {
|
|
115
|
+
this.metrics.set(operation, []);
|
|
116
|
+
}
|
|
117
|
+
this.metrics.get(operation)!.push(duration);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
getStats(operation: string): { count: number; avg: number; min: number; max: number } | null {
|
|
121
|
+
const durations = this.metrics.get(operation);
|
|
122
|
+
if (!durations || durations.length === 0) return null;
|
|
123
|
+
return {
|
|
124
|
+
count: durations.length,
|
|
125
|
+
avg: durations.reduce((a, b) => a + b, 0) / durations.length,
|
|
126
|
+
min: Math.min(...durations),
|
|
127
|
+
max: Math.max(...durations),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
getAllStats(): Record<string, ReturnType<PerformanceTracker["getStats"]>> {
|
|
132
|
+
const stats: Record<string, ReturnType<PerformanceTracker["getStats"]>> = {};
|
|
133
|
+
for (const operation of this.metrics.keys()) {
|
|
134
|
+
stats[operation] = this.getStats(operation);
|
|
135
|
+
}
|
|
136
|
+
return stats;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
clear(): void {
|
|
140
|
+
this.metrics.clear();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const performanceTracker = new PerformanceTracker();
|
|
145
|
+
|