@umituz/react-native-ai-gemini-provider 3.0.28 → 3.0.30
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/dist/index.d.ts +5 -6
- package/dist/infrastructure/services/BaseService.d.ts +4 -9
- package/dist/infrastructure/services/ChatSession.d.ts +0 -4
- package/dist/infrastructure/services/GeminiClient.d.ts +1 -4
- package/dist/infrastructure/services/StructuredText.d.ts +0 -1
- package/dist/infrastructure/telemetry/TelemetryHooks.d.ts +10 -26
- package/dist/infrastructure/utils/async/execute-state.util.d.ts +0 -1
- package/dist/infrastructure/utils/content-mapper.util.d.ts +3 -3
- package/dist/presentation/hooks/useGemini.d.ts +1 -1
- package/dist/providers/ConfigBuilder.d.ts +0 -4
- package/dist/providers/ProviderFactory.d.ts +0 -6
- package/package.json +1 -1
- package/src/index.ts +0 -2
- package/src/infrastructure/services/BaseService.ts +3 -2
- package/src/infrastructure/services/ChatSession.ts +9 -105
- package/src/infrastructure/services/GeminiClient.ts +37 -25
- package/src/infrastructure/services/StructuredText.ts +0 -7
- package/src/infrastructure/telemetry/TelemetryHooks.ts +10 -70
- package/src/infrastructure/utils/async/execute-state.util.ts +0 -6
- package/src/infrastructure/utils/content-mapper.util.ts +10 -2
- package/src/infrastructure/utils/stream-processor.util.ts +12 -6
- package/src/infrastructure/utils/text-calculations.util.ts +70 -0
- package/src/presentation/hooks/useGemini.ts +5 -18
- package/src/providers/ConfigBuilder.ts +0 -13
- package/src/providers/ProviderFactory.ts +7 -16
- package/src/global.d.ts +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -5,12 +5,11 @@
|
|
|
5
5
|
export type { GeminiConfig, GeminiGenerationConfig, GeminiHarmCategory, GeminiHarmBlockThreshold, GeminiContent, GeminiPart, GeminiInlineDataPart, GeminiMessagePart, GeminiSafetySetting, GeminiModelOptions, GeminiChatConfig, GeminiResponse, GeminiCandidate, GeminiFinishReason, GeminiSafetyRating, GeminiUsageMetadata, GeminiErrorInfo, GeminiApiError, } from "./domain/entities";
|
|
6
6
|
export { GeminiErrorType, GeminiError, GEMINI_MODELS, DEFAULT_MODELS } from "./domain/entities";
|
|
7
7
|
export { geminiClient } from "./infrastructure/services/GeminiClient";
|
|
8
|
-
export { createChatSession, sendChatMessage, buildChatHistory, trimChatHistory,
|
|
8
|
+
export { createChatSession, sendChatMessage, buildChatHistory, trimChatHistory, type ChatSendResult, type ChatHistoryMessage, type SendChatMessageOptions, } from "./infrastructure/services/ChatSession";
|
|
9
9
|
export { textGeneration } from "./infrastructure/services/TextGeneration";
|
|
10
10
|
export { structuredText } from "./infrastructure/services/StructuredText";
|
|
11
11
|
export { streaming } from "./infrastructure/services/Streaming";
|
|
12
|
-
export {
|
|
13
|
-
export {
|
|
14
|
-
export
|
|
15
|
-
export {
|
|
16
|
-
export type { ProviderConfig, ProviderFactoryOptions, } from "./providers";
|
|
12
|
+
export { useGemini } from "./presentation/hooks/useGemini";
|
|
13
|
+
export type { UseGeminiOptions, UseGeminiReturn } from "./presentation/hooks/useGemini";
|
|
14
|
+
export { ConfigBuilder, providerFactory } from "./providers/ProviderFactory";
|
|
15
|
+
export type { ProviderConfig, ProviderFactoryOptions, } from "./providers/ProviderFactory";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { GeminiContent, GeminiGenerationConfig } from "../../domain/entities";
|
|
2
2
|
import type { GenerativeModel } from "@google/generative-ai";
|
|
3
|
+
import type { Part } from "@google/generative-ai";
|
|
3
4
|
export interface BaseRequestOptions {
|
|
4
5
|
model: string;
|
|
5
6
|
contents: GeminiContent[];
|
|
@@ -11,23 +12,17 @@ export declare abstract class BaseGeminiService {
|
|
|
11
12
|
genModel: GenerativeModel;
|
|
12
13
|
sdkContents: Array<{
|
|
13
14
|
role: string;
|
|
14
|
-
parts:
|
|
15
|
-
text: string;
|
|
16
|
-
}>;
|
|
15
|
+
parts: Part[];
|
|
17
16
|
}>;
|
|
18
17
|
};
|
|
19
18
|
protected handleError(error: unknown, abortMessage: string): never;
|
|
20
19
|
protected createRequestOptions(sdkContents: Array<{
|
|
21
20
|
role: string;
|
|
22
|
-
parts:
|
|
23
|
-
text: string;
|
|
24
|
-
}>;
|
|
21
|
+
parts: Part[];
|
|
25
22
|
}>, generationConfig?: GeminiGenerationConfig): {
|
|
26
23
|
contents: {
|
|
27
24
|
role: string;
|
|
28
|
-
parts:
|
|
29
|
-
text: string;
|
|
30
|
-
}>;
|
|
25
|
+
parts: Part[];
|
|
31
26
|
}[];
|
|
32
27
|
generationConfig: import("@google/generative-ai").GenerationConfig | undefined;
|
|
33
28
|
};
|
|
@@ -29,10 +29,6 @@ export interface SendChatMessageOptions {
|
|
|
29
29
|
/** Min messages to always keep regardless of budget (default 4) */
|
|
30
30
|
historyMinMessages?: number;
|
|
31
31
|
}
|
|
32
|
-
/** Resolve MIME type for an audio file extension */
|
|
33
|
-
export declare function resolveAudioMimeType(extension: string): string;
|
|
34
|
-
/** Resolve MIME type for an image file extension */
|
|
35
|
-
export declare function resolveImageMimeType(extension: string): string;
|
|
36
32
|
/**
|
|
37
33
|
* Converts chat messages (user/assistant/system) to Gemini SDK content format.
|
|
38
34
|
* Skips system messages, merges consecutive same-role messages.
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type GenerativeModel } from "@google/generative-ai";
|
|
2
2
|
import type { GeminiConfig, GeminiModelOptions } from "../../domain/entities";
|
|
3
3
|
declare class GeminiClient {
|
|
4
4
|
private client;
|
|
5
5
|
private config;
|
|
6
6
|
private initialized;
|
|
7
7
|
initialize(config: GeminiConfig): void;
|
|
8
|
-
isInitialized(): boolean;
|
|
9
|
-
getConfig(): GeminiConfig | null;
|
|
10
|
-
getClient(): GoogleGenerativeAI | null;
|
|
11
8
|
/**
|
|
12
9
|
* Returns a GenerativeModel configured with optional safety settings and system instruction.
|
|
13
10
|
* When no safety settings are provided, defaults to BLOCK_NONE for all categories.
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { GeminiGenerationConfig } from "../../domain/entities";
|
|
2
2
|
declare class StructuredTextService {
|
|
3
3
|
generateStructuredText<T>(model: string, prompt: string, schema: Record<string, unknown>, config?: Omit<GeminiGenerationConfig, "responseMimeType" | "responseSchema">, signal?: AbortSignal): Promise<T>;
|
|
4
|
-
private parseJSONResponse;
|
|
5
4
|
}
|
|
6
5
|
export declare const structuredText: StructuredTextService;
|
|
7
6
|
export {};
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
export interface TelemetryEvent {
|
|
2
|
-
type: "
|
|
2
|
+
type: "error";
|
|
3
3
|
timestamp: number;
|
|
4
4
|
model?: string;
|
|
5
5
|
feature?: string;
|
|
6
|
-
duration?: number;
|
|
7
6
|
metadata?: Record<string, unknown>;
|
|
8
7
|
}
|
|
9
8
|
export type TelemetryListener = (event: TelemetryEvent) => void;
|
|
@@ -14,44 +13,29 @@ declare class TelemetryHooks {
|
|
|
14
13
|
private listenerFailureCounts;
|
|
15
14
|
/**
|
|
16
15
|
* Register a telemetry listener
|
|
16
|
+
*
|
|
17
|
+
* @returns Unsubscribe function - IMPORTANT: Call this when done listening to prevent memory leaks
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* const unsubscribe = telemetryHooks.subscribe((event) => console.log(event));
|
|
22
|
+
* // ... later when done
|
|
23
|
+
* unsubscribe(); // Prevents memory leak
|
|
24
|
+
* ```
|
|
17
25
|
*/
|
|
18
26
|
subscribe(listener: TelemetryListener): () => void;
|
|
19
27
|
/**
|
|
20
28
|
* Emit a telemetry event to all listeners
|
|
21
29
|
*/
|
|
22
30
|
emit(event: TelemetryEvent): void;
|
|
23
|
-
/**
|
|
24
|
-
* Log request start
|
|
25
|
-
*/
|
|
26
|
-
logRequest(model: string, feature?: string): number;
|
|
27
|
-
/**
|
|
28
|
-
* Log response received
|
|
29
|
-
*/
|
|
30
|
-
logResponse(model: string, startTime: number, feature?: string, metadata?: Record<string, unknown>): void;
|
|
31
31
|
/**
|
|
32
32
|
* Log error
|
|
33
33
|
*/
|
|
34
34
|
logError(model: string, error: Error, feature?: string): void;
|
|
35
|
-
/**
|
|
36
|
-
* Log retry attempt
|
|
37
|
-
*/
|
|
38
|
-
logRetry(model: string, attempt: number, feature?: string): void;
|
|
39
35
|
/**
|
|
40
36
|
* Remove a specific listener
|
|
41
37
|
*/
|
|
42
38
|
unsubscribe(listener: TelemetryListener): void;
|
|
43
|
-
/**
|
|
44
|
-
* Reset failure counts for all listeners (clear blacklist)
|
|
45
|
-
*/
|
|
46
|
-
resetFailures(): void;
|
|
47
|
-
/**
|
|
48
|
-
* Clear all listeners
|
|
49
|
-
*/
|
|
50
|
-
clear(): void;
|
|
51
|
-
/**
|
|
52
|
-
* Get current listener count
|
|
53
|
-
*/
|
|
54
|
-
getListenerCount(): number;
|
|
55
39
|
}
|
|
56
40
|
export declare const telemetryHooks: TelemetryHooks;
|
|
57
41
|
export {};
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
* Content Mapper Utilities
|
|
3
3
|
* Handles transformation between domain content and SDK format
|
|
4
4
|
*/
|
|
5
|
+
import type { Part } from "@google/generative-ai";
|
|
5
6
|
import type { GeminiContent, GeminiPart, GeminiResponse } from "../../domain/entities";
|
|
6
7
|
/**
|
|
7
8
|
* Convert domain content to SDK format
|
|
9
|
+
* Preserves both text and inlineData parts
|
|
8
10
|
*/
|
|
9
11
|
export declare function toSdkContent(contents: GeminiContent[]): Array<{
|
|
10
12
|
role: string;
|
|
11
|
-
parts:
|
|
12
|
-
text: string;
|
|
13
|
-
}>;
|
|
13
|
+
parts: Part[];
|
|
14
14
|
}>;
|
|
15
15
|
/**
|
|
16
16
|
* Create a simple text content
|
|
@@ -7,7 +7,7 @@ export interface UseGeminiOptions {
|
|
|
7
7
|
}
|
|
8
8
|
export interface UseGeminiReturn {
|
|
9
9
|
generate: (prompt: string) => Promise<void>;
|
|
10
|
-
generateJSON: <T>(prompt: string, schema
|
|
10
|
+
generateJSON: <T>(prompt: string, schema: Record<string, unknown>) => Promise<T | null>;
|
|
11
11
|
result: string | null;
|
|
12
12
|
jsonResult: unknown;
|
|
13
13
|
isGenerating: boolean;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { type ProviderConfig } from "./ConfigBuilder";
|
|
2
1
|
export { ConfigBuilder } from "./ConfigBuilder";
|
|
3
2
|
export type { ProviderConfig } from "./ConfigBuilder";
|
|
4
3
|
export interface ProviderFactoryOptions {
|
|
@@ -9,15 +8,10 @@ export interface ProviderFactoryOptions {
|
|
|
9
8
|
}
|
|
10
9
|
declare class ProviderFactory {
|
|
11
10
|
private currentConfig;
|
|
12
|
-
private builder;
|
|
13
11
|
/**
|
|
14
12
|
* Initialize provider with configuration
|
|
15
13
|
*/
|
|
16
14
|
initialize(options: ProviderFactoryOptions): void;
|
|
17
|
-
/**
|
|
18
|
-
* Get current configuration
|
|
19
|
-
*/
|
|
20
|
-
getConfig(): ProviderConfig | null;
|
|
21
15
|
/**
|
|
22
16
|
* Check if provider is initialized
|
|
23
17
|
*/
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { createGeminiError } from "../utils/error-mapper.util";
|
|
|
4
4
|
import { GeminiError } from "../../domain/entities";
|
|
5
5
|
import type { GeminiContent, GeminiGenerationConfig } from "../../domain/entities";
|
|
6
6
|
import type { GenerativeModel } from "@google/generative-ai";
|
|
7
|
+
import type { Part } from "@google/generative-ai";
|
|
7
8
|
|
|
8
9
|
export interface BaseRequestOptions {
|
|
9
10
|
model: string;
|
|
@@ -15,7 +16,7 @@ export interface BaseRequestOptions {
|
|
|
15
16
|
export abstract class BaseGeminiService {
|
|
16
17
|
protected validateAndPrepare(options: BaseRequestOptions): {
|
|
17
18
|
genModel: GenerativeModel;
|
|
18
|
-
sdkContents: Array<{ role: string; parts:
|
|
19
|
+
sdkContents: Array<{ role: string; parts: Part[] }>;
|
|
19
20
|
} {
|
|
20
21
|
if (!options.contents || options.contents.length === 0) {
|
|
21
22
|
throw new Error("Contents array cannot be empty");
|
|
@@ -44,7 +45,7 @@ export abstract class BaseGeminiService {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
protected createRequestOptions(
|
|
47
|
-
sdkContents: Array<{ role: string; parts:
|
|
48
|
+
sdkContents: Array<{ role: string; parts: Part[] }>,
|
|
48
49
|
generationConfig?: GeminiGenerationConfig
|
|
49
50
|
) {
|
|
50
51
|
return { contents: sdkContents, generationConfig };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ChatSession as SdkChatSession, Part } from "@google/generative-ai";
|
|
2
2
|
import { geminiClient } from "./GeminiClient";
|
|
3
3
|
import { DEFAULT_MODELS } from "../../domain/entities";
|
|
4
|
+
import { trimArrayByCharBudget } from "../utils/text-calculations.util";
|
|
4
5
|
import type {
|
|
5
6
|
GeminiChatConfig,
|
|
6
7
|
GeminiGenerationConfig,
|
|
@@ -45,41 +46,6 @@ export interface SendChatMessageOptions {
|
|
|
45
46
|
historyMinMessages?: number;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
// ─── MIME Utilities ──────────────────────────────────────────────────────────
|
|
49
|
-
|
|
50
|
-
const AUDIO_MIME: Record<string, string> = {
|
|
51
|
-
mp3: "audio/mpeg",
|
|
52
|
-
wav: "audio/wav",
|
|
53
|
-
ogg: "audio/ogg",
|
|
54
|
-
flac: "audio/flac",
|
|
55
|
-
aac: "audio/aac",
|
|
56
|
-
mp4: "audio/mp4",
|
|
57
|
-
m4a: "audio/mp4",
|
|
58
|
-
caf: "audio/mp4",
|
|
59
|
-
"3gp": "audio/3gpp",
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
/** Resolve MIME type for an audio file extension */
|
|
63
|
-
export function resolveAudioMimeType(extension: string): string {
|
|
64
|
-
return AUDIO_MIME[extension.toLowerCase()] ?? "audio/mp4";
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const IMAGE_MIME: Record<string, string> = {
|
|
68
|
-
png: "image/png",
|
|
69
|
-
jpg: "image/jpeg",
|
|
70
|
-
jpeg: "image/jpeg",
|
|
71
|
-
webp: "image/webp",
|
|
72
|
-
gif: "image/gif",
|
|
73
|
-
heic: "image/heic",
|
|
74
|
-
heif: "image/heif",
|
|
75
|
-
bmp: "image/bmp",
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
/** Resolve MIME type for an image file extension */
|
|
79
|
-
export function resolveImageMimeType(extension: string): string {
|
|
80
|
-
return IMAGE_MIME[extension.toLowerCase()] ?? "image/jpeg";
|
|
81
|
-
}
|
|
82
|
-
|
|
83
49
|
// ─── History Utilities ───────────────────────────────────────────────────────
|
|
84
50
|
|
|
85
51
|
/**
|
|
@@ -102,10 +68,8 @@ export function buildChatHistory(
|
|
|
102
68
|
|
|
103
69
|
if (last && last.role === role) {
|
|
104
70
|
// Merge by extracting all text from existing parts and appending new content
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
.filter(Boolean)
|
|
108
|
-
.join("");
|
|
71
|
+
// Optimized: reduce instead of map + filter + join
|
|
72
|
+
const existingText = last.parts.reduce((acc, p) => acc + ("text" in p ? p.text : ""), "");
|
|
109
73
|
last.parts = [{ text: existingText + "\n" + m.content }];
|
|
110
74
|
} else {
|
|
111
75
|
result.push({ role, parts: [{ text: m.content }] });
|
|
@@ -124,19 +88,12 @@ export function trimChatHistory(
|
|
|
124
88
|
maxChars = 12000,
|
|
125
89
|
minMessages = 4,
|
|
126
90
|
): ChatHistoryMessage[] {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const chars = history[i].content.length;
|
|
134
|
-
if (trimmed.length >= minMessages && totalChars + chars > maxChars) break;
|
|
135
|
-
trimmed.unshift(history[i]);
|
|
136
|
-
totalChars += chars;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return trimmed;
|
|
91
|
+
return trimArrayByCharBudget(
|
|
92
|
+
history,
|
|
93
|
+
(m) => m.content.length,
|
|
94
|
+
maxChars,
|
|
95
|
+
minMessages,
|
|
96
|
+
);
|
|
140
97
|
}
|
|
141
98
|
|
|
142
99
|
// ─── Low-level: createChatSession ────────────────────────────────────────────
|
|
@@ -164,15 +121,6 @@ export function createChatSession(config: GeminiChatConfig = {}) {
|
|
|
164
121
|
|
|
165
122
|
return {
|
|
166
123
|
async send(parts: GeminiMessagePart[]): Promise<ChatSendResult> {
|
|
167
|
-
if (__DEV__) {
|
|
168
|
-
console.log("[ChatSession.send] >>> SENDING TO GEMINI SDK");
|
|
169
|
-
console.log("[ChatSession.send] parts count:", parts.length);
|
|
170
|
-
parts.forEach((p, i) => {
|
|
171
|
-
if ("text" in p) console.log(`[ChatSession.send] part[${i}] text:`, (p.text ?? "").substring(0, 200));
|
|
172
|
-
if ("inlineData" in p) console.log(`[ChatSession.send] part[${i}] inlineData: mime=${(p as GeminiInlineDataPart).inlineData.mimeType}, size=${(p as GeminiInlineDataPart).inlineData.data.length}`);
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
124
|
const result = await chat.sendMessage(parts as Part[]);
|
|
177
125
|
if (!result.response) throw new Error("No response from Gemini SDK");
|
|
178
126
|
const candidate = result.response.candidates?.[0];
|
|
@@ -188,15 +136,6 @@ export function createChatSession(config: GeminiChatConfig = {}) {
|
|
|
188
136
|
throw new Error("No text content in response");
|
|
189
137
|
}
|
|
190
138
|
|
|
191
|
-
if (__DEV__) {
|
|
192
|
-
console.log("[ChatSession.send] <<< GEMINI SDK RESPONSE");
|
|
193
|
-
console.log("[ChatSession.send] finishReason:", candidate?.finishReason ?? "N/A");
|
|
194
|
-
console.log("[ChatSession.send] safetyRatings:", JSON.stringify(candidate?.safetyRatings ?? []));
|
|
195
|
-
console.log("[ChatSession.send] response text length:", text.length);
|
|
196
|
-
console.log("[ChatSession.send] response text:", text.substring(0, 500));
|
|
197
|
-
console.log("[ChatSession.send] response FULL text:", text);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
139
|
return {
|
|
201
140
|
text,
|
|
202
141
|
finishReason: candidate?.finishReason,
|
|
@@ -228,40 +167,14 @@ export async function sendChatMessage(
|
|
|
228
167
|
throw new Error("Message cannot be empty");
|
|
229
168
|
}
|
|
230
169
|
|
|
231
|
-
if (__DEV__) {
|
|
232
|
-
console.log("═══════════════════════════════════════════════════");
|
|
233
|
-
console.log("[sendChatMessage] >>> START");
|
|
234
|
-
console.log("[sendChatMessage] model:", opts.model ?? DEFAULT_MODELS.CHAT);
|
|
235
|
-
console.log("[sendChatMessage] message:", opts.message.substring(0, 200));
|
|
236
|
-
console.log("[sendChatMessage] history count:", opts.history.length);
|
|
237
|
-
console.log("[sendChatMessage] systemPrompt length:", opts.systemPrompt?.length ?? 0);
|
|
238
|
-
console.log("[sendChatMessage] systemPrompt (first 500):", opts.systemPrompt?.substring(0, 500));
|
|
239
|
-
console.log("[sendChatMessage] generationConfig:", JSON.stringify(opts.generationConfig));
|
|
240
|
-
console.log("[sendChatMessage] safetySettings:", opts.safetySettings ? JSON.stringify(opts.safetySettings) : "USING DEFAULT (BLOCK_NONE)");
|
|
241
|
-
console.log("[sendChatMessage] attachments:", opts.attachments?.length ?? 0);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
170
|
const trimmed = trimChatHistory(
|
|
245
171
|
opts.history,
|
|
246
172
|
opts.historyMaxChars,
|
|
247
173
|
opts.historyMinMessages,
|
|
248
174
|
);
|
|
249
175
|
|
|
250
|
-
if (__DEV__) {
|
|
251
|
-
console.log("[sendChatMessage] history after trim:", trimmed.length, "messages");
|
|
252
|
-
trimmed.forEach((m, i) => console.log(`[sendChatMessage] history[${i}]: role=${m.role}, content=${m.content.substring(0, 100)}`));
|
|
253
|
-
}
|
|
254
|
-
|
|
255
176
|
const geminiHistory = buildChatHistory(trimmed);
|
|
256
177
|
|
|
257
|
-
if (__DEV__) {
|
|
258
|
-
console.log("[sendChatMessage] geminiHistory (SDK format):", geminiHistory.length, "turns");
|
|
259
|
-
geminiHistory.forEach((h, i) => {
|
|
260
|
-
const text = "text" in h.parts[0] ? (h.parts[0] as { text: string }).text : "[non-text]";
|
|
261
|
-
console.log(`[sendChatMessage] geminiHistory[${i}]: role=${h.role}, text=${text.substring(0, 100)}`);
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
|
|
265
178
|
const session = createChatSession({
|
|
266
179
|
model: opts.model ?? DEFAULT_MODELS.CHAT,
|
|
267
180
|
systemInstruction: opts.systemPrompt,
|
|
@@ -277,16 +190,7 @@ export async function sendChatMessage(
|
|
|
277
190
|
|
|
278
191
|
const result = await session.send(parts);
|
|
279
192
|
|
|
280
|
-
if (__DEV__) {
|
|
281
|
-
console.log("[sendChatMessage] finishReason:", result.finishReason);
|
|
282
|
-
console.log("[sendChatMessage] response text length:", result.text.length);
|
|
283
|
-
console.log("[sendChatMessage] response FULL:", result.text);
|
|
284
|
-
console.log("[sendChatMessage] <<< END");
|
|
285
|
-
console.log("═══════════════════════════════════════════════════");
|
|
286
|
-
}
|
|
287
|
-
|
|
288
193
|
if (result.finishReason === "SAFETY") {
|
|
289
|
-
if (__DEV__) console.warn("[sendChatMessage] ⚠️ SAFETY FILTER TRIGGERED but has text, returning partial");
|
|
290
194
|
if (result.text.trim()) return result.text;
|
|
291
195
|
throw new Error("Response blocked by safety filter.");
|
|
292
196
|
}
|
|
@@ -26,28 +26,23 @@ class GeminiClient {
|
|
|
26
26
|
private initialized = false;
|
|
27
27
|
|
|
28
28
|
initialize(config: GeminiConfig): void {
|
|
29
|
-
|
|
29
|
+
const apiKey = config.apiKey?.trim();
|
|
30
|
+
|
|
31
|
+
if (!apiKey || apiKey.length < 10) {
|
|
30
32
|
throw new Error("API key is required and must be at least 10 characters");
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
// Basic format validation for Google AI API keys (starts with "AIza")
|
|
36
|
+
if (!apiKey.startsWith("AIza")) {
|
|
37
|
+
throw new Error('Invalid API key format. Google AI API keys should start with "AIza"');
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
// Allow re-initialization with new config (e.g. API key change)
|
|
34
|
-
this.client = new GoogleGenerativeAI(
|
|
35
|
-
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
41
|
+
this.client = new GoogleGenerativeAI(apiKey);
|
|
42
|
+
this.config = { ...DEFAULT_CONFIG, ...config, apiKey };
|
|
36
43
|
this.initialized = true;
|
|
37
44
|
}
|
|
38
45
|
|
|
39
|
-
isInitialized(): boolean {
|
|
40
|
-
return this.initialized;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
getConfig(): GeminiConfig | null {
|
|
44
|
-
return this.config;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
getClient(): GoogleGenerativeAI | null {
|
|
48
|
-
return this.client;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
46
|
/**
|
|
52
47
|
* Returns a GenerativeModel configured with optional safety settings and system instruction.
|
|
53
48
|
* When no safety settings are provided, defaults to BLOCK_NONE for all categories.
|
|
@@ -71,18 +66,35 @@ class GeminiClient {
|
|
|
71
66
|
|
|
72
67
|
// Map package safety settings to SDK format
|
|
73
68
|
const sdkSafety: SafetySetting[] = opts.safetySettings
|
|
74
|
-
? opts.safetySettings.map((s) =>
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
69
|
+
? opts.safetySettings.map((s) => {
|
|
70
|
+
// Validate safety settings to prevent runtime errors
|
|
71
|
+
const validCategories = [
|
|
72
|
+
HarmCategory.HARM_CATEGORY_HARASSMENT,
|
|
73
|
+
HarmCategory.HARM_CATEGORY_HATE_SPEECH,
|
|
74
|
+
HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
|
75
|
+
HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
const validThresholds = [
|
|
79
|
+
HarmBlockThreshold.BLOCK_NONE,
|
|
80
|
+
HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
|
|
81
|
+
HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
|
|
82
|
+
HarmBlockThreshold.BLOCK_ONLY_HIGH,
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
// Check if category and threshold are valid enum values
|
|
86
|
+
const category = validCategories.includes(s.category as unknown as HarmCategory)
|
|
87
|
+
? (s.category as unknown as HarmCategory)
|
|
88
|
+
: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT; // Fallback to safest
|
|
89
|
+
|
|
90
|
+
const threshold = validThresholds.includes(s.threshold as unknown as HarmBlockThreshold)
|
|
91
|
+
? (s.threshold as unknown as HarmBlockThreshold)
|
|
92
|
+
: HarmBlockThreshold.BLOCK_NONE; // Fallback to most permissive
|
|
93
|
+
|
|
94
|
+
return { category, threshold };
|
|
95
|
+
})
|
|
78
96
|
: PERMISSIVE_SAFETY;
|
|
79
97
|
|
|
80
|
-
if (__DEV__) {
|
|
81
|
-
console.log("[GeminiClient.getModel] model:", effectiveModel);
|
|
82
|
-
console.log("[GeminiClient.getModel] systemInstruction length:", opts.systemInstruction?.length ?? 0);
|
|
83
|
-
console.log("[GeminiClient.getModel] safetySettings:", JSON.stringify(sdkSafety.map(s => ({ category: s.category, threshold: s.threshold }))));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
98
|
return this.client.getGenerativeModel({
|
|
87
99
|
model: effectiveModel,
|
|
88
100
|
...(opts.systemInstruction && { systemInstruction: opts.systemInstruction }),
|
|
@@ -5,7 +5,6 @@ import type { GenerationConfig } from "@google/generative-ai";
|
|
|
5
5
|
import type {
|
|
6
6
|
GeminiContent,
|
|
7
7
|
GeminiGenerationConfig,
|
|
8
|
-
GeminiResponse,
|
|
9
8
|
} from "../../domain/entities";
|
|
10
9
|
|
|
11
10
|
class StructuredTextService {
|
|
@@ -41,18 +40,12 @@ class StructuredTextService {
|
|
|
41
40
|
signal,
|
|
42
41
|
);
|
|
43
42
|
|
|
44
|
-
return this.parseJSONResponse<T>(response);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
private parseJSONResponse<T>(response: GeminiResponse): T {
|
|
48
43
|
const candidates = response.candidates;
|
|
49
|
-
|
|
50
44
|
if (!candidates || candidates.length === 0) {
|
|
51
45
|
throw new Error("No candidates in response");
|
|
52
46
|
}
|
|
53
47
|
|
|
54
48
|
const text = extractTextFromParts(candidates[0]?.content?.parts);
|
|
55
|
-
|
|
56
49
|
if (!text || text.trim().length === 0) {
|
|
57
50
|
throw new Error("Empty response received from Gemini");
|
|
58
51
|
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
|
|
2
2
|
export interface TelemetryEvent {
|
|
3
|
-
type: "
|
|
3
|
+
type: "error";
|
|
4
4
|
timestamp: number;
|
|
5
5
|
model?: string;
|
|
6
6
|
feature?: string;
|
|
7
|
-
duration?: number;
|
|
8
7
|
metadata?: Record<string, unknown>;
|
|
9
8
|
}
|
|
10
9
|
|
|
@@ -18,6 +17,15 @@ class TelemetryHooks {
|
|
|
18
17
|
|
|
19
18
|
/**
|
|
20
19
|
* Register a telemetry listener
|
|
20
|
+
*
|
|
21
|
+
* @returns Unsubscribe function - IMPORTANT: Call this when done listening to prevent memory leaks
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* const unsubscribe = telemetryHooks.subscribe((event) => console.log(event));
|
|
26
|
+
* // ... later when done
|
|
27
|
+
* unsubscribe(); // Prevents memory leak
|
|
28
|
+
* ```
|
|
21
29
|
*/
|
|
22
30
|
subscribe(listener: TelemetryListener): () => void {
|
|
23
31
|
this.listeners.push(listener);
|
|
@@ -69,35 +77,6 @@ class TelemetryHooks {
|
|
|
69
77
|
}
|
|
70
78
|
}
|
|
71
79
|
|
|
72
|
-
/**
|
|
73
|
-
* Log request start
|
|
74
|
-
*/
|
|
75
|
-
logRequest(model: string, feature?: string): number {
|
|
76
|
-
const timestamp = Date.now();
|
|
77
|
-
this.emit({
|
|
78
|
-
type: "request",
|
|
79
|
-
timestamp,
|
|
80
|
-
model,
|
|
81
|
-
feature,
|
|
82
|
-
});
|
|
83
|
-
return timestamp;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Log response received
|
|
88
|
-
*/
|
|
89
|
-
logResponse(model: string, startTime: number, feature?: string, metadata?: Record<string, unknown>): void {
|
|
90
|
-
const now = Date.now();
|
|
91
|
-
this.emit({
|
|
92
|
-
type: "response",
|
|
93
|
-
timestamp: now,
|
|
94
|
-
model,
|
|
95
|
-
feature,
|
|
96
|
-
duration: now - startTime,
|
|
97
|
-
metadata,
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
80
|
/**
|
|
102
81
|
* Log error
|
|
103
82
|
*/
|
|
@@ -114,19 +93,6 @@ class TelemetryHooks {
|
|
|
114
93
|
});
|
|
115
94
|
}
|
|
116
95
|
|
|
117
|
-
/**
|
|
118
|
-
* Log retry attempt
|
|
119
|
-
*/
|
|
120
|
-
logRetry(model: string, attempt: number, feature?: string): void {
|
|
121
|
-
this.emit({
|
|
122
|
-
type: "retry",
|
|
123
|
-
timestamp: Date.now(),
|
|
124
|
-
model,
|
|
125
|
-
feature,
|
|
126
|
-
metadata: { attempt },
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
96
|
/**
|
|
131
97
|
* Remove a specific listener
|
|
132
98
|
*/
|
|
@@ -139,32 +105,6 @@ class TelemetryHooks {
|
|
|
139
105
|
this.failedListeners.delete(listener);
|
|
140
106
|
this.listenerFailureCounts.delete(listener);
|
|
141
107
|
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Reset failure counts for all listeners (clear blacklist)
|
|
145
|
-
*/
|
|
146
|
-
resetFailures(): void {
|
|
147
|
-
this.failedListeners.clear();
|
|
148
|
-
for (const listener of this.listeners) {
|
|
149
|
-
this.listenerFailureCounts.set(listener, 0);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Clear all listeners
|
|
155
|
-
*/
|
|
156
|
-
clear(): void {
|
|
157
|
-
this.listeners = [];
|
|
158
|
-
this.failedListeners.clear();
|
|
159
|
-
this.listenerFailureCounts.clear();
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Get current listener count
|
|
164
|
-
*/
|
|
165
|
-
getListenerCount(): number {
|
|
166
|
-
return this.listeners.length;
|
|
167
|
-
}
|
|
168
108
|
}
|
|
169
109
|
|
|
170
110
|
export const telemetryHooks = new TelemetryHooks();
|
|
@@ -26,7 +26,6 @@ export interface AsyncStateSetters<T = string, U = unknown> {
|
|
|
26
26
|
*/
|
|
27
27
|
export interface AsyncStateConfig<T = string> {
|
|
28
28
|
resetState?: boolean;
|
|
29
|
-
throwOnError?: boolean;
|
|
30
29
|
transformResult?: (result: T) => T;
|
|
31
30
|
}
|
|
32
31
|
|
|
@@ -60,7 +59,6 @@ export async function executeWithState<T, U = unknown>(
|
|
|
60
59
|
): Promise<T | null> {
|
|
61
60
|
const {
|
|
62
61
|
resetState = true,
|
|
63
|
-
throwOnError = false,
|
|
64
62
|
transformResult,
|
|
65
63
|
} = config;
|
|
66
64
|
|
|
@@ -88,10 +86,6 @@ export async function executeWithState<T, U = unknown>(
|
|
|
88
86
|
setters.setError(errorMessage);
|
|
89
87
|
callbacks.onError?.(errorMessage);
|
|
90
88
|
|
|
91
|
-
if (throwOnError) {
|
|
92
|
-
throw err;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
89
|
return null;
|
|
96
90
|
} finally {
|
|
97
91
|
setters.setIsLoading(false);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Handles transformation between domain content and SDK format
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import type { Part } from "@google/generative-ai";
|
|
6
7
|
import type {
|
|
7
8
|
GeminiContent,
|
|
8
9
|
GeminiPart,
|
|
@@ -49,14 +50,21 @@ function isValidProbability(value: string): value is GeminiSafetyRating["probabi
|
|
|
49
50
|
|
|
50
51
|
/**
|
|
51
52
|
* Convert domain content to SDK format
|
|
53
|
+
* Preserves both text and inlineData parts
|
|
52
54
|
*/
|
|
53
55
|
export function toSdkContent(contents: GeminiContent[]): Array<{
|
|
54
56
|
role: string;
|
|
55
|
-
parts:
|
|
57
|
+
parts: Part[];
|
|
56
58
|
}> {
|
|
57
59
|
return contents.map((content) => ({
|
|
58
60
|
role: content.role || "user",
|
|
59
|
-
parts: content.parts.map((part) =>
|
|
61
|
+
parts: content.parts.map((part) => {
|
|
62
|
+
if ("text" in part) {
|
|
63
|
+
return { text: part.text } as Part;
|
|
64
|
+
}
|
|
65
|
+
// For inlineData and other part types, cast to SDK Part type
|
|
66
|
+
return part as Part;
|
|
67
|
+
}),
|
|
60
68
|
}));
|
|
61
69
|
}
|
|
62
70
|
|
|
@@ -21,12 +21,18 @@ export async function processStream(
|
|
|
21
21
|
let fullText = "";
|
|
22
22
|
|
|
23
23
|
for await (const chunk of stream) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
try {
|
|
25
|
+
// chunk.text() can throw critical errors (e.g. safety blocks mid-stream)
|
|
26
|
+
// and must propagate, unlike callback errors which are safe to swallow
|
|
27
|
+
const chunkText = chunk.text();
|
|
28
|
+
if (chunkText) {
|
|
29
|
+
fullText += chunkText;
|
|
30
|
+
safeCallChunk(onChunk, chunkText, onError);
|
|
31
|
+
}
|
|
32
|
+
} catch (chunkError) {
|
|
33
|
+
// Log chunk error but re-throw to properly fail the stream
|
|
34
|
+
logError(onError, chunkError, "stream-chunk");
|
|
35
|
+
throw chunkError;
|
|
30
36
|
}
|
|
31
37
|
}
|
|
32
38
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text Calculation Utilities
|
|
3
|
+
* Reusable functions for text length and character budget calculations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Calculate total character count in an array of text objects
|
|
8
|
+
* @param items - Array of objects with text content
|
|
9
|
+
* @param getText - Function to extract text from each item
|
|
10
|
+
* @returns Total character count
|
|
11
|
+
*/
|
|
12
|
+
export function calculateTotalChars<T>(
|
|
13
|
+
items: readonly T[],
|
|
14
|
+
getItemLength: (item: T) => number
|
|
15
|
+
): number {
|
|
16
|
+
return items.reduce((sum, item) => sum + getItemLength(item), 0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if adding an item would exceed the character budget
|
|
21
|
+
* @param currentTotal - Current total character count
|
|
22
|
+
* @param itemLength - Length of item to add
|
|
23
|
+
* @param maxBudget - Maximum allowed budget
|
|
24
|
+
* @returns true if within budget, false otherwise
|
|
25
|
+
*/
|
|
26
|
+
export function fitsWithinBudget(
|
|
27
|
+
currentTotal: number,
|
|
28
|
+
itemLength: number,
|
|
29
|
+
maxBudget: number
|
|
30
|
+
): boolean {
|
|
31
|
+
return currentTotal + itemLength <= maxBudget;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Trim array to fit within character budget while keeping minimum items
|
|
36
|
+
* Keeps the last `minItems` regardless of budget, then adds as many as possible from the end
|
|
37
|
+
*
|
|
38
|
+
* @param items - Array to trim
|
|
39
|
+
* @param getItemLength - Function to get length of each item
|
|
40
|
+
* @param maxBudget - Maximum total length allowed
|
|
41
|
+
* @param minItems - Minimum number of items to keep (from the end)
|
|
42
|
+
* @returns Trimmed array
|
|
43
|
+
*/
|
|
44
|
+
export function trimArrayByCharBudget<T>(
|
|
45
|
+
items: readonly T[],
|
|
46
|
+
getItemLength: (item: T) => number,
|
|
47
|
+
maxBudget: number,
|
|
48
|
+
minItems: number
|
|
49
|
+
): T[] {
|
|
50
|
+
if (items.length <= minItems) return [...items];
|
|
51
|
+
|
|
52
|
+
// First, guarantee minimum items (from the end)
|
|
53
|
+
const guaranteedMin = items.slice(-minItems);
|
|
54
|
+
const remaining = items.slice(0, -minItems);
|
|
55
|
+
|
|
56
|
+
// Calculate current total
|
|
57
|
+
let totalChars = calculateTotalChars(guaranteedMin, getItemLength);
|
|
58
|
+
const trimmed: T[] = [...guaranteedMin];
|
|
59
|
+
|
|
60
|
+
// Add older items in reverse order until budget is exceeded
|
|
61
|
+
for (let i = remaining.length - 1; i >= 0; i--) {
|
|
62
|
+
const itemLength = getItemLength(remaining[i]);
|
|
63
|
+
if (!fitsWithinBudget(totalChars, itemLength, maxBudget)) break;
|
|
64
|
+
|
|
65
|
+
trimmed.unshift(remaining[i]);
|
|
66
|
+
totalChars += itemLength;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return trimmed;
|
|
70
|
+
}
|
|
@@ -4,7 +4,6 @@ import { DEFAULT_MODELS } from "../../domain/entities";
|
|
|
4
4
|
import { textGeneration } from "../../infrastructure/services/TextGeneration";
|
|
5
5
|
import { structuredText } from "../../infrastructure/services/StructuredText";
|
|
6
6
|
import { executeWithState, type AsyncStateSetters } from "../../infrastructure/utils/async/execute-state.util";
|
|
7
|
-
import { parseJsonResponse } from "../../infrastructure/utils/json-parser.util";
|
|
8
7
|
import { useOperationManager } from "./useOperationManager";
|
|
9
8
|
|
|
10
9
|
export interface UseGeminiOptions {
|
|
@@ -16,7 +15,7 @@ export interface UseGeminiOptions {
|
|
|
16
15
|
|
|
17
16
|
export interface UseGeminiReturn {
|
|
18
17
|
generate: (prompt: string) => Promise<void>;
|
|
19
|
-
generateJSON: <T>(prompt: string, schema
|
|
18
|
+
generateJSON: <T>(prompt: string, schema: Record<string, unknown>) => Promise<T | null>;
|
|
20
19
|
result: string | null;
|
|
21
20
|
jsonResult: unknown;
|
|
22
21
|
isGenerating: boolean;
|
|
@@ -65,7 +64,6 @@ export function useGemini(options: UseGeminiOptions = {}): UseGeminiReturn {
|
|
|
65
64
|
},
|
|
66
65
|
(text: string) => {
|
|
67
66
|
setResult(text);
|
|
68
|
-
// onSuccess is called by executeWithState via callbacks - do not call here again
|
|
69
67
|
}
|
|
70
68
|
);
|
|
71
69
|
});
|
|
@@ -74,7 +72,7 @@ export function useGemini(options: UseGeminiOptions = {}): UseGeminiReturn {
|
|
|
74
72
|
);
|
|
75
73
|
|
|
76
74
|
const generateJSON = useCallback(
|
|
77
|
-
async <T>(prompt: string, schema
|
|
75
|
+
async <T>(prompt: string, schema: Record<string, unknown>): Promise<T | null> => {
|
|
78
76
|
return executeOperation(async (signal, _operationId) => {
|
|
79
77
|
const jsonSetters: AsyncStateSetters<unknown, unknown> = {
|
|
80
78
|
setIsLoading: setIsGenerating,
|
|
@@ -95,24 +93,13 @@ export function useGemini(options: UseGeminiOptions = {}): UseGeminiReturn {
|
|
|
95
93
|
jsonSetters,
|
|
96
94
|
jsonCallbacks,
|
|
97
95
|
async () => {
|
|
98
|
-
|
|
99
|
-
return structuredText.generateStructuredText<T>(
|
|
100
|
-
model,
|
|
101
|
-
prompt,
|
|
102
|
-
schema,
|
|
103
|
-
options.generationConfig,
|
|
104
|
-
signal
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const text = await textGeneration.generateText(
|
|
96
|
+
return structuredText.generateStructuredText<T>(
|
|
109
97
|
model,
|
|
110
98
|
prompt,
|
|
111
|
-
|
|
99
|
+
schema,
|
|
100
|
+
options.generationConfig,
|
|
112
101
|
signal
|
|
113
102
|
);
|
|
114
|
-
|
|
115
|
-
return parseJsonResponse<T>(text);
|
|
116
103
|
},
|
|
117
104
|
(parsed: unknown) => {
|
|
118
105
|
setJsonResult(parsed);
|
|
@@ -109,17 +109,4 @@ export class ConfigBuilder {
|
|
|
109
109
|
static create(): ConfigBuilder {
|
|
110
110
|
return new ConfigBuilder();
|
|
111
111
|
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Create from existing config (for updates)
|
|
115
|
-
*/
|
|
116
|
-
static from(config: Partial<ProviderConfig>): ConfigBuilder {
|
|
117
|
-
const builder = new ConfigBuilder();
|
|
118
|
-
if (config.apiKey) builder.withApiKey(config.apiKey);
|
|
119
|
-
if (config.textModel) builder.withTextModel(config.textModel);
|
|
120
|
-
// Apply strategy first (sets default timeout), then explicit timeout overrides it
|
|
121
|
-
if (config.strategy) builder.withStrategy(config.strategy);
|
|
122
|
-
if (config.timeout != null && config.timeout > 0) builder.withTimeout(config.timeout);
|
|
123
|
-
return builder;
|
|
124
|
-
}
|
|
125
112
|
}
|
|
@@ -14,42 +14,33 @@ export interface ProviderFactoryOptions {
|
|
|
14
14
|
|
|
15
15
|
class ProviderFactory {
|
|
16
16
|
private currentConfig: ProviderConfig | null = null;
|
|
17
|
-
private builder: ConfigBuilder | null = null;
|
|
18
17
|
|
|
19
18
|
/**
|
|
20
19
|
* Initialize provider with configuration
|
|
21
20
|
*/
|
|
22
21
|
initialize(options: ProviderFactoryOptions): void {
|
|
23
|
-
// Build configuration using builder pattern
|
|
24
|
-
|
|
25
|
-
.withApiKey(options.apiKey);
|
|
22
|
+
// Build configuration using builder pattern (inline, no state needed)
|
|
23
|
+
const builder = ConfigBuilder.create().withApiKey(options.apiKey);
|
|
26
24
|
|
|
27
25
|
if (options.strategy) {
|
|
28
|
-
|
|
26
|
+
builder.withStrategy(options.strategy);
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
if (options.textModel) {
|
|
32
|
-
|
|
30
|
+
builder.withTextModel(options.textModel);
|
|
33
31
|
}
|
|
34
32
|
|
|
35
33
|
if (options.timeout) {
|
|
36
|
-
|
|
34
|
+
builder.withTimeout(options.timeout);
|
|
37
35
|
}
|
|
38
36
|
|
|
39
|
-
this.currentConfig =
|
|
37
|
+
this.currentConfig = builder.build();
|
|
40
38
|
|
|
41
39
|
// Initialize Gemini client
|
|
42
|
-
const geminiConfig =
|
|
40
|
+
const geminiConfig = builder.toGeminiConfig();
|
|
43
41
|
geminiClient.initialize(geminiConfig);
|
|
44
42
|
}
|
|
45
43
|
|
|
46
|
-
/**
|
|
47
|
-
* Get current configuration
|
|
48
|
-
*/
|
|
49
|
-
getConfig(): ProviderConfig | null {
|
|
50
|
-
return this.currentConfig;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
44
|
/**
|
|
54
45
|
* Check if provider is initialized
|
|
55
46
|
*/
|
package/src/global.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
declare const __DEV__: boolean;
|