@umituz/react-native-ai-gemini-provider 2.1.5 → 2.1.6
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 +11 -104
- package/src/infrastructure/interceptors/BaseInterceptor.ts +78 -0
- package/src/infrastructure/interceptors/RequestInterceptors.ts +6 -62
- package/src/infrastructure/interceptors/ResponseInterceptors.ts +6 -61
- package/src/infrastructure/interceptors/index.ts +7 -13
- package/src/infrastructure/services/base-gemini.service.ts +82 -0
- package/src/infrastructure/services/gemini-streaming.service.ts +31 -61
- package/src/infrastructure/services/gemini-text-generation.service.ts +11 -33
- package/src/infrastructure/services/index.ts +7 -4
- package/src/infrastructure/utils/async/index.ts +1 -10
- package/src/infrastructure/utils/index.ts +43 -26
- package/src/infrastructure/utils/stream-processor.util.ts +156 -0
- package/src/infrastructure/utils/validation-composer.util.ts +160 -0
- package/src/infrastructure/utils/validation.util.ts +9 -68
- package/src/presentation/hooks/index.ts +6 -1
- package/src/presentation/hooks/use-gemini.ts +21 -72
- package/src/presentation/hooks/use-operation-manager.ts +88 -0
- package/src/providers/ConfigBuilder.ts +121 -0
- package/src/providers/ProviderFactory.ts +37 -49
- package/src/providers/index.ts +4 -13
- package/src/infrastructure/utils/async/debounce.util.ts +0 -100
- package/src/infrastructure/utils/async/memoize.util.ts +0 -55
- package/src/infrastructure/utils/env.util.ts +0 -175
- package/src/infrastructure/utils/performance.util.ts +0 -139
- package/src/infrastructure/utils/rate-limiter.util.ts +0 -86
- package/src/infrastructure/utils/retry.util.ts +0 -158
- package/src/providers/ProviderConfig.ts +0 -36
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @umituz/react-native-ai-gemini-provider
|
|
3
3
|
* Google Gemini AI provider for React Native applications
|
|
4
|
-
* Text generation only - for image/video use FAL Provider
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
// Domain Types
|
|
@@ -21,120 +20,28 @@ export type {
|
|
|
21
20
|
GeminiApiError,
|
|
22
21
|
} from "./domain/entities";
|
|
23
22
|
|
|
24
|
-
export {
|
|
23
|
+
export {
|
|
24
|
+
GeminiErrorType,
|
|
25
|
+
GeminiError,
|
|
26
|
+
GEMINI_MODELS,
|
|
27
|
+
DEFAULT_MODELS
|
|
28
|
+
} from "./domain/entities";
|
|
25
29
|
|
|
26
|
-
//
|
|
30
|
+
// Main Service
|
|
27
31
|
export {
|
|
28
|
-
geminiClientCoreService,
|
|
29
|
-
geminiTextGenerationService,
|
|
30
|
-
geminiStructuredTextService,
|
|
31
|
-
geminiStreamingService,
|
|
32
32
|
geminiProviderService,
|
|
33
33
|
GeminiProvider,
|
|
34
34
|
} from "./infrastructure/services";
|
|
35
35
|
|
|
36
36
|
export type { GeminiProviderConfig } from "./infrastructure/services";
|
|
37
37
|
|
|
38
|
-
//
|
|
39
|
-
export {
|
|
40
|
-
// Error handling
|
|
41
|
-
mapGeminiError,
|
|
42
|
-
isGeminiErrorRetryable,
|
|
43
|
-
categorizeGeminiError,
|
|
44
|
-
createGeminiError,
|
|
45
|
-
// Data transformation
|
|
46
|
-
extractTextFromResponse,
|
|
47
|
-
cleanJsonText,
|
|
48
|
-
parseJsonResponse,
|
|
49
|
-
safeParseJson,
|
|
50
|
-
extractJsonFromText,
|
|
51
|
-
toSdkContent,
|
|
52
|
-
createTextContent,
|
|
53
|
-
transformCandidate,
|
|
54
|
-
transformResponse,
|
|
55
|
-
extractTextFromParts,
|
|
56
|
-
// Performance
|
|
57
|
-
measureAsync,
|
|
58
|
-
measureSync,
|
|
59
|
-
debounce,
|
|
60
|
-
throttle,
|
|
61
|
-
PerformanceTimer,
|
|
62
|
-
// Rate limiting
|
|
63
|
-
RateLimiter,
|
|
64
|
-
// Retry logic
|
|
65
|
-
retryWithBackoff,
|
|
66
|
-
retryIf,
|
|
67
|
-
retryWithFixedDelay,
|
|
68
|
-
shouldRetryNetworkError,
|
|
69
|
-
createRetryPredicate,
|
|
70
|
-
// Validation
|
|
71
|
-
validateModelName,
|
|
72
|
-
validateApiKey,
|
|
73
|
-
validateSchema,
|
|
74
|
-
validatePrompt,
|
|
75
|
-
validateTimeout,
|
|
76
|
-
isValidObject,
|
|
77
|
-
validateRequiredFields,
|
|
78
|
-
// Environment
|
|
79
|
-
getRequiredEnv,
|
|
80
|
-
getOptionalEnv,
|
|
81
|
-
getEnvNumber,
|
|
82
|
-
getEnvBoolean,
|
|
83
|
-
loadGeminiEnv,
|
|
84
|
-
getApiKeyFromEnv,
|
|
85
|
-
isDevelopment,
|
|
86
|
-
isDebugEnabled,
|
|
87
|
-
validateEnv,
|
|
88
|
-
getGeminiConfigFromEnv,
|
|
89
|
-
// Async state management
|
|
90
|
-
executeWithState,
|
|
91
|
-
createDebouncedAsync,
|
|
92
|
-
createMemoizedAsync,
|
|
93
|
-
} from "./infrastructure/utils";
|
|
94
|
-
|
|
95
|
-
export type {
|
|
96
|
-
PerformanceMetrics,
|
|
97
|
-
RateLimiterOptions,
|
|
98
|
-
RetryOptions,
|
|
99
|
-
RetryResult,
|
|
100
|
-
EnvConfig,
|
|
101
|
-
AsyncStateCallbacks,
|
|
102
|
-
AsyncStateSetters,
|
|
103
|
-
AsyncStateConfig,
|
|
104
|
-
} from "./infrastructure/utils";
|
|
105
|
-
|
|
106
|
-
// Hooks
|
|
38
|
+
// React Hook
|
|
107
39
|
export { useGemini } from "./presentation/hooks";
|
|
108
|
-
|
|
109
40
|
export type { UseGeminiOptions, UseGeminiReturn } from "./presentation/hooks";
|
|
110
41
|
|
|
111
|
-
//
|
|
112
|
-
export {
|
|
113
|
-
export type { TelemetryEvent, TelemetryListener } from "./infrastructure/telemetry";
|
|
114
|
-
|
|
115
|
-
// Interceptors
|
|
116
|
-
export { requestInterceptors, responseInterceptors } from "./infrastructure/interceptors";
|
|
117
|
-
|
|
118
|
-
export type {
|
|
119
|
-
RequestContext,
|
|
120
|
-
RequestInterceptor,
|
|
121
|
-
InterceptorErrorStrategy,
|
|
122
|
-
} from "./infrastructure/interceptors";
|
|
123
|
-
|
|
124
|
-
export type {
|
|
125
|
-
ResponseContext,
|
|
126
|
-
ResponseInterceptor,
|
|
127
|
-
} from "./infrastructure/interceptors/ResponseInterceptors";
|
|
128
|
-
|
|
129
|
-
// Provider Config
|
|
130
|
-
export {
|
|
131
|
-
providerFactory,
|
|
132
|
-
resolveProviderConfig,
|
|
133
|
-
} from "./providers";
|
|
134
|
-
|
|
42
|
+
// Provider Configuration & Factory
|
|
43
|
+
export { ConfigBuilder, providerFactory } from "./providers";
|
|
135
44
|
export type {
|
|
136
|
-
|
|
137
|
-
ProviderConfigInput,
|
|
138
|
-
ResolvedProviderConfig,
|
|
45
|
+
ProviderConfig,
|
|
139
46
|
ProviderFactoryOptions,
|
|
140
47
|
} from "./providers";
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Interceptor Class
|
|
3
|
+
* Eliminates code duplication between Request and Response interceptors
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { telemetryHooks } from "../telemetry";
|
|
7
|
+
|
|
8
|
+
export type InterceptorErrorStrategy = "fail" | "skip" | "log";
|
|
9
|
+
|
|
10
|
+
export interface BaseContext {
|
|
11
|
+
model: string;
|
|
12
|
+
feature?: string;
|
|
13
|
+
timestamp: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export abstract class BaseInterceptor<TContext extends BaseContext> {
|
|
17
|
+
protected interceptors: Array<(context: TContext) => TContext | Promise<TContext>> = [];
|
|
18
|
+
protected errorStrategy: InterceptorErrorStrategy = "fail";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Register an interceptor
|
|
22
|
+
*/
|
|
23
|
+
use(interceptor: (context: TContext) => TContext | Promise<TContext>): () => void {
|
|
24
|
+
this.interceptors.push(interceptor);
|
|
25
|
+
|
|
26
|
+
// Return unsubscribe function
|
|
27
|
+
return () => {
|
|
28
|
+
const index = this.interceptors.indexOf(interceptor);
|
|
29
|
+
if (index > -1) {
|
|
30
|
+
this.interceptors.splice(index, 1);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Set error handling strategy
|
|
37
|
+
*/
|
|
38
|
+
setErrorStrategy(strategy: InterceptorErrorStrategy): void {
|
|
39
|
+
this.errorStrategy = strategy;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Clear all interceptors
|
|
44
|
+
*/
|
|
45
|
+
clear(): void {
|
|
46
|
+
this.interceptors = [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get interceptor count
|
|
51
|
+
*/
|
|
52
|
+
count(): number {
|
|
53
|
+
return this.interceptors.length;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Handle interceptor error based on strategy
|
|
58
|
+
*/
|
|
59
|
+
protected handleError(context: TContext, error: unknown): void {
|
|
60
|
+
telemetryHooks.logError(
|
|
61
|
+
context.model,
|
|
62
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
63
|
+
context.feature
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
if (this.errorStrategy === "fail") {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Interceptor failed: ${error instanceof Error ? error.message : String(error)}`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
// For "skip" and "log", we just continue (error already logged)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Apply interceptors - to be implemented by subclasses
|
|
76
|
+
*/
|
|
77
|
+
abstract apply(context: TContext): Promise<TContext>;
|
|
78
|
+
}
|
|
@@ -1,46 +1,15 @@
|
|
|
1
|
+
import { BaseInterceptor, type BaseContext } from "./BaseInterceptor";
|
|
1
2
|
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export interface RequestContext {
|
|
5
|
-
model: string;
|
|
6
|
-
feature?: string;
|
|
3
|
+
export interface RequestContext extends BaseContext {
|
|
7
4
|
payload: Record<string, unknown>;
|
|
8
|
-
timestamp: number;
|
|
9
5
|
}
|
|
10
6
|
|
|
11
7
|
export type RequestInterceptor = (context: RequestContext) => RequestContext | Promise<RequestContext>;
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class RequestInterceptors {
|
|
16
|
-
private interceptors: RequestInterceptor[] = [];
|
|
17
|
-
private errorStrategy: InterceptorErrorStrategy = "fail";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Register a request interceptor
|
|
21
|
-
* Interceptors are called in order (first registered = first called)
|
|
22
|
-
*/
|
|
23
|
-
use(interceptor: RequestInterceptor): () => void {
|
|
24
|
-
this.interceptors.push(interceptor);
|
|
25
|
-
|
|
26
|
-
// Return unsubscribe function
|
|
27
|
-
return () => {
|
|
28
|
-
const index = this.interceptors.indexOf(interceptor);
|
|
29
|
-
if (index > -1) {
|
|
30
|
-
this.interceptors.splice(index, 1);
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Set error handling strategy for interceptors
|
|
37
|
-
*/
|
|
38
|
-
setErrorStrategy(strategy: InterceptorErrorStrategy): void {
|
|
39
|
-
this.errorStrategy = strategy;
|
|
40
|
-
}
|
|
41
|
-
|
|
9
|
+
class RequestInterceptors extends BaseInterceptor<RequestContext> {
|
|
42
10
|
/**
|
|
43
11
|
* Apply all interceptors to a request context
|
|
12
|
+
* Interceptors are called in order (first registered = first called)
|
|
44
13
|
*/
|
|
45
14
|
async apply(context: RequestContext): Promise<RequestContext> {
|
|
46
15
|
let result = context;
|
|
@@ -49,38 +18,13 @@ class RequestInterceptors {
|
|
|
49
18
|
try {
|
|
50
19
|
result = await interceptor(result);
|
|
51
20
|
} catch (error) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
switch (this.errorStrategy) {
|
|
56
|
-
case "fail":
|
|
57
|
-
throw new Error(`Request interceptor failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
58
|
-
case "skip":
|
|
59
|
-
// Skip this interceptor and continue with previous result
|
|
60
|
-
break;
|
|
61
|
-
case "log":
|
|
62
|
-
// Error already logged, continue with previous result
|
|
63
|
-
break;
|
|
64
|
-
}
|
|
21
|
+
this.handleError(context, error);
|
|
22
|
+
// If we get here, strategy was "skip" or "log" - continue with previous result
|
|
65
23
|
}
|
|
66
24
|
}
|
|
67
25
|
|
|
68
26
|
return result;
|
|
69
27
|
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Clear all interceptors
|
|
73
|
-
*/
|
|
74
|
-
clear(): void {
|
|
75
|
-
this.interceptors = [];
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Get interceptor count
|
|
80
|
-
*/
|
|
81
|
-
count(): number {
|
|
82
|
-
return this.interceptors.length;
|
|
83
|
-
}
|
|
84
28
|
}
|
|
85
29
|
|
|
86
30
|
export const requestInterceptors = new RequestInterceptors();
|
|
@@ -1,48 +1,18 @@
|
|
|
1
|
+
import { BaseInterceptor, type BaseContext } from "./BaseInterceptor";
|
|
1
2
|
|
|
2
|
-
|
|
3
|
-
import { telemetryHooks } from "../telemetry";
|
|
4
|
-
|
|
5
|
-
export interface ResponseContext<T = unknown> {
|
|
6
|
-
model: string;
|
|
7
|
-
feature?: string;
|
|
3
|
+
export interface ResponseContext<T = unknown> extends BaseContext {
|
|
8
4
|
data: T;
|
|
9
5
|
duration: number;
|
|
10
|
-
timestamp: number;
|
|
11
6
|
}
|
|
12
7
|
|
|
13
8
|
export type ResponseInterceptor<T = unknown> = (
|
|
14
9
|
context: ResponseContext<T>,
|
|
15
10
|
) => ResponseContext<T> | Promise<ResponseContext<T>>;
|
|
16
11
|
|
|
17
|
-
class ResponseInterceptors {
|
|
18
|
-
private interceptors: Array<ResponseInterceptor<unknown>> = [];
|
|
19
|
-
private errorStrategy: InterceptorErrorStrategy = "fail";
|
|
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
|
-
* Set error handling strategy for interceptors
|
|
39
|
-
*/
|
|
40
|
-
setErrorStrategy(strategy: InterceptorErrorStrategy): void {
|
|
41
|
-
this.errorStrategy = strategy;
|
|
42
|
-
}
|
|
43
|
-
|
|
12
|
+
class ResponseInterceptors extends BaseInterceptor<ResponseContext<unknown>> {
|
|
44
13
|
/**
|
|
45
14
|
* Apply all interceptors to a response context
|
|
15
|
+
* Interceptors are called in reverse order (last registered = first called)
|
|
46
16
|
*/
|
|
47
17
|
async apply<T>(context: ResponseContext<T>): Promise<ResponseContext<T>> {
|
|
48
18
|
let result: ResponseContext<unknown> = context;
|
|
@@ -53,38 +23,13 @@ class ResponseInterceptors {
|
|
|
53
23
|
try {
|
|
54
24
|
result = await interceptor(result);
|
|
55
25
|
} catch (error) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
switch (this.errorStrategy) {
|
|
60
|
-
case "fail":
|
|
61
|
-
throw new Error(`Response interceptor failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
62
|
-
case "skip":
|
|
63
|
-
// Skip this interceptor and continue with previous result
|
|
64
|
-
break;
|
|
65
|
-
case "log":
|
|
66
|
-
// Error already logged, continue with previous result
|
|
67
|
-
break;
|
|
68
|
-
}
|
|
26
|
+
this.handleError(context, error);
|
|
27
|
+
// If we get here, strategy was "skip" or "log" - continue with previous result
|
|
69
28
|
}
|
|
70
29
|
}
|
|
71
30
|
|
|
72
31
|
return result as ResponseContext<T>;
|
|
73
32
|
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Clear all interceptors
|
|
77
|
-
*/
|
|
78
|
-
clear(): void {
|
|
79
|
-
this.interceptors = [];
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Get interceptor count
|
|
84
|
-
*/
|
|
85
|
-
count(): number {
|
|
86
|
-
return this.interceptors.length;
|
|
87
|
-
}
|
|
88
33
|
}
|
|
89
34
|
|
|
90
35
|
export const responseInterceptors = new ResponseInterceptors();
|
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Interceptors Module
|
|
3
|
-
* Allows applications to modify requests and responses
|
|
2
|
+
* Interceptors Module - Internal Use Only
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
5
|
+
export { BaseInterceptor } from "./BaseInterceptor";
|
|
6
|
+
export type { BaseContext, InterceptorErrorStrategy } from "./BaseInterceptor";
|
|
8
7
|
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
RequestInterceptor,
|
|
12
|
-
InterceptorErrorStrategy,
|
|
13
|
-
} from "./RequestInterceptors";
|
|
8
|
+
export { requestInterceptors } from "./RequestInterceptors";
|
|
9
|
+
export type { RequestContext, RequestInterceptor } from "./RequestInterceptors";
|
|
14
10
|
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
ResponseInterceptor,
|
|
18
|
-
} from "./ResponseInterceptors";
|
|
11
|
+
export { responseInterceptors } from "./ResponseInterceptors";
|
|
12
|
+
export type { ResponseContext, ResponseInterceptor } from "./ResponseInterceptors";
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Gemini Service
|
|
3
|
+
* Common functionality for all Gemini services to eliminate code duplication
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { geminiClientCoreService } from "./gemini-client-core.service";
|
|
7
|
+
import { toSdkContent } from "../utils/content-mapper.util";
|
|
8
|
+
import { createGeminiError } from "../utils/error-mapper.util";
|
|
9
|
+
import type { GeminiContent, GeminiGenerationConfig } from "../../domain/entities";
|
|
10
|
+
import type { GenerativeModel } from "@google/generative-ai";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Base request options structure
|
|
14
|
+
*/
|
|
15
|
+
export interface BaseRequestOptions {
|
|
16
|
+
model: string;
|
|
17
|
+
contents: GeminiContent[];
|
|
18
|
+
generationConfig?: GeminiGenerationConfig;
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Abstract base service with common patterns
|
|
24
|
+
*/
|
|
25
|
+
export abstract class BaseGeminiService {
|
|
26
|
+
/**
|
|
27
|
+
* Validate and prepare request
|
|
28
|
+
* Eliminates duplicate validation and abort checks
|
|
29
|
+
*/
|
|
30
|
+
protected validateAndPrepare(options: BaseRequestOptions): {
|
|
31
|
+
genModel: GenerativeModel;
|
|
32
|
+
sdkContents: Array<{ role: string; parts: Array<{ text: string }> }>;
|
|
33
|
+
} {
|
|
34
|
+
// Validate contents
|
|
35
|
+
if (!options.contents || options.contents.length === 0) {
|
|
36
|
+
throw new Error("Contents array cannot be empty");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Check for early abort
|
|
40
|
+
if (options.signal?.aborted) {
|
|
41
|
+
throw new Error("Request was aborted");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const genModel = geminiClientCoreService.getModel(options.model);
|
|
45
|
+
const sdkContents = toSdkContent(options.contents);
|
|
46
|
+
|
|
47
|
+
return { genModel, sdkContents };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Handle errors uniformly across all services
|
|
52
|
+
* Eliminates duplicate error handling logic
|
|
53
|
+
*/
|
|
54
|
+
protected handleError(error: unknown, abortMessage: string): never {
|
|
55
|
+
// Re-throw as GeminiError if it's already an API error
|
|
56
|
+
if (error instanceof Error && error.name === "GeminiError") {
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check for abort error
|
|
61
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
62
|
+
throw new Error(abortMessage);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Wrap other errors
|
|
66
|
+
throw createGeminiError(error);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create typed request options for SDK
|
|
71
|
+
* Type-safe request creation
|
|
72
|
+
*/
|
|
73
|
+
protected createRequestOptions(
|
|
74
|
+
sdkContents: Array<{ role: string; parts: Array<{ text: string }> }>,
|
|
75
|
+
generationConfig?: GeminiGenerationConfig
|
|
76
|
+
) {
|
|
77
|
+
return {
|
|
78
|
+
contents: sdkContents,
|
|
79
|
+
generationConfig,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
import { geminiClientCoreService } from "./gemini-client-core.service";
|
|
3
|
-
import { toSdkContent } from "../utils/content-mapper.util";
|
|
4
|
-
import { createGeminiError } from "../utils/error-mapper.util";
|
|
1
|
+
import { BaseGeminiService } from "./base-gemini.service";
|
|
5
2
|
import { telemetryHooks } from "../telemetry";
|
|
3
|
+
import { processStream } from "../utils/stream-processor.util";
|
|
6
4
|
import type {
|
|
7
5
|
GeminiContent,
|
|
8
6
|
GeminiGenerationConfig,
|
|
9
7
|
} from "../../domain/entities";
|
|
10
8
|
|
|
11
|
-
class GeminiStreamingService {
|
|
9
|
+
class GeminiStreamingService extends BaseGeminiService {
|
|
12
10
|
/**
|
|
13
11
|
* Stream content generation
|
|
14
12
|
*
|
|
@@ -31,75 +29,47 @@ class GeminiStreamingService {
|
|
|
31
29
|
generationConfig?: GeminiGenerationConfig,
|
|
32
30
|
signal?: AbortSignal,
|
|
33
31
|
): Promise<string> {
|
|
34
|
-
// Validate
|
|
35
|
-
if (!contents || contents.length === 0) {
|
|
36
|
-
throw new Error("Contents array cannot be empty");
|
|
37
|
-
}
|
|
38
|
-
|
|
32
|
+
// Validate callback
|
|
39
33
|
if (typeof onChunk !== "function") {
|
|
40
34
|
throw new Error("onChunk must be a function");
|
|
41
35
|
}
|
|
42
36
|
|
|
43
|
-
// Check for early abort
|
|
44
|
-
if (signal?.aborted) {
|
|
45
|
-
throw new Error("Stream generation was aborted");
|
|
46
|
-
}
|
|
47
|
-
|
|
48
37
|
try {
|
|
49
|
-
const genModel =
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const requestOptions = {
|
|
53
|
-
contents: sdkContents as Parameters<typeof genModel.generateContentStream>[0] extends { contents: infer C } ? C : never,
|
|
38
|
+
const { genModel, sdkContents } = this.validateAndPrepare({
|
|
39
|
+
model,
|
|
40
|
+
contents,
|
|
54
41
|
generationConfig,
|
|
55
|
-
|
|
42
|
+
signal,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const requestOptions = this.createRequestOptions(sdkContents, generationConfig);
|
|
56
46
|
|
|
57
47
|
const result = signal
|
|
58
48
|
? await genModel.generateContentStream(requestOptions, { signal })
|
|
59
49
|
: await genModel.generateContentStream(requestOptions);
|
|
60
50
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (chunkText) {
|
|
67
|
-
fullText += chunkText;
|
|
68
|
-
// Safely call onChunk - errors in callback won't break the stream
|
|
69
|
-
try {
|
|
70
|
-
onChunk(chunkText);
|
|
71
|
-
} catch (callbackError) {
|
|
72
|
-
try {
|
|
73
|
-
telemetryHooks.logError(model, callbackError instanceof Error ? callbackError : new Error(String(callbackError)), "stream-callback");
|
|
74
|
-
} catch {
|
|
75
|
-
// Silently ignore telemetry errors to prevent breaking the stream
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
} catch (chunkError) {
|
|
80
|
-
// Log chunk error via telemetry, but don't let telemetry errors break the stream
|
|
81
|
-
try {
|
|
82
|
-
telemetryHooks.logError(model, chunkError instanceof Error ? chunkError : new Error(String(chunkError)), "stream-chunk");
|
|
83
|
-
} catch {
|
|
84
|
-
// Silently ignore telemetry errors
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return fullText;
|
|
51
|
+
return await processStream(
|
|
52
|
+
result.stream,
|
|
53
|
+
onChunk,
|
|
54
|
+
(error, context) => this.logStreamError(model, error, context)
|
|
55
|
+
);
|
|
90
56
|
} catch (error) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Check for abort error
|
|
97
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
98
|
-
throw new Error("Stream generation was aborted");
|
|
99
|
-
}
|
|
57
|
+
return this.handleError(error, "Stream generation was aborted");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
100
60
|
|
|
101
|
-
|
|
102
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Log stream errors via telemetry
|
|
63
|
+
*/
|
|
64
|
+
private logStreamError(model: string, error: unknown, context?: string): void {
|
|
65
|
+
try {
|
|
66
|
+
telemetryHooks.logError(
|
|
67
|
+
model,
|
|
68
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
69
|
+
context
|
|
70
|
+
);
|
|
71
|
+
} catch {
|
|
72
|
+
// Silently ignore telemetry errors
|
|
103
73
|
}
|
|
104
74
|
}
|
|
105
75
|
}
|