@umituz/react-native-ai-gemini-provider 2.0.16 → 2.0.17
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 +2 -2
- package/src/infrastructure/interceptors/RequestInterceptors.ts +6 -1
- package/src/infrastructure/interceptors/ResponseInterceptors.ts +7 -3
- package/src/infrastructure/interceptors/index.ts +1 -0
- package/src/infrastructure/services/gemini-client-core.service.ts +9 -7
- package/src/infrastructure/services/gemini-provider.ts +0 -32
- package/src/infrastructure/services/gemini-streaming.service.ts +7 -2
- package/src/infrastructure/services/gemini-structured-text.service.ts +16 -5
- package/src/infrastructure/services/gemini-text-generation.service.ts +37 -10
- package/src/infrastructure/services/index.ts +0 -1
- package/src/infrastructure/utils/async-state.util.ts +0 -1
- package/src/infrastructure/utils/gemini-data-transformer.util.ts +4 -3
- package/src/infrastructure/utils/index.ts +6 -5
- package/src/infrastructure/utils/rate-limiter.util.ts +23 -23
- package/src/presentation/hooks/use-gemini.ts +18 -6
- package/src/providers/ProviderFactory.ts +16 -6
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -30,7 +30,6 @@ export {
|
|
|
30
30
|
geminiStructuredTextService,
|
|
31
31
|
geminiStreamingService,
|
|
32
32
|
geminiProviderService,
|
|
33
|
-
createGeminiProvider,
|
|
34
33
|
GeminiProvider,
|
|
35
34
|
} from "./infrastructure/services";
|
|
36
35
|
|
|
@@ -42,6 +41,7 @@ export {
|
|
|
42
41
|
isGeminiErrorRetryable,
|
|
43
42
|
categorizeGeminiError,
|
|
44
43
|
createGeminiError,
|
|
44
|
+
extractTextFromResponse,
|
|
45
45
|
measureAsync,
|
|
46
46
|
measureSync,
|
|
47
47
|
debounce,
|
|
@@ -71,7 +71,7 @@ export type {
|
|
|
71
71
|
RequestContext,
|
|
72
72
|
RequestInterceptor,
|
|
73
73
|
InterceptorErrorStrategy,
|
|
74
|
-
} from "./infrastructure/interceptors
|
|
74
|
+
} from "./infrastructure/interceptors";
|
|
75
75
|
|
|
76
76
|
export type {
|
|
77
77
|
ResponseContext,
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
|
|
2
|
+
import { telemetryHooks } from "../telemetry";
|
|
3
|
+
|
|
2
4
|
export interface RequestContext {
|
|
3
5
|
model: string;
|
|
4
6
|
feature?: string;
|
|
@@ -47,6 +49,9 @@ class RequestInterceptors {
|
|
|
47
49
|
try {
|
|
48
50
|
result = await interceptor(result);
|
|
49
51
|
} catch (error) {
|
|
52
|
+
// Log to telemetry
|
|
53
|
+
telemetryHooks.logError(context.model, error instanceof Error ? error : new Error(String(error)), context.feature);
|
|
54
|
+
|
|
50
55
|
switch (this.errorStrategy) {
|
|
51
56
|
case "fail":
|
|
52
57
|
throw new Error(`Request interceptor failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -54,7 +59,7 @@ class RequestInterceptors {
|
|
|
54
59
|
// Skip this interceptor and continue with previous result
|
|
55
60
|
break;
|
|
56
61
|
case "log":
|
|
57
|
-
//
|
|
62
|
+
// Error already logged, continue with previous result
|
|
58
63
|
break;
|
|
59
64
|
}
|
|
60
65
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
|
|
2
|
+
import type { InterceptorErrorStrategy } from "./RequestInterceptors";
|
|
3
|
+
import { telemetryHooks } from "../telemetry";
|
|
4
|
+
|
|
2
5
|
export interface ResponseContext<T = unknown> {
|
|
3
6
|
model: string;
|
|
4
7
|
feature?: string;
|
|
@@ -11,8 +14,6 @@ export type ResponseInterceptor<T = unknown> = (
|
|
|
11
14
|
context: ResponseContext<T>,
|
|
12
15
|
) => ResponseContext<T> | Promise<ResponseContext<T>>;
|
|
13
16
|
|
|
14
|
-
export type InterceptorErrorStrategy = "fail" | "skip" | "log";
|
|
15
|
-
|
|
16
17
|
class ResponseInterceptors {
|
|
17
18
|
private interceptors: Array<ResponseInterceptor<unknown>> = [];
|
|
18
19
|
private errorStrategy: InterceptorErrorStrategy = "fail";
|
|
@@ -52,6 +53,9 @@ class ResponseInterceptors {
|
|
|
52
53
|
try {
|
|
53
54
|
result = await interceptor(result);
|
|
54
55
|
} catch (error) {
|
|
56
|
+
// Log to telemetry
|
|
57
|
+
telemetryHooks.logError(context.model, error instanceof Error ? error : new Error(String(error)), context.feature);
|
|
58
|
+
|
|
55
59
|
switch (this.errorStrategy) {
|
|
56
60
|
case "fail":
|
|
57
61
|
throw new Error(`Response interceptor failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -59,7 +63,7 @@ class ResponseInterceptors {
|
|
|
59
63
|
// Skip this interceptor and continue with previous result
|
|
60
64
|
break;
|
|
61
65
|
case "log":
|
|
62
|
-
//
|
|
66
|
+
// Error already logged, continue with previous result
|
|
63
67
|
break;
|
|
64
68
|
}
|
|
65
69
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GoogleGenerativeAI, type GenerativeModel } from "@google/generative-ai";
|
|
2
|
-
import { DEFAULT_MODELS
|
|
2
|
+
import { DEFAULT_MODELS } from "../../domain/entities";
|
|
3
3
|
import type { GeminiConfig } from "../../domain/entities";
|
|
4
4
|
|
|
5
5
|
const DEFAULT_CONFIG: Partial<GeminiConfig> = {
|
|
@@ -40,14 +40,16 @@ class GeminiClientCoreService {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* Validate model name
|
|
43
|
+
* Validate model name format (allows any valid model string)
|
|
44
44
|
*/
|
|
45
45
|
private validateModel(modelName: string): void {
|
|
46
|
-
|
|
47
|
-
|
|
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
|
+
}
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
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-".`);
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
55
|
|
|
@@ -60,7 +62,7 @@ class GeminiClientCoreService {
|
|
|
60
62
|
|
|
61
63
|
const effectiveModel = modelName || this.config?.textModel || DEFAULT_MODELS.TEXT;
|
|
62
64
|
|
|
63
|
-
// Validate model name
|
|
65
|
+
// Validate model name format (not against hardcoded list)
|
|
64
66
|
this.validateModel(effectiveModel);
|
|
65
67
|
|
|
66
68
|
return this.client.getGenerativeModel({ model: effectiveModel });
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
import type { GeminiConfig } from "../../domain/entities";
|
|
3
3
|
import { geminiClientCoreService } from "./gemini-client-core.service";
|
|
4
|
-
import { geminiTextGenerationService } from "./gemini-text-generation.service";
|
|
5
4
|
import { geminiStructuredTextService } from "./gemini-structured-text.service";
|
|
6
5
|
|
|
7
6
|
export type GeminiProviderConfig = GeminiConfig;
|
|
@@ -25,15 +24,6 @@ export class GeminiProvider {
|
|
|
25
24
|
geminiClientCoreService.reset();
|
|
26
25
|
}
|
|
27
26
|
|
|
28
|
-
/**
|
|
29
|
-
* Generate text from prompt
|
|
30
|
-
*/
|
|
31
|
-
async generateText(prompt: string, model: string): Promise<string> {
|
|
32
|
-
const contents = [{ parts: [{ text: prompt }], role: "user" as const }];
|
|
33
|
-
const response = await geminiTextGenerationService.generateContent(model, contents);
|
|
34
|
-
return this.extractTextFromResponse(response);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
27
|
/**
|
|
38
28
|
* Generate structured JSON response
|
|
39
29
|
*/
|
|
@@ -44,28 +34,6 @@ export class GeminiProvider {
|
|
|
44
34
|
): Promise<T> {
|
|
45
35
|
return geminiStructuredTextService.generateStructuredText<T>(model, prompt, schema);
|
|
46
36
|
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Extract text from Gemini response
|
|
50
|
-
*/
|
|
51
|
-
private extractTextFromResponse(response: unknown): string {
|
|
52
|
-
const resp = response as {
|
|
53
|
-
candidates?: Array<{
|
|
54
|
-
content: {
|
|
55
|
-
parts: Array<{ text?: string }>;
|
|
56
|
-
};
|
|
57
|
-
}>;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
return resp.candidates?.[0]?.content.parts
|
|
61
|
-
.filter((p): p is { text: string } => "text" in p && typeof p.text === "string")
|
|
62
|
-
.map((p) => p.text)
|
|
63
|
-
.join("") || "";
|
|
64
|
-
}
|
|
65
37
|
}
|
|
66
38
|
|
|
67
39
|
export const geminiProviderService = new GeminiProvider();
|
|
68
|
-
|
|
69
|
-
export function createGeminiProvider(): GeminiProvider {
|
|
70
|
-
return new GeminiProvider();
|
|
71
|
-
}
|
|
@@ -14,6 +14,7 @@ class GeminiStreamingService {
|
|
|
14
14
|
contents: GeminiContent[],
|
|
15
15
|
onChunk: (text: string) => void,
|
|
16
16
|
generationConfig?: GeminiGenerationConfig,
|
|
17
|
+
signal?: AbortSignal,
|
|
17
18
|
): Promise<string> {
|
|
18
19
|
const genModel = geminiClientCoreService.getModel(model);
|
|
19
20
|
|
|
@@ -22,10 +23,14 @@ class GeminiStreamingService {
|
|
|
22
23
|
parts: content.parts.map((part) => ({ text: part.text })),
|
|
23
24
|
}));
|
|
24
25
|
|
|
25
|
-
const
|
|
26
|
+
const requestOptions = {
|
|
26
27
|
contents: sdkContents as Parameters<typeof genModel.generateContentStream>[0] extends { contents: infer C } ? C : never,
|
|
27
28
|
generationConfig,
|
|
28
|
-
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const result = signal
|
|
32
|
+
? await genModel.generateContentStream(requestOptions, { signal })
|
|
33
|
+
: await genModel.generateContentStream(requestOptions);
|
|
29
34
|
|
|
30
35
|
let fullText = "";
|
|
31
36
|
|
|
@@ -4,6 +4,7 @@ import type { GenerationConfig } from "@google/generative-ai";
|
|
|
4
4
|
import type {
|
|
5
5
|
GeminiContent,
|
|
6
6
|
GeminiGenerationConfig,
|
|
7
|
+
GeminiResponse,
|
|
7
8
|
} from "../../domain/entities";
|
|
8
9
|
|
|
9
10
|
|
|
@@ -16,6 +17,7 @@ class GeminiStructuredTextService {
|
|
|
16
17
|
prompt: string,
|
|
17
18
|
schema: Record<string, unknown>,
|
|
18
19
|
config?: Omit<GeminiGenerationConfig, "responseMimeType" | "responseSchema">,
|
|
20
|
+
signal?: AbortSignal,
|
|
19
21
|
): Promise<T> {
|
|
20
22
|
// Validate schema structure before passing to SDK
|
|
21
23
|
if (!schema || typeof schema !== "object" || Object.keys(schema).length === 0) {
|
|
@@ -37,6 +39,7 @@ class GeminiStructuredTextService {
|
|
|
37
39
|
model,
|
|
38
40
|
contents,
|
|
39
41
|
generationConfig,
|
|
42
|
+
signal,
|
|
40
43
|
);
|
|
41
44
|
|
|
42
45
|
return this.parseJSONResponse<T>(response);
|
|
@@ -45,24 +48,32 @@ class GeminiStructuredTextService {
|
|
|
45
48
|
/**
|
|
46
49
|
* Parse JSON response from Gemini
|
|
47
50
|
*/
|
|
48
|
-
private parseJSONResponse<T>(response:
|
|
49
|
-
const candidates =
|
|
51
|
+
private parseJSONResponse<T>(response: GeminiResponse): T {
|
|
52
|
+
const candidates = response.candidates;
|
|
53
|
+
|
|
54
|
+
if (!candidates || candidates.length === 0) {
|
|
55
|
+
throw new Error("No candidates in response");
|
|
56
|
+
}
|
|
50
57
|
|
|
51
58
|
let text = "";
|
|
52
59
|
|
|
53
|
-
if (candidates
|
|
60
|
+
if (candidates[0]?.content?.parts) {
|
|
54
61
|
text = candidates[0].content.parts
|
|
55
|
-
.map((part) => part.text || "")
|
|
62
|
+
.map((part) => "text" in part ? (part.text || "") : "")
|
|
56
63
|
.join("");
|
|
57
64
|
}
|
|
58
65
|
|
|
66
|
+
if (!text || text.trim().length === 0) {
|
|
67
|
+
throw new Error("Empty response received from Gemini");
|
|
68
|
+
}
|
|
69
|
+
|
|
59
70
|
// Clean and parse JSON (remove markdown code blocks if present)
|
|
60
71
|
const cleanedText = text.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
|
|
61
72
|
|
|
62
73
|
try {
|
|
63
74
|
return JSON.parse(cleanedText) as T;
|
|
64
75
|
} catch (error) {
|
|
65
|
-
throw new Error(`Failed to parse structured response: ${error instanceof Error ? error.message : String(error)}
|
|
76
|
+
throw new Error(`Failed to parse structured response: ${error instanceof Error ? error.message : String(error)}. Cleaned text: ${cleanedText.substring(0, 200)}...`);
|
|
66
77
|
}
|
|
67
78
|
}
|
|
68
79
|
}
|
|
@@ -6,6 +6,8 @@ import type {
|
|
|
6
6
|
GeminiGenerationConfig,
|
|
7
7
|
GeminiResponse,
|
|
8
8
|
GeminiPart,
|
|
9
|
+
GeminiFinishReason,
|
|
10
|
+
GeminiSafetyRating,
|
|
9
11
|
} from "../../domain/entities";
|
|
10
12
|
|
|
11
13
|
class GeminiTextGenerationService {
|
|
@@ -16,6 +18,7 @@ class GeminiTextGenerationService {
|
|
|
16
18
|
model: string,
|
|
17
19
|
contents: GeminiContent[],
|
|
18
20
|
generationConfig?: GeminiGenerationConfig,
|
|
21
|
+
signal?: AbortSignal,
|
|
19
22
|
): Promise<GeminiResponse> {
|
|
20
23
|
const genModel = geminiClientCoreService.getModel(model);
|
|
21
24
|
|
|
@@ -24,12 +27,20 @@ class GeminiTextGenerationService {
|
|
|
24
27
|
parts: content.parts,
|
|
25
28
|
}));
|
|
26
29
|
|
|
27
|
-
const
|
|
30
|
+
const requestOptions = {
|
|
28
31
|
contents: sdkContents as Parameters<typeof genModel.generateContent>[0] extends { contents: infer C } ? C : never,
|
|
29
32
|
generationConfig,
|
|
30
|
-
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const result = signal
|
|
36
|
+
? await genModel.generateContent(requestOptions, { signal })
|
|
37
|
+
: await genModel.generateContent(requestOptions);
|
|
31
38
|
|
|
32
|
-
const response =
|
|
39
|
+
const response = result.response;
|
|
40
|
+
|
|
41
|
+
if (!response) {
|
|
42
|
+
throw new Error("No response received from Gemini API");
|
|
43
|
+
}
|
|
33
44
|
|
|
34
45
|
return {
|
|
35
46
|
candidates: response.candidates?.map((candidate) => {
|
|
@@ -41,14 +52,33 @@ class GeminiTextGenerationService {
|
|
|
41
52
|
// Ignore unsupported part types (inlineData, etc.)
|
|
42
53
|
}
|
|
43
54
|
|
|
55
|
+
// Map SDK finish reason to our domain type
|
|
56
|
+
const finishReason: GeminiFinishReason | undefined = candidate.finishReason
|
|
57
|
+
? (candidate.finishReason as GeminiFinishReason)
|
|
58
|
+
: undefined;
|
|
59
|
+
|
|
60
|
+
// Map safety ratings
|
|
61
|
+
const safetyRatings: GeminiSafetyRating[] | undefined = candidate.safetyRatings
|
|
62
|
+
? candidate.safetyRatings.map((rating) => ({
|
|
63
|
+
category: rating.category as GeminiSafetyRating["category"],
|
|
64
|
+
probability: rating.probability as GeminiSafetyRating["probability"],
|
|
65
|
+
}))
|
|
66
|
+
: undefined;
|
|
67
|
+
|
|
44
68
|
return {
|
|
45
69
|
content: {
|
|
46
70
|
parts: transformedParts,
|
|
47
|
-
role: (candidate.content.role || "model"),
|
|
71
|
+
role: (candidate.content.role || "model") as "user" | "model",
|
|
48
72
|
},
|
|
49
|
-
finishReason
|
|
73
|
+
finishReason,
|
|
74
|
+
safetyRatings,
|
|
50
75
|
};
|
|
51
76
|
}),
|
|
77
|
+
usageMetadata: response.usageMetadata ? {
|
|
78
|
+
promptTokenCount: response.usageMetadata.promptTokenCount,
|
|
79
|
+
candidatesTokenCount: response.usageMetadata.candidatesTokenCount,
|
|
80
|
+
totalTokenCount: response.usageMetadata.totalTokenCount,
|
|
81
|
+
} : undefined,
|
|
52
82
|
};
|
|
53
83
|
}
|
|
54
84
|
|
|
@@ -59,18 +89,15 @@ class GeminiTextGenerationService {
|
|
|
59
89
|
model: string,
|
|
60
90
|
prompt: string,
|
|
61
91
|
config?: GeminiGenerationConfig,
|
|
92
|
+
signal?: AbortSignal,
|
|
62
93
|
): Promise<string> {
|
|
63
94
|
const contents: GeminiContent[] = [
|
|
64
95
|
{ parts: [{ text: prompt }], role: "user" },
|
|
65
96
|
];
|
|
66
97
|
|
|
67
|
-
const response = await this.generateContent(model, contents, config);
|
|
98
|
+
const response = await this.generateContent(model, contents, config, signal);
|
|
68
99
|
return extractTextFromResponse(response);
|
|
69
100
|
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Generate content with images (multimodal)
|
|
73
|
-
*/
|
|
74
101
|
}
|
|
75
102
|
|
|
76
103
|
export const geminiTextGenerationService = new GeminiTextGenerationService();
|
|
@@ -12,7 +12,6 @@ export { geminiStreamingService } from "./gemini-streaming.service";
|
|
|
12
12
|
// Provider
|
|
13
13
|
export {
|
|
14
14
|
geminiProviderService,
|
|
15
|
-
createGeminiProvider,
|
|
16
15
|
GeminiProvider,
|
|
17
16
|
} from "./gemini-provider";
|
|
18
17
|
export type { GeminiProviderConfig } from "./gemini-provider";
|
|
@@ -3,12 +3,12 @@ import type { GeminiResponse } from "../../domain/entities";
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
export function extractTextFromResponse(response: GeminiResponse): string {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
if (!candidate) {
|
|
6
|
+
if (!response.candidates || response.candidates.length === 0) {
|
|
9
7
|
throw new Error("No response candidates");
|
|
10
8
|
}
|
|
11
9
|
|
|
10
|
+
const candidate = response.candidates[0];
|
|
11
|
+
|
|
12
12
|
// Handle all finish reasons appropriately
|
|
13
13
|
switch (candidate.finishReason) {
|
|
14
14
|
case "SAFETY":
|
|
@@ -19,6 +19,7 @@ export function extractTextFromResponse(response: GeminiResponse): string {
|
|
|
19
19
|
case "FINISH_REASON_UNSPECIFIED":
|
|
20
20
|
case "OTHER":
|
|
21
21
|
case "STOP":
|
|
22
|
+
case undefined:
|
|
22
23
|
// Continue to extract text
|
|
23
24
|
break;
|
|
24
25
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export
|
|
5
|
-
export
|
|
1
|
+
export { mapGeminiError, isGeminiErrorRetryable, categorizeGeminiError, createGeminiError } from "./error-mapper.util";
|
|
2
|
+
export { extractTextFromResponse } from "./gemini-data-transformer.util";
|
|
3
|
+
export { measureAsync, measureSync, debounce, throttle, PerformanceTimer } from "./performance.util";
|
|
4
|
+
export { RateLimiter } from "./rate-limiter.util";
|
|
5
|
+
export type { PerformanceMetrics } from "./performance.util";
|
|
6
|
+
export type { RateLimiterOptions } from "./rate-limiter.util";
|
|
@@ -10,7 +10,6 @@ export class RateLimiter {
|
|
|
10
10
|
private lastRequest = 0;
|
|
11
11
|
private minInterval: number;
|
|
12
12
|
private maxQueueSize: number;
|
|
13
|
-
private processQueuePromise: Promise<void> | null = null;
|
|
14
13
|
|
|
15
14
|
constructor(options: RateLimiterOptions = {}) {
|
|
16
15
|
this.minInterval = options.minInterval ?? 100; // 100ms minimum interval
|
|
@@ -32,36 +31,37 @@ export class RateLimiter {
|
|
|
32
31
|
}
|
|
33
32
|
});
|
|
34
33
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
this.processQueuePromise = null;
|
|
40
|
-
}).catch(() => {
|
|
41
|
-
this.processQueuePromise = null;
|
|
42
|
-
});
|
|
43
|
-
}
|
|
34
|
+
// Start queue processing if not already running
|
|
35
|
+
this.processQueue().catch(() => {
|
|
36
|
+
// Individual task errors are handled above, ignore queue processing errors
|
|
37
|
+
});
|
|
44
38
|
});
|
|
45
39
|
}
|
|
46
40
|
|
|
47
|
-
private async processQueue() {
|
|
48
|
-
|
|
41
|
+
private async processQueue(): Promise<void> {
|
|
42
|
+
// Only one processQueue can run at a time
|
|
43
|
+
if (this.processing) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
49
47
|
this.processing = true;
|
|
50
48
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
try {
|
|
50
|
+
while (this.queue.length > 0) {
|
|
51
|
+
const elapsed = Date.now() - this.lastRequest;
|
|
52
|
+
if (elapsed < this.minInterval) {
|
|
53
|
+
await new Promise((r) => setTimeout(r, this.minInterval - elapsed));
|
|
54
|
+
}
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
const task = this.queue.shift();
|
|
57
|
+
if (task) {
|
|
58
|
+
this.lastRequest = Date.now();
|
|
59
|
+
await task();
|
|
60
|
+
}
|
|
61
61
|
}
|
|
62
|
+
} finally {
|
|
63
|
+
this.processing = false;
|
|
62
64
|
}
|
|
63
|
-
|
|
64
|
-
this.processing = false;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
getQueueSize(): number {
|
|
@@ -3,7 +3,7 @@ import { useState, useCallback, useRef, useMemo, useEffect } from "react";
|
|
|
3
3
|
import type { GeminiGenerationConfig } from "../../domain/entities";
|
|
4
4
|
import { DEFAULT_MODELS } from "../../domain/entities";
|
|
5
5
|
import { geminiTextGenerationService, geminiStructuredTextService } from "../../infrastructure/services";
|
|
6
|
-
import { executeWithState } from "../../infrastructure/utils";
|
|
6
|
+
import { executeWithState } from "../../infrastructure/utils/async-state.util";
|
|
7
7
|
|
|
8
8
|
export interface UseGeminiOptions {
|
|
9
9
|
model?: string;
|
|
@@ -41,6 +41,11 @@ export function useGemini(options: UseGeminiOptions = {}): UseGeminiReturn {
|
|
|
41
41
|
const model = options.model ?? DEFAULT_MODELS.TEXT;
|
|
42
42
|
|
|
43
43
|
const generate = useCallback(async (prompt: string) => {
|
|
44
|
+
// Abort previous operation if still running
|
|
45
|
+
if (abortControllerRef.current) {
|
|
46
|
+
abortControllerRef.current.abort();
|
|
47
|
+
}
|
|
48
|
+
|
|
44
49
|
// Create new abort controller for this operation
|
|
45
50
|
const controller = new AbortController();
|
|
46
51
|
abortControllerRef.current = controller;
|
|
@@ -54,11 +59,12 @@ export function useGemini(options: UseGeminiOptions = {}): UseGeminiReturn {
|
|
|
54
59
|
async () => {
|
|
55
60
|
// Check if this operation is still the latest one
|
|
56
61
|
if (currentOpId !== operationIdRef.current) {
|
|
62
|
+
controller.abort();
|
|
57
63
|
throw new Error("Operation cancelled by newer request");
|
|
58
64
|
}
|
|
59
|
-
return geminiTextGenerationService.generateText(model, prompt, options.generationConfig);
|
|
65
|
+
return geminiTextGenerationService.generateText(model, prompt, options.generationConfig, controller.signal);
|
|
60
66
|
},
|
|
61
|
-
(text) => {
|
|
67
|
+
(text: string) => {
|
|
62
68
|
// Only update if this is still the latest operation
|
|
63
69
|
if (currentOpId === operationIdRef.current) {
|
|
64
70
|
setResult(text);
|
|
@@ -75,6 +81,11 @@ export function useGemini(options: UseGeminiOptions = {}): UseGeminiReturn {
|
|
|
75
81
|
}, [model, options.generationConfig, setters, callbacks, options.onSuccess]);
|
|
76
82
|
|
|
77
83
|
const generateJSON = useCallback(async <T>(prompt: string, schema?: Record<string, unknown>): Promise<T | null> => {
|
|
84
|
+
// Abort previous operation if still running
|
|
85
|
+
if (abortControllerRef.current) {
|
|
86
|
+
abortControllerRef.current.abort();
|
|
87
|
+
}
|
|
88
|
+
|
|
78
89
|
// Create new abort controller for this operation
|
|
79
90
|
const controller = new AbortController();
|
|
80
91
|
abortControllerRef.current = controller;
|
|
@@ -88,14 +99,15 @@ export function useGemini(options: UseGeminiOptions = {}): UseGeminiReturn {
|
|
|
88
99
|
async () => {
|
|
89
100
|
// Check if this operation is still the latest one
|
|
90
101
|
if (currentOpId !== operationIdRef.current) {
|
|
102
|
+
controller.abort();
|
|
91
103
|
throw new Error("Operation cancelled by newer request");
|
|
92
104
|
}
|
|
93
105
|
|
|
94
106
|
if (schema) {
|
|
95
|
-
return geminiStructuredTextService.generateStructuredText<T>(model, prompt, schema, options.generationConfig);
|
|
107
|
+
return geminiStructuredTextService.generateStructuredText<T>(model, prompt, schema, options.generationConfig, controller.signal);
|
|
96
108
|
}
|
|
97
109
|
|
|
98
|
-
const text = await geminiTextGenerationService.generateText(model, prompt, { ...options.generationConfig, responseMimeType: "application/json" });
|
|
110
|
+
const text = await geminiTextGenerationService.generateText(model, prompt, { ...options.generationConfig, responseMimeType: "application/json" }, controller.signal);
|
|
99
111
|
const cleanedText = cleanJsonResponse(text);
|
|
100
112
|
|
|
101
113
|
try {
|
|
@@ -104,7 +116,7 @@ export function useGemini(options: UseGeminiOptions = {}): UseGeminiReturn {
|
|
|
104
116
|
throw new Error(`Failed to parse JSON response: ${parseError instanceof Error ? parseError.message : String(parseError)}. Response: ${cleanedText.substring(0, 200)}...`);
|
|
105
117
|
}
|
|
106
118
|
},
|
|
107
|
-
(parsed) => {
|
|
119
|
+
(parsed: unknown) => {
|
|
108
120
|
// Only update if this is still the latest operation
|
|
109
121
|
if (currentOpId === operationIdRef.current) {
|
|
110
122
|
setJsonResult(parsed);
|
|
@@ -14,6 +14,7 @@ export interface ProviderFactoryOptions extends ProviderConfigInput {
|
|
|
14
14
|
|
|
15
15
|
class ProviderFactory {
|
|
16
16
|
private currentConfig: ResolvedProviderConfig | null = null;
|
|
17
|
+
private currentOptions: ProviderFactoryOptions | null = null;
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Initialize provider with configuration
|
|
@@ -27,6 +28,7 @@ class ProviderFactory {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
this.currentConfig = config;
|
|
31
|
+
this.currentOptions = options;
|
|
30
32
|
|
|
31
33
|
// Initialize Gemini client with resolved config
|
|
32
34
|
const geminiConfig: GeminiConfig = {
|
|
@@ -57,25 +59,33 @@ class ProviderFactory {
|
|
|
57
59
|
* Note: Changing apiKey requires full re-initialization
|
|
58
60
|
*/
|
|
59
61
|
updateConfig(updates: Partial<ProviderConfigInput>): void {
|
|
60
|
-
if (!this.currentConfig) {
|
|
62
|
+
if (!this.currentConfig || !this.currentOptions) {
|
|
61
63
|
throw new Error("Provider not initialized. Call initialize() first.");
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
// If API key is changing, we need to re-initialize
|
|
65
67
|
if (updates.apiKey && updates.apiKey !== this.currentConfig.apiKey) {
|
|
66
|
-
const newInput:
|
|
68
|
+
const newInput: ProviderFactoryOptions = {
|
|
67
69
|
apiKey: updates.apiKey,
|
|
68
|
-
preferences: updates.preferences ||
|
|
70
|
+
preferences: updates.preferences || this.currentOptions.preferences,
|
|
71
|
+
strategy: this.currentOptions.strategy,
|
|
69
72
|
};
|
|
70
73
|
this.initialize(newInput);
|
|
71
74
|
return;
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
// For other updates, merge with current config
|
|
75
|
-
|
|
76
|
-
...this.
|
|
78
|
+
const mergedPreferences = {
|
|
79
|
+
...this.currentOptions.preferences,
|
|
77
80
|
...updates.preferences,
|
|
78
|
-
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
this.currentOptions.preferences = mergedPreferences;
|
|
84
|
+
|
|
85
|
+
this.currentConfig = {
|
|
86
|
+
apiKey: this.currentConfig.apiKey,
|
|
87
|
+
textModel: this.currentConfig.textModel,
|
|
88
|
+
timeout: mergedPreferences.timeout ?? this.currentConfig.timeout,
|
|
79
89
|
};
|
|
80
90
|
}
|
|
81
91
|
}
|