@umituz/react-native-ai-gemini-provider 2.0.23 → 2.0.24
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 +2 -3
- package/src/domain/entities/error.types.ts +54 -0
- package/src/domain/entities/gemini.types.ts +51 -1
- package/src/domain/entities/models.ts +13 -0
- package/src/index.ts +48 -0
- package/src/infrastructure/services/gemini-client-core.service.ts +11 -15
- package/src/infrastructure/services/gemini-provider.ts +23 -0
- package/src/infrastructure/services/gemini-streaming.service.ts +78 -20
- package/src/infrastructure/services/gemini-structured-text.service.ts +6 -20
- package/src/infrastructure/services/gemini-text-generation.service.ts +48 -57
- package/src/infrastructure/telemetry/TelemetryHooks.ts +9 -2
- package/src/infrastructure/utils/async/debounce.util.ts +100 -0
- package/src/infrastructure/utils/async/execute-state.util.ts +99 -0
- package/src/infrastructure/utils/async/index.ts +19 -0
- package/src/infrastructure/utils/async/memoize.util.ts +55 -0
- package/src/infrastructure/utils/content-mapper.util.ts +123 -0
- package/src/infrastructure/utils/env.util.ts +175 -0
- package/src/infrastructure/utils/index.ts +25 -1
- package/src/infrastructure/utils/json-parser.util.ts +90 -0
- package/src/infrastructure/utils/retry.util.ts +158 -0
- package/src/infrastructure/utils/validation.util.ts +132 -0
- package/src/presentation/hooks/use-gemini.ts +120 -95
- package/src/infrastructure/utils/async-state.util.ts +0 -44
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-gemini-provider",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.24",
|
|
4
4
|
"description": "Google Gemini AI text generation provider for React Native applications",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -72,8 +72,7 @@
|
|
|
72
72
|
"react-native-gesture-handler": "^2.30.0",
|
|
73
73
|
"react-native-safe-area-context": "^5.6.2",
|
|
74
74
|
"react-native-svg": "^15.15.1",
|
|
75
|
-
"typescript": "^5.3.0"
|
|
76
|
-
"zustand": "^5.0.9"
|
|
75
|
+
"typescript": "^5.3.0"
|
|
77
76
|
},
|
|
78
77
|
"publishConfig": {
|
|
79
78
|
"access": "public"
|
|
@@ -1,29 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Categories of errors that can occur with Gemini API
|
|
3
|
+
*/
|
|
1
4
|
export enum GeminiErrorType {
|
|
5
|
+
/** Network connectivity issues */
|
|
2
6
|
NETWORK = "NETWORK",
|
|
7
|
+
/** API rate limit exceeded */
|
|
3
8
|
RATE_LIMIT = "RATE_LIMIT",
|
|
9
|
+
/** Authentication/authorization failures */
|
|
4
10
|
AUTHENTICATION = "AUTHENTICATION",
|
|
11
|
+
/** Invalid input data or parameters */
|
|
5
12
|
VALIDATION = "VALIDATION",
|
|
13
|
+
/** Content blocked by safety filters */
|
|
6
14
|
SAFETY = "SAFETY",
|
|
15
|
+
/** Server-side errors */
|
|
7
16
|
SERVER = "SERVER",
|
|
17
|
+
/** Request timeout */
|
|
8
18
|
TIMEOUT = "TIMEOUT",
|
|
19
|
+
/** API quota exceeded */
|
|
9
20
|
QUOTA_EXCEEDED = "QUOTA_EXCEEDED",
|
|
21
|
+
/** Requested model not found */
|
|
10
22
|
MODEL_NOT_FOUND = "MODEL_NOT_FOUND",
|
|
23
|
+
/** Unknown/unclassified error */
|
|
11
24
|
UNKNOWN = "UNKNOWN",
|
|
12
25
|
}
|
|
13
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Detailed error information for Gemini API errors
|
|
29
|
+
*/
|
|
14
30
|
export interface GeminiErrorInfo {
|
|
31
|
+
/** Category of the error */
|
|
15
32
|
type: GeminiErrorType;
|
|
33
|
+
/** Message key for i18n translation */
|
|
16
34
|
messageKey: string;
|
|
35
|
+
/** Whether the request can be retried */
|
|
17
36
|
retryable: boolean;
|
|
37
|
+
/** Original error that caused this error */
|
|
18
38
|
originalError?: unknown;
|
|
39
|
+
/** HTTP status code if applicable */
|
|
19
40
|
statusCode?: number;
|
|
20
41
|
}
|
|
21
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Structure of Gemini API error responses
|
|
45
|
+
*/
|
|
22
46
|
export interface GeminiApiError {
|
|
23
47
|
error?: {
|
|
48
|
+
/** Error code */
|
|
24
49
|
code?: number;
|
|
50
|
+
/** Error message */
|
|
25
51
|
message?: string;
|
|
52
|
+
/** Error status */
|
|
26
53
|
status?: string;
|
|
54
|
+
/** Additional error details */
|
|
27
55
|
details?: Array<{
|
|
28
56
|
"@type"?: string;
|
|
29
57
|
reason?: string;
|
|
@@ -33,12 +61,24 @@ export interface GeminiApiError {
|
|
|
33
61
|
};
|
|
34
62
|
}
|
|
35
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Custom error class for Gemini API errors
|
|
66
|
+
* Provides structured error information and retry capability
|
|
67
|
+
*/
|
|
36
68
|
export class GeminiError extends Error {
|
|
69
|
+
/** Error category */
|
|
37
70
|
readonly type: GeminiErrorType;
|
|
71
|
+
/** Whether the operation can be retried */
|
|
38
72
|
readonly retryable: boolean;
|
|
73
|
+
/** HTTP status code if applicable */
|
|
39
74
|
readonly statusCode?: number;
|
|
75
|
+
/** Original error that caused this error */
|
|
40
76
|
readonly originalError?: unknown;
|
|
41
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Create a new GeminiError
|
|
80
|
+
* @param info - Error information
|
|
81
|
+
*/
|
|
42
82
|
constructor(info: GeminiErrorInfo) {
|
|
43
83
|
super(info.messageKey);
|
|
44
84
|
this.name = "GeminiError";
|
|
@@ -53,14 +93,28 @@ export class GeminiError extends Error {
|
|
|
53
93
|
}
|
|
54
94
|
}
|
|
55
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Check if this error is retryable
|
|
98
|
+
* @returns true if the operation can be retried
|
|
99
|
+
*/
|
|
56
100
|
isRetryable(): boolean {
|
|
57
101
|
return this.retryable;
|
|
58
102
|
}
|
|
59
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Get the error type
|
|
106
|
+
* @returns The error category
|
|
107
|
+
*/
|
|
60
108
|
getErrorType(): GeminiErrorType {
|
|
61
109
|
return this.type;
|
|
62
110
|
}
|
|
63
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Create a GeminiError from an unknown error
|
|
114
|
+
* @param error - The original error
|
|
115
|
+
* @param info - Error information
|
|
116
|
+
* @returns A new GeminiError instance
|
|
117
|
+
*/
|
|
64
118
|
static fromError(error: unknown, info: GeminiErrorInfo): GeminiError {
|
|
65
119
|
const geminiError = new GeminiError(info);
|
|
66
120
|
|
|
@@ -1,46 +1,85 @@
|
|
|
1
1
|
import type { GenerationConfig } from "@google/generative-ai";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Configuration for Gemini AI client initialization
|
|
5
|
+
*/
|
|
3
6
|
export interface GeminiConfig {
|
|
7
|
+
/** API key for authentication */
|
|
4
8
|
apiKey: string;
|
|
9
|
+
/** Optional base URL for API requests */
|
|
5
10
|
baseUrl?: string;
|
|
11
|
+
/** Default timeout in milliseconds */
|
|
6
12
|
defaultTimeoutMs?: number;
|
|
13
|
+
/** Default model to use for text generation */
|
|
7
14
|
textModel?: string;
|
|
8
15
|
}
|
|
9
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Generation configuration for AI requests
|
|
19
|
+
* Extends the SDK's GenerationConfig with proper schema typing
|
|
20
|
+
*/
|
|
10
21
|
export type GeminiGenerationConfig = Omit<GenerationConfig, "responseSchema"> & {
|
|
11
22
|
responseSchema?: GenerationConfig["responseSchema"];
|
|
12
23
|
};
|
|
13
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Harm categories for content safety filtering
|
|
27
|
+
*/
|
|
14
28
|
export type GeminiHarmCategory =
|
|
15
29
|
| "HARM_CATEGORY_HARASSMENT"
|
|
16
30
|
| "HARM_CATEGORY_HATE_SPEECH"
|
|
17
31
|
| "HARM_CATEGORY_SEXUALLY_EXPLICIT"
|
|
18
32
|
| "HARM_CATEGORY_DANGEROUS_CONTENT";
|
|
19
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Threshold levels for blocking harmful content
|
|
36
|
+
*/
|
|
20
37
|
export type GeminiHarmBlockThreshold =
|
|
21
38
|
| "BLOCK_NONE"
|
|
22
39
|
| "BLOCK_LOW_AND_ABOVE"
|
|
23
40
|
| "BLOCK_MEDIUM_AND_ABOVE"
|
|
24
41
|
| "BLOCK_ONLY_HIGH";
|
|
25
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Content structure for Gemini API requests
|
|
45
|
+
*/
|
|
26
46
|
export interface GeminiContent {
|
|
47
|
+
/** Array of content parts (text, images, etc.) */
|
|
27
48
|
parts: GeminiPart[];
|
|
49
|
+
/** Role of the content creator (user or model) */
|
|
28
50
|
role?: "user" | "model";
|
|
29
51
|
}
|
|
30
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Individual content part
|
|
55
|
+
*/
|
|
31
56
|
export type GeminiPart = { text: string };
|
|
32
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Response structure from Gemini API
|
|
60
|
+
*/
|
|
33
61
|
export interface GeminiResponse {
|
|
62
|
+
/** Array of response candidates */
|
|
34
63
|
candidates?: GeminiCandidate[];
|
|
64
|
+
/** Token usage information */
|
|
35
65
|
usageMetadata?: GeminiUsageMetadata;
|
|
36
66
|
}
|
|
37
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Individual response candidate
|
|
70
|
+
*/
|
|
38
71
|
export interface GeminiCandidate {
|
|
72
|
+
/** Generated content */
|
|
39
73
|
content: GeminiContent;
|
|
74
|
+
/** Reason for generation completion */
|
|
40
75
|
finishReason?: GeminiFinishReason;
|
|
76
|
+
/** Safety ratings for the content */
|
|
41
77
|
safetyRatings?: GeminiSafetyRating[];
|
|
42
78
|
}
|
|
43
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Reasons why generation finished
|
|
82
|
+
*/
|
|
44
83
|
export type GeminiFinishReason =
|
|
45
84
|
| "FINISH_REASON_UNSPECIFIED"
|
|
46
85
|
| "STOP"
|
|
@@ -49,15 +88,26 @@ export type GeminiFinishReason =
|
|
|
49
88
|
| "RECITATION"
|
|
50
89
|
| "OTHER";
|
|
51
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Safety rating for generated content
|
|
93
|
+
*/
|
|
52
94
|
export interface GeminiSafetyRating {
|
|
95
|
+
/** Category of safety check */
|
|
53
96
|
category: GeminiHarmCategory;
|
|
97
|
+
/** Probability of content being unsafe */
|
|
54
98
|
probability: "NEGLIGIBLE" | "LOW" | "MEDIUM" | "HIGH";
|
|
99
|
+
/** Whether the content was blocked */
|
|
55
100
|
blocked?: boolean;
|
|
56
101
|
}
|
|
57
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Token usage metadata for the request
|
|
105
|
+
*/
|
|
58
106
|
export interface GeminiUsageMetadata {
|
|
107
|
+
/** Number of tokens in the prompt */
|
|
59
108
|
promptTokenCount?: number;
|
|
109
|
+
/** Number of tokens in the response candidates */
|
|
60
110
|
candidatesTokenCount?: number;
|
|
111
|
+
/** Total number of tokens used */
|
|
61
112
|
totalTokenCount?: number;
|
|
62
113
|
}
|
|
63
|
-
|
|
@@ -1,13 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Available Gemini AI models
|
|
3
|
+
*/
|
|
1
4
|
export const GEMINI_MODELS = {
|
|
5
|
+
/** Text generation models */
|
|
2
6
|
TEXT: {
|
|
7
|
+
/** Lightweight flash model for fast text generation */
|
|
3
8
|
FLASH_LITE: "gemini-2.5-flash-lite",
|
|
4
9
|
},
|
|
5
10
|
} as const;
|
|
6
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Default models to use for each category
|
|
14
|
+
*/
|
|
7
15
|
export const DEFAULT_MODELS = {
|
|
16
|
+
/** Default model for text generation */
|
|
8
17
|
TEXT: GEMINI_MODELS.TEXT.FLASH_LITE,
|
|
9
18
|
} as const;
|
|
10
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Pricing information for Gemini models
|
|
22
|
+
* Prices are per 1M tokens (USD)
|
|
23
|
+
*/
|
|
11
24
|
export const MODEL_PRICING = {
|
|
12
25
|
[GEMINI_MODELS.TEXT.FLASH_LITE]: { input: 0.10, output: 0.40, freePerDay: 1000 },
|
|
13
26
|
} as const;
|
package/src/index.ts
CHANGED
|
@@ -37,22 +37,70 @@ export type { GeminiProviderConfig } from "./infrastructure/services";
|
|
|
37
37
|
|
|
38
38
|
// Utils
|
|
39
39
|
export {
|
|
40
|
+
// Error handling
|
|
40
41
|
mapGeminiError,
|
|
41
42
|
isGeminiErrorRetryable,
|
|
42
43
|
categorizeGeminiError,
|
|
43
44
|
createGeminiError,
|
|
45
|
+
// Data transformation
|
|
44
46
|
extractTextFromResponse,
|
|
47
|
+
cleanJsonText,
|
|
48
|
+
parseJsonResponse,
|
|
49
|
+
safeParseJson,
|
|
50
|
+
extractJsonFromText,
|
|
51
|
+
toSdkContent,
|
|
52
|
+
createTextContent,
|
|
53
|
+
transformCandidate,
|
|
54
|
+
transformResponse,
|
|
55
|
+
extractTextFromParts,
|
|
56
|
+
// Performance
|
|
45
57
|
measureAsync,
|
|
46
58
|
measureSync,
|
|
47
59
|
debounce,
|
|
48
60
|
throttle,
|
|
49
61
|
PerformanceTimer,
|
|
62
|
+
// Rate limiting
|
|
50
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,
|
|
51
93
|
} from "./infrastructure/utils";
|
|
52
94
|
|
|
53
95
|
export type {
|
|
54
96
|
PerformanceMetrics,
|
|
55
97
|
RateLimiterOptions,
|
|
98
|
+
RetryOptions,
|
|
99
|
+
RetryResult,
|
|
100
|
+
EnvConfig,
|
|
101
|
+
AsyncStateCallbacks,
|
|
102
|
+
AsyncStateSetters,
|
|
103
|
+
AsyncStateConfig,
|
|
56
104
|
} from "./infrastructure/utils";
|
|
57
105
|
|
|
58
106
|
// Hooks
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { GoogleGenerativeAI, type GenerativeModel } from "@google/generative-ai";
|
|
2
2
|
import { DEFAULT_MODELS } from "../../domain/entities";
|
|
3
3
|
import type { GeminiConfig } from "../../domain/entities";
|
|
4
|
+
import { validateModelName, validateApiKey } from "../utils/validation.util";
|
|
4
5
|
|
|
5
6
|
const DEFAULT_CONFIG: Partial<GeminiConfig> = {
|
|
6
7
|
textModel: DEFAULT_MODELS.TEXT,
|
|
@@ -11,11 +12,19 @@ class GeminiClientCoreService {
|
|
|
11
12
|
private config: GeminiConfig | null = null;
|
|
12
13
|
private initialized = false;
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Initialize the Gemini client with configuration
|
|
17
|
+
*
|
|
18
|
+
* @throws {Error} If already initialized or API key is invalid
|
|
19
|
+
*/
|
|
14
20
|
initialize(config: GeminiConfig): void {
|
|
15
21
|
if (this.initialized) {
|
|
16
22
|
throw new Error("Gemini client already initialized. Call reset() before re-initializing with new config.");
|
|
17
23
|
}
|
|
18
24
|
|
|
25
|
+
// Validate API key
|
|
26
|
+
validateApiKey(config.apiKey);
|
|
27
|
+
|
|
19
28
|
this.client = new GoogleGenerativeAI(config.apiKey);
|
|
20
29
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
21
30
|
this.initialized = true;
|
|
@@ -39,19 +48,6 @@ class GeminiClientCoreService {
|
|
|
39
48
|
}
|
|
40
49
|
}
|
|
41
50
|
|
|
42
|
-
/**
|
|
43
|
-
* Validate model name format (allows any valid model string)
|
|
44
|
-
*/
|
|
45
|
-
private validateModel(modelName: string): void {
|
|
46
|
-
if (!modelName || typeof modelName !== "string" || modelName.trim().length === 0) {
|
|
47
|
-
throw new Error(`Invalid model name: "${modelName}". Model name must be a non-empty string.`);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Check for valid model format (starts with gemini-)
|
|
51
|
-
if (!modelName.startsWith("gemini-")) {
|
|
52
|
-
throw new Error(`Invalid model name: "${modelName}". Gemini models should start with "gemini-".`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
51
|
|
|
56
52
|
getModel(modelName?: string): GenerativeModel {
|
|
57
53
|
this.validateInitialization();
|
|
@@ -62,8 +58,8 @@ class GeminiClientCoreService {
|
|
|
62
58
|
|
|
63
59
|
const effectiveModel = modelName || this.config?.textModel || DEFAULT_MODELS.TEXT;
|
|
64
60
|
|
|
65
|
-
// Validate model name format
|
|
66
|
-
|
|
61
|
+
// Validate model name format
|
|
62
|
+
validateModelName(effectiveModel);
|
|
67
63
|
|
|
68
64
|
return this.client.getGenerativeModel({ model: effectiveModel });
|
|
69
65
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { GeminiConfig } from "../../domain/entities";
|
|
3
3
|
import { geminiClientCoreService } from "./gemini-client-core.service";
|
|
4
4
|
import { geminiStructuredTextService } from "./gemini-structured-text.service";
|
|
5
|
+
import { validatePrompt } from "../utils/validation.util";
|
|
5
6
|
|
|
6
7
|
export type GeminiProviderConfig = GeminiConfig;
|
|
7
8
|
|
|
@@ -9,6 +10,11 @@ export class GeminiProvider {
|
|
|
9
10
|
readonly providerId = "gemini";
|
|
10
11
|
readonly providerName = "Google Gemini";
|
|
11
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Initialize the Gemini provider
|
|
15
|
+
*
|
|
16
|
+
* @throws {Error} If already initialized or configuration is invalid
|
|
17
|
+
*/
|
|
12
18
|
initialize(config: GeminiProviderConfig): void {
|
|
13
19
|
if (geminiClientCoreService.isInitialized()) {
|
|
14
20
|
throw new Error("Provider already initialized. Call reset() before re-initializing with new config.");
|
|
@@ -16,22 +22,39 @@ export class GeminiProvider {
|
|
|
16
22
|
geminiClientCoreService.initialize(config);
|
|
17
23
|
}
|
|
18
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Check if provider is initialized
|
|
27
|
+
*/
|
|
19
28
|
isInitialized(): boolean {
|
|
20
29
|
return geminiClientCoreService.isInitialized();
|
|
21
30
|
}
|
|
22
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Reset the provider to uninitialized state
|
|
34
|
+
*/
|
|
23
35
|
reset(): void {
|
|
24
36
|
geminiClientCoreService.reset();
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
/**
|
|
28
40
|
* Generate structured JSON response
|
|
41
|
+
*
|
|
42
|
+
* @throws {GeminiError} For API-specific errors
|
|
43
|
+
* @throws {Error} For validation or network errors
|
|
29
44
|
*/
|
|
30
45
|
async generateStructuredText<T>(
|
|
31
46
|
prompt: string,
|
|
32
47
|
schema: Record<string, unknown>,
|
|
33
48
|
model: string,
|
|
34
49
|
): Promise<T> {
|
|
50
|
+
// Validate inputs
|
|
51
|
+
validatePrompt(prompt);
|
|
52
|
+
|
|
53
|
+
// Check if initialized
|
|
54
|
+
if (!this.isInitialized()) {
|
|
55
|
+
throw new Error("Provider not initialized. Call initialize() first.");
|
|
56
|
+
}
|
|
57
|
+
|
|
35
58
|
return geminiStructuredTextService.generateStructuredText<T>(model, prompt, schema);
|
|
36
59
|
}
|
|
37
60
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
|
|
2
2
|
import { geminiClientCoreService } from "./gemini-client-core.service";
|
|
3
|
+
import { toSdkContent } from "../utils/content-mapper.util";
|
|
4
|
+
import { createGeminiError } from "../utils/error-mapper.util";
|
|
5
|
+
import { telemetryHooks } from "../telemetry";
|
|
3
6
|
import type {
|
|
4
7
|
GeminiContent,
|
|
5
8
|
GeminiGenerationConfig,
|
|
@@ -8,6 +11,18 @@ import type {
|
|
|
8
11
|
class GeminiStreamingService {
|
|
9
12
|
/**
|
|
10
13
|
* Stream content generation
|
|
14
|
+
*
|
|
15
|
+
* @throws {GeminiError} For API-specific errors
|
|
16
|
+
* @throws {Error} For validation or network errors
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const fullText = await streamContent(
|
|
21
|
+
* "gemini-2.5-flash-lite",
|
|
22
|
+
* [{ parts: [{ text: "Hello" }], role: "user" }],
|
|
23
|
+
* (chunk) => console.log(chunk)
|
|
24
|
+
* );
|
|
25
|
+
* ```
|
|
11
26
|
*/
|
|
12
27
|
async streamContent(
|
|
13
28
|
model: string,
|
|
@@ -16,33 +31,76 @@ class GeminiStreamingService {
|
|
|
16
31
|
generationConfig?: GeminiGenerationConfig,
|
|
17
32
|
signal?: AbortSignal,
|
|
18
33
|
): Promise<string> {
|
|
19
|
-
|
|
34
|
+
// Validate input
|
|
35
|
+
if (!contents || contents.length === 0) {
|
|
36
|
+
throw new Error("Contents array cannot be empty");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof onChunk !== "function") {
|
|
40
|
+
throw new Error("onChunk must be a function");
|
|
41
|
+
}
|
|
20
42
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
43
|
+
// Check for early abort
|
|
44
|
+
if (signal?.aborted) {
|
|
45
|
+
throw new Error("Stream generation was aborted");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const genModel = geminiClientCoreService.getModel(model);
|
|
50
|
+
const sdkContents = toSdkContent(contents);
|
|
25
51
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
52
|
+
const requestOptions = {
|
|
53
|
+
contents: sdkContents as Parameters<typeof genModel.generateContentStream>[0] extends { contents: infer C } ? C : never,
|
|
54
|
+
generationConfig,
|
|
55
|
+
};
|
|
30
56
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
57
|
+
const result = signal
|
|
58
|
+
? await genModel.generateContentStream(requestOptions, { signal })
|
|
59
|
+
: await genModel.generateContentStream(requestOptions);
|
|
34
60
|
|
|
35
|
-
|
|
61
|
+
let fullText = "";
|
|
36
62
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
63
|
+
for await (const chunk of result.stream) {
|
|
64
|
+
try {
|
|
65
|
+
const chunkText = chunk.text();
|
|
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
|
+
}
|
|
42
87
|
}
|
|
43
|
-
}
|
|
44
88
|
|
|
45
|
-
|
|
89
|
+
return fullText;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
// Re-throw as GeminiError if it's an API error
|
|
92
|
+
if (error instanceof Error && error.name === "GeminiError") {
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check for abort error
|
|
97
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
98
|
+
throw new Error("Stream generation was aborted");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Wrap other errors
|
|
102
|
+
throw createGeminiError(error);
|
|
103
|
+
}
|
|
46
104
|
}
|
|
47
105
|
}
|
|
48
106
|
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
|
|
2
2
|
import { geminiTextGenerationService } from "./gemini-text-generation.service";
|
|
3
|
+
import { parseJsonResponse } from "../utils/json-parser.util";
|
|
4
|
+
import { extractTextFromParts } from "../utils/content-mapper.util";
|
|
5
|
+
import { validateSchema } from "../utils/validation.util";
|
|
3
6
|
import type { GenerationConfig } from "@google/generative-ai";
|
|
4
7
|
import type {
|
|
5
8
|
GeminiContent,
|
|
@@ -19,15 +22,11 @@ class GeminiStructuredTextService {
|
|
|
19
22
|
config?: Omit<GeminiGenerationConfig, "responseMimeType" | "responseSchema">,
|
|
20
23
|
signal?: AbortSignal,
|
|
21
24
|
): Promise<T> {
|
|
22
|
-
|
|
23
|
-
if (!schema || typeof schema !== "object" || Object.keys(schema).length === 0) {
|
|
24
|
-
throw new Error("Schema must be a non-empty object");
|
|
25
|
-
}
|
|
25
|
+
validateSchema(schema);
|
|
26
26
|
|
|
27
27
|
const generationConfig: GeminiGenerationConfig = {
|
|
28
28
|
...config,
|
|
29
29
|
responseMimeType: "application/json",
|
|
30
|
-
// Pass schema directly - Google SDK will validate it
|
|
31
30
|
responseSchema: schema as GenerationConfig["responseSchema"],
|
|
32
31
|
};
|
|
33
32
|
|
|
@@ -55,26 +54,13 @@ class GeminiStructuredTextService {
|
|
|
55
54
|
throw new Error("No candidates in response");
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (candidates[0]?.content?.parts) {
|
|
61
|
-
text = candidates[0].content.parts
|
|
62
|
-
.map((part) => "text" in part ? (part.text || "") : "")
|
|
63
|
-
.join("");
|
|
64
|
-
}
|
|
57
|
+
const text = extractTextFromParts(candidates[0].content.parts);
|
|
65
58
|
|
|
66
59
|
if (!text || text.trim().length === 0) {
|
|
67
60
|
throw new Error("Empty response received from Gemini");
|
|
68
61
|
}
|
|
69
62
|
|
|
70
|
-
|
|
71
|
-
const cleanedText = text.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
return JSON.parse(cleanedText) as T;
|
|
75
|
-
} catch (error) {
|
|
76
|
-
throw new Error(`Failed to parse structured response: ${error instanceof Error ? error.message : String(error)}. Cleaned text: ${cleanedText.substring(0, 200)}...`);
|
|
77
|
-
}
|
|
63
|
+
return parseJsonResponse<T>(text);
|
|
78
64
|
}
|
|
79
65
|
}
|
|
80
66
|
|