@umituz/react-native-ai-gemini-provider 2.1.13 → 3.0.1
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/domain/entities/models.ts +0 -8
- package/src/index.ts +6 -7
- package/src/infrastructure/services/{base-gemini.service.ts → BaseService.ts} +3 -34
- package/src/infrastructure/services/{gemini-client-core.service.ts → GeminiClient.ts} +10 -25
- package/src/infrastructure/services/GeminiProvider.ts +38 -0
- package/src/infrastructure/services/{gemini-streaming.service.ts → Streaming.ts} +14 -35
- package/src/infrastructure/services/{gemini-structured-text.service.ts → StructuredText.ts} +7 -14
- package/src/infrastructure/services/{gemini-text-generation.service.ts → TextGeneration.ts} +8 -23
- package/src/infrastructure/services/index.ts +5 -21
- package/src/infrastructure/telemetry/index.ts +0 -1
- package/src/infrastructure/utils/async/index.ts +0 -2
- package/src/infrastructure/utils/content-mapper.util.ts +1 -1
- package/src/infrastructure/utils/error-mapper.util.ts +1 -9
- package/src/infrastructure/utils/json-parser.util.ts +1 -50
- package/src/infrastructure/utils/stream-processor.util.ts +3 -90
- package/src/presentation/hooks/index.ts +1 -11
- package/src/presentation/hooks/{use-gemini.ts → useGemini.ts} +5 -5
- package/src/providers/ProviderFactory.ts +2 -2
- package/src/infrastructure/interceptors/BaseInterceptor.ts +0 -78
- package/src/infrastructure/interceptors/RequestInterceptors.ts +0 -30
- package/src/infrastructure/interceptors/ResponseInterceptors.ts +0 -35
- package/src/infrastructure/interceptors/index.ts +0 -12
- package/src/infrastructure/services/gemini-provider.ts +0 -62
- package/src/infrastructure/utils/index.ts +0 -47
- package/src/infrastructure/utils/validation-composer.util.ts +0 -160
- package/src/infrastructure/utils/validation.util.ts +0 -73
- /package/src/presentation/hooks/{use-operation-manager.ts → useOperationManager.ts} +0 -0
package/package.json
CHANGED
|
@@ -16,11 +16,3 @@ export const DEFAULT_MODELS = {
|
|
|
16
16
|
/** Default model for text generation */
|
|
17
17
|
TEXT: GEMINI_MODELS.TEXT.FLASH_LITE,
|
|
18
18
|
} as const;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Pricing information for Gemini models
|
|
22
|
-
* Prices are per 1M tokens (USD)
|
|
23
|
-
*/
|
|
24
|
-
export const MODEL_PRICING = {
|
|
25
|
-
[GEMINI_MODELS.TEXT.FLASH_LITE]: { input: 0.10, output: 0.40, freePerDay: 1000 },
|
|
26
|
-
} as const;
|
package/src/index.ts
CHANGED
|
@@ -27,13 +27,12 @@ export {
|
|
|
27
27
|
DEFAULT_MODELS
|
|
28
28
|
} from "./domain/entities";
|
|
29
29
|
|
|
30
|
-
//
|
|
31
|
-
export {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
} from "./infrastructure/services";
|
|
35
|
-
|
|
36
|
-
export type { GeminiProviderConfig } from "./infrastructure/services";
|
|
30
|
+
// Services
|
|
31
|
+
export { geminiClient } from "./infrastructure/services/GeminiClient";
|
|
32
|
+
export { textGeneration } from "./infrastructure/services/TextGeneration";
|
|
33
|
+
export { structuredText } from "./infrastructure/services/StructuredText";
|
|
34
|
+
export { streaming } from "./infrastructure/services/Streaming";
|
|
35
|
+
export { geminiProvider, GeminiProvider } from "./infrastructure/services/GeminiProvider";
|
|
37
36
|
|
|
38
37
|
// React Hook
|
|
39
38
|
export { useGemini } from "./presentation/hooks";
|
|
@@ -1,17 +1,9 @@
|
|
|
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";
|
|
1
|
+
import { geminiClient } from "./GeminiClient";
|
|
7
2
|
import { toSdkContent } from "../utils/content-mapper.util";
|
|
8
3
|
import { createGeminiError } from "../utils/error-mapper.util";
|
|
9
4
|
import type { GeminiContent, GeminiGenerationConfig } from "../../domain/entities";
|
|
10
5
|
import type { GenerativeModel } from "@google/generative-ai";
|
|
11
6
|
|
|
12
|
-
/**
|
|
13
|
-
* Base request options structure
|
|
14
|
-
*/
|
|
15
7
|
export interface BaseRequestOptions {
|
|
16
8
|
model: string;
|
|
17
9
|
contents: GeminiContent[];
|
|
@@ -19,64 +11,41 @@ export interface BaseRequestOptions {
|
|
|
19
11
|
signal?: AbortSignal;
|
|
20
12
|
}
|
|
21
13
|
|
|
22
|
-
/**
|
|
23
|
-
* Abstract base service with common patterns
|
|
24
|
-
*/
|
|
25
14
|
export abstract class BaseGeminiService {
|
|
26
|
-
/**
|
|
27
|
-
* Validate and prepare request
|
|
28
|
-
* Eliminates duplicate validation and abort checks
|
|
29
|
-
*/
|
|
30
15
|
protected validateAndPrepare(options: BaseRequestOptions): {
|
|
31
16
|
genModel: GenerativeModel;
|
|
32
17
|
sdkContents: Array<{ role: string; parts: Array<{ text: string }> }>;
|
|
33
18
|
} {
|
|
34
|
-
// Validate contents
|
|
35
19
|
if (!options.contents || options.contents.length === 0) {
|
|
36
20
|
throw new Error("Contents array cannot be empty");
|
|
37
21
|
}
|
|
38
22
|
|
|
39
|
-
// Check for early abort
|
|
40
23
|
if (options.signal?.aborted) {
|
|
41
24
|
throw new Error("Request was aborted");
|
|
42
25
|
}
|
|
43
26
|
|
|
44
|
-
const genModel =
|
|
27
|
+
const genModel = geminiClient.getModel(options.model);
|
|
45
28
|
const sdkContents = toSdkContent(options.contents);
|
|
46
29
|
|
|
47
30
|
return { genModel, sdkContents };
|
|
48
31
|
}
|
|
49
32
|
|
|
50
|
-
/**
|
|
51
|
-
* Handle errors uniformly across all services
|
|
52
|
-
* Eliminates duplicate error handling logic
|
|
53
|
-
*/
|
|
54
33
|
protected handleError(error: unknown, abortMessage: string): never {
|
|
55
|
-
// Re-throw as GeminiError if it's already an API error
|
|
56
34
|
if (error instanceof Error && error.name === "GeminiError") {
|
|
57
35
|
throw error;
|
|
58
36
|
}
|
|
59
37
|
|
|
60
|
-
// Check for abort error
|
|
61
38
|
if (error instanceof Error && error.name === "AbortError") {
|
|
62
39
|
throw new Error(abortMessage);
|
|
63
40
|
}
|
|
64
41
|
|
|
65
|
-
// Wrap other errors
|
|
66
42
|
throw createGeminiError(error);
|
|
67
43
|
}
|
|
68
44
|
|
|
69
|
-
/**
|
|
70
|
-
* Create typed request options for SDK
|
|
71
|
-
* Type-safe request creation
|
|
72
|
-
*/
|
|
73
45
|
protected createRequestOptions(
|
|
74
46
|
sdkContents: Array<{ role: string; parts: Array<{ text: string }> }>,
|
|
75
47
|
generationConfig?: GeminiGenerationConfig
|
|
76
48
|
) {
|
|
77
|
-
return {
|
|
78
|
-
contents: sdkContents,
|
|
79
|
-
generationConfig,
|
|
80
|
-
};
|
|
49
|
+
return { contents: sdkContents, generationConfig };
|
|
81
50
|
}
|
|
82
51
|
}
|
|
@@ -1,29 +1,22 @@
|
|
|
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";
|
|
5
4
|
|
|
6
5
|
const DEFAULT_CONFIG: Partial<GeminiConfig> = {
|
|
7
6
|
textModel: DEFAULT_MODELS.TEXT,
|
|
8
7
|
};
|
|
9
8
|
|
|
10
|
-
class
|
|
9
|
+
class GeminiClient {
|
|
11
10
|
private client: GoogleGenerativeAI | null = null;
|
|
12
11
|
private config: GeminiConfig | null = null;
|
|
13
12
|
private initialized = false;
|
|
14
13
|
|
|
15
|
-
/**
|
|
16
|
-
* Initialize the Gemini client with configuration
|
|
17
|
-
*
|
|
18
|
-
* @throws {Error} If already initialized or API key is invalid
|
|
19
|
-
*/
|
|
20
14
|
initialize(config: GeminiConfig): void {
|
|
21
|
-
if (this.initialized)
|
|
22
|
-
throw new Error("Gemini client already initialized. Call reset() before re-initializing with new config.");
|
|
23
|
-
}
|
|
15
|
+
if (this.initialized) return;
|
|
24
16
|
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
if (!config.apiKey || config.apiKey.trim().length < 10) {
|
|
18
|
+
throw new Error("API key is required and must be at least 10 characters");
|
|
19
|
+
}
|
|
27
20
|
|
|
28
21
|
this.client = new GoogleGenerativeAI(config.apiKey);
|
|
29
22
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
@@ -42,24 +35,16 @@ class GeminiClientCoreService {
|
|
|
42
35
|
return this.client;
|
|
43
36
|
}
|
|
44
37
|
|
|
45
|
-
|
|
38
|
+
getModel(modelName?: string): GenerativeModel {
|
|
46
39
|
if (!this.client || !this.initialized) {
|
|
47
40
|
throw new Error("Gemini client not initialized. Call initialize() first.");
|
|
48
41
|
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
getModel(modelName?: string): GenerativeModel {
|
|
53
|
-
this.validateInitialization();
|
|
54
|
-
|
|
55
|
-
if (!this.client) {
|
|
56
|
-
throw new Error("Gemini client not available");
|
|
57
|
-
}
|
|
58
42
|
|
|
59
43
|
const effectiveModel = modelName || this.config?.textModel || DEFAULT_MODELS.TEXT;
|
|
60
44
|
|
|
61
|
-
|
|
62
|
-
|
|
45
|
+
if (!effectiveModel.startsWith("gemini-")) {
|
|
46
|
+
throw new Error('Model name must start with "gemini-"');
|
|
47
|
+
}
|
|
63
48
|
|
|
64
49
|
return this.client.getGenerativeModel({ model: effectiveModel });
|
|
65
50
|
}
|
|
@@ -71,4 +56,4 @@ class GeminiClientCoreService {
|
|
|
71
56
|
}
|
|
72
57
|
}
|
|
73
58
|
|
|
74
|
-
export const
|
|
59
|
+
export const geminiClient = new GeminiClient();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { GeminiConfig } from "../../domain/entities";
|
|
2
|
+
import { geminiClient } from "./GeminiClient";
|
|
3
|
+
import { structuredText } from "./StructuredText";
|
|
4
|
+
|
|
5
|
+
export class GeminiProvider {
|
|
6
|
+
readonly providerId = "gemini";
|
|
7
|
+
readonly providerName = "Google Gemini";
|
|
8
|
+
|
|
9
|
+
initialize(config: GeminiConfig): void {
|
|
10
|
+
geminiClient.initialize(config);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
isInitialized(): boolean {
|
|
14
|
+
return geminiClient.isInitialized();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
reset(): void {
|
|
18
|
+
geminiClient.reset();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async generateStructuredText<T>(
|
|
22
|
+
prompt: string,
|
|
23
|
+
schema: Record<string, unknown>,
|
|
24
|
+
model: string,
|
|
25
|
+
): Promise<T> {
|
|
26
|
+
if (!prompt || prompt.trim().length < 3) {
|
|
27
|
+
throw new Error("Prompt must be at least 3 characters");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!this.isInitialized()) {
|
|
31
|
+
throw new Error("Provider not initialized. Call initialize() first.");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return structuredText.generateStructuredText<T>(model, prompt, schema);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const geminiProvider = new GeminiProvider();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseGeminiService } from "./
|
|
1
|
+
import { BaseGeminiService } from "./BaseService";
|
|
2
2
|
import { telemetryHooks } from "../telemetry";
|
|
3
3
|
import { processStream } from "../utils/stream-processor.util";
|
|
4
4
|
import type {
|
|
@@ -6,22 +6,7 @@ import type {
|
|
|
6
6
|
GeminiGenerationConfig,
|
|
7
7
|
} from "../../domain/entities";
|
|
8
8
|
|
|
9
|
-
class
|
|
10
|
-
/**
|
|
11
|
-
* Stream content generation
|
|
12
|
-
*
|
|
13
|
-
* @throws {GeminiError} For API-specific errors
|
|
14
|
-
* @throws {Error} For validation or network errors
|
|
15
|
-
*
|
|
16
|
-
* @example
|
|
17
|
-
* ```ts
|
|
18
|
-
* const fullText = await streamContent(
|
|
19
|
-
* "gemini-2.5-flash-lite",
|
|
20
|
-
* [{ parts: [{ text: "Hello" }], role: "user" }],
|
|
21
|
-
* (chunk) => console.log(chunk)
|
|
22
|
-
* );
|
|
23
|
-
* ```
|
|
24
|
-
*/
|
|
9
|
+
class StreamingService extends BaseGeminiService {
|
|
25
10
|
async streamContent(
|
|
26
11
|
model: string,
|
|
27
12
|
contents: GeminiContent[],
|
|
@@ -29,7 +14,6 @@ class GeminiStreamingService extends BaseGeminiService {
|
|
|
29
14
|
generationConfig?: GeminiGenerationConfig,
|
|
30
15
|
signal?: AbortSignal,
|
|
31
16
|
): Promise<string> {
|
|
32
|
-
// Validate callback
|
|
33
17
|
if (typeof onChunk !== "function") {
|
|
34
18
|
throw new Error("onChunk must be a function");
|
|
35
19
|
}
|
|
@@ -51,27 +35,22 @@ class GeminiStreamingService extends BaseGeminiService {
|
|
|
51
35
|
return await processStream(
|
|
52
36
|
result.stream,
|
|
53
37
|
onChunk,
|
|
54
|
-
(error, context) =>
|
|
38
|
+
(error, context) => {
|
|
39
|
+
try {
|
|
40
|
+
telemetryHooks.logError(
|
|
41
|
+
model,
|
|
42
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
43
|
+
context
|
|
44
|
+
);
|
|
45
|
+
} catch {
|
|
46
|
+
// Silently ignore telemetry errors
|
|
47
|
+
}
|
|
48
|
+
}
|
|
55
49
|
);
|
|
56
50
|
} catch (error) {
|
|
57
51
|
return this.handleError(error, "Stream generation was aborted");
|
|
58
52
|
}
|
|
59
53
|
}
|
|
60
|
-
|
|
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
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
54
|
}
|
|
76
55
|
|
|
77
|
-
export const
|
|
56
|
+
export const streaming = new StreamingService();
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
import { geminiTextGenerationService } from "./gemini-text-generation.service";
|
|
1
|
+
import { textGeneration } from "./TextGeneration";
|
|
3
2
|
import { parseJsonResponse } from "../utils/json-parser.util";
|
|
4
3
|
import { extractTextFromParts } from "../utils/content-mapper.util";
|
|
5
|
-
import { validateSchema } from "../utils/validation.util";
|
|
6
4
|
import type { GenerationConfig } from "@google/generative-ai";
|
|
7
5
|
import type {
|
|
8
6
|
GeminiContent,
|
|
@@ -10,11 +8,7 @@ import type {
|
|
|
10
8
|
GeminiResponse,
|
|
11
9
|
} from "../../domain/entities";
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
class GeminiStructuredTextService {
|
|
15
|
-
/**
|
|
16
|
-
* Generate structured JSON response with schema
|
|
17
|
-
*/
|
|
11
|
+
class StructuredTextService {
|
|
18
12
|
async generateStructuredText<T>(
|
|
19
13
|
model: string,
|
|
20
14
|
prompt: string,
|
|
@@ -22,7 +16,9 @@ class GeminiStructuredTextService {
|
|
|
22
16
|
config?: Omit<GeminiGenerationConfig, "responseMimeType" | "responseSchema">,
|
|
23
17
|
signal?: AbortSignal,
|
|
24
18
|
): Promise<T> {
|
|
25
|
-
|
|
19
|
+
if (!schema || typeof schema !== "object" || Object.keys(schema).length === 0) {
|
|
20
|
+
throw new Error("Schema must be a non-empty object");
|
|
21
|
+
}
|
|
26
22
|
|
|
27
23
|
const generationConfig: GeminiGenerationConfig = {
|
|
28
24
|
...config,
|
|
@@ -34,7 +30,7 @@ class GeminiStructuredTextService {
|
|
|
34
30
|
{ parts: [{ text: prompt }], role: "user" },
|
|
35
31
|
];
|
|
36
32
|
|
|
37
|
-
const response = await
|
|
33
|
+
const response = await textGeneration.generateContent(
|
|
38
34
|
model,
|
|
39
35
|
contents,
|
|
40
36
|
generationConfig,
|
|
@@ -44,9 +40,6 @@ class GeminiStructuredTextService {
|
|
|
44
40
|
return this.parseJSONResponse<T>(response);
|
|
45
41
|
}
|
|
46
42
|
|
|
47
|
-
/**
|
|
48
|
-
* Parse JSON response from Gemini
|
|
49
|
-
*/
|
|
50
43
|
private parseJSONResponse<T>(response: GeminiResponse): T {
|
|
51
44
|
const candidates = response.candidates;
|
|
52
45
|
|
|
@@ -64,4 +57,4 @@ class GeminiStructuredTextService {
|
|
|
64
57
|
}
|
|
65
58
|
}
|
|
66
59
|
|
|
67
|
-
export const
|
|
60
|
+
export const structuredText = new StructuredTextService();
|
|
@@ -1,20 +1,13 @@
|
|
|
1
|
-
import { BaseGeminiService } from "./
|
|
1
|
+
import { BaseGeminiService } from "./BaseService";
|
|
2
2
|
import { extractTextFromResponse } from "../utils/gemini-data-transformer.util";
|
|
3
3
|
import { transformResponse, createTextContent } from "../utils/content-mapper.util";
|
|
4
|
-
import { validatePrompt } from "../utils/validation.util";
|
|
5
4
|
import type {
|
|
6
5
|
GeminiContent,
|
|
7
6
|
GeminiGenerationConfig,
|
|
8
7
|
GeminiResponse,
|
|
9
8
|
} from "../../domain/entities";
|
|
10
9
|
|
|
11
|
-
class
|
|
12
|
-
/**
|
|
13
|
-
* Generate content (text, with optional images)
|
|
14
|
-
*
|
|
15
|
-
* @throws {GeminiError} For API-specific errors
|
|
16
|
-
* @throws {Error} For validation or network errors
|
|
17
|
-
*/
|
|
10
|
+
class TextGenerationService extends BaseGeminiService {
|
|
18
11
|
async generateContent(
|
|
19
12
|
model: string,
|
|
20
13
|
contents: GeminiContent[],
|
|
@@ -35,38 +28,30 @@ class GeminiTextGenerationService extends BaseGeminiService {
|
|
|
35
28
|
? await genModel.generateContent(requestOptions, { signal })
|
|
36
29
|
: await genModel.generateContent(requestOptions);
|
|
37
30
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (!response) {
|
|
31
|
+
if (!result.response) {
|
|
41
32
|
throw new Error("No response received from Gemini API");
|
|
42
33
|
}
|
|
43
34
|
|
|
44
|
-
return transformResponse(response);
|
|
35
|
+
return transformResponse(result.response);
|
|
45
36
|
} catch (error) {
|
|
46
37
|
return this.handleError(error, "Request was aborted");
|
|
47
38
|
}
|
|
48
39
|
}
|
|
49
40
|
|
|
50
|
-
/**
|
|
51
|
-
* Generate text from prompt
|
|
52
|
-
*
|
|
53
|
-
* @throws {GeminiError} For API-specific errors
|
|
54
|
-
* @throws {Error} For validation or network errors
|
|
55
|
-
*/
|
|
56
41
|
async generateText(
|
|
57
42
|
model: string,
|
|
58
43
|
prompt: string,
|
|
59
44
|
config?: GeminiGenerationConfig,
|
|
60
45
|
signal?: AbortSignal,
|
|
61
46
|
): Promise<string> {
|
|
62
|
-
|
|
63
|
-
|
|
47
|
+
if (!prompt || prompt.trim().length < 3) {
|
|
48
|
+
throw new Error("Prompt must be at least 3 characters");
|
|
49
|
+
}
|
|
64
50
|
|
|
65
51
|
const contents: GeminiContent[] = [createTextContent(prompt, "user")];
|
|
66
|
-
|
|
67
52
|
const response = await this.generateContent(model, contents, config, signal);
|
|
68
53
|
return extractTextFromResponse(response);
|
|
69
54
|
}
|
|
70
55
|
}
|
|
71
56
|
|
|
72
|
-
export const
|
|
57
|
+
export const textGeneration = new TextGenerationService();
|
|
@@ -1,21 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export { BaseGeminiService } from "./base-gemini.service";
|
|
7
|
-
export type { BaseRequestOptions } from "./base-gemini.service";
|
|
8
|
-
|
|
9
|
-
// Internal services
|
|
10
|
-
export { geminiClientCoreService } from "./gemini-client-core.service";
|
|
11
|
-
export { geminiTextGenerationService } from "./gemini-text-generation.service";
|
|
12
|
-
export { geminiStructuredTextService } from "./gemini-structured-text.service";
|
|
13
|
-
export { geminiStreamingService } from "./gemini-streaming.service";
|
|
14
|
-
|
|
15
|
-
// Main Provider - Public API
|
|
16
|
-
export {
|
|
17
|
-
geminiProviderService,
|
|
18
|
-
GeminiProvider,
|
|
19
|
-
} from "./gemini-provider";
|
|
20
|
-
export type { GeminiProviderConfig } from "./gemini-provider";
|
|
21
|
-
|
|
1
|
+
export { geminiClient } from "./GeminiClient";
|
|
2
|
+
export { textGeneration } from "./TextGeneration";
|
|
3
|
+
export { structuredText } from "./StructuredText";
|
|
4
|
+
export { streaming } from "./Streaming";
|
|
5
|
+
export { geminiProvider, GeminiProvider } from "./GeminiProvider";
|
|
@@ -76,7 +76,7 @@ export function createTextContent(
|
|
|
76
76
|
/**
|
|
77
77
|
* Transform SDK candidate to domain format
|
|
78
78
|
*/
|
|
79
|
-
|
|
79
|
+
function transformCandidate(
|
|
80
80
|
candidate: {
|
|
81
81
|
content: { parts: Array<{ text?: string }>; role?: string };
|
|
82
82
|
finishReason?: string;
|
|
@@ -87,7 +87,7 @@ function matchesPattern(message: string, patterns: string[]): boolean {
|
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
function mapGeminiError(error: unknown): GeminiErrorInfo {
|
|
91
91
|
const message = error instanceof Error ? error.message : String(error);
|
|
92
92
|
const statusCode = getStatusCode(error);
|
|
93
93
|
|
|
@@ -114,14 +114,6 @@ export function mapGeminiError(error: unknown): GeminiErrorInfo {
|
|
|
114
114
|
};
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
export function isGeminiErrorRetryable(error: unknown): boolean {
|
|
118
|
-
return mapGeminiError(error).retryable;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function categorizeGeminiError(error: unknown): GeminiErrorType {
|
|
122
|
-
return mapGeminiError(error).type;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
117
|
export function createGeminiError(error: unknown): GeminiError {
|
|
126
118
|
const errorInfo = mapGeminiError(error);
|
|
127
119
|
return GeminiError.fromError(error, errorInfo);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
/**
|
|
7
7
|
* Clean JSON text by removing markdown code blocks and extra whitespace
|
|
8
8
|
*/
|
|
9
|
-
|
|
9
|
+
function cleanJsonText(text: string): string {
|
|
10
10
|
if (!text || typeof text !== "string") {
|
|
11
11
|
return "";
|
|
12
12
|
}
|
|
@@ -39,52 +39,3 @@ export function parseJsonResponse<T>(text: string): T {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
/**
|
|
43
|
-
* Safely parse JSON with optional fallback value
|
|
44
|
-
*/
|
|
45
|
-
export function safeParseJson<T>(
|
|
46
|
-
text: string,
|
|
47
|
-
fallback: T
|
|
48
|
-
): T {
|
|
49
|
-
try {
|
|
50
|
-
return parseJsonResponse<T>(text);
|
|
51
|
-
} catch {
|
|
52
|
-
return fallback;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Extract and parse JSON from a larger text response
|
|
58
|
-
* Looks for JSON objects within markdown code blocks or standalone
|
|
59
|
-
*/
|
|
60
|
-
export function extractJsonFromText<T>(text: string): T | null {
|
|
61
|
-
// Try to find JSON in code blocks first
|
|
62
|
-
const codeBlockMatch = text.match(/```json\s*([\s\S]*?)\s*```/);
|
|
63
|
-
if (codeBlockMatch) {
|
|
64
|
-
try {
|
|
65
|
-
return JSON.parse(codeBlockMatch[1].trim()) as T;
|
|
66
|
-
} catch {
|
|
67
|
-
// Continue to other methods
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Try to find JSON object boundaries
|
|
72
|
-
const firstBrace = text.indexOf("{");
|
|
73
|
-
const lastBrace = text.lastIndexOf("}");
|
|
74
|
-
|
|
75
|
-
if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
|
|
76
|
-
try {
|
|
77
|
-
const jsonStr = text.substring(firstBrace, lastBrace + 1);
|
|
78
|
-
return JSON.parse(jsonStr) as T;
|
|
79
|
-
} catch {
|
|
80
|
-
// Continue to fallback
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Try parsing the whole text as JSON
|
|
85
|
-
try {
|
|
86
|
-
return parseJsonResponse<T>(text);
|
|
87
|
-
} catch {
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
* Reusable stream handling logic
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
interface StreamChunk {
|
|
7
7
|
text: () => string;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
type ChunkCallback = (text: string) => void;
|
|
11
|
+
type ErrorLogger = (error: unknown, context?: string) => void;
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Process async stream with chunk callback
|
|
@@ -67,90 +67,3 @@ function logError(
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
/**
|
|
71
|
-
* Create a buffered stream processor
|
|
72
|
-
* Accumulates chunks until a condition is met
|
|
73
|
-
*/
|
|
74
|
-
export class BufferedStreamProcessor {
|
|
75
|
-
private buffer = "";
|
|
76
|
-
private fullText = "";
|
|
77
|
-
|
|
78
|
-
constructor(
|
|
79
|
-
private onFlush: (text: string) => void,
|
|
80
|
-
private flushCondition: (buffer: string) => boolean = () => false
|
|
81
|
-
) {}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Process a single chunk
|
|
85
|
-
*/
|
|
86
|
-
processChunk(chunk: StreamChunk): void {
|
|
87
|
-
try {
|
|
88
|
-
const chunkText = chunk.text();
|
|
89
|
-
if (!chunkText) return;
|
|
90
|
-
|
|
91
|
-
this.buffer += chunkText;
|
|
92
|
-
this.fullText += chunkText;
|
|
93
|
-
|
|
94
|
-
if (this.flushCondition(this.buffer)) {
|
|
95
|
-
this.flush();
|
|
96
|
-
}
|
|
97
|
-
} catch (error) {
|
|
98
|
-
// Ignore chunk errors
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Flush buffer to callback
|
|
104
|
-
*/
|
|
105
|
-
flush(): void {
|
|
106
|
-
if (this.buffer) {
|
|
107
|
-
try {
|
|
108
|
-
this.onFlush(this.buffer);
|
|
109
|
-
this.buffer = "";
|
|
110
|
-
} catch {
|
|
111
|
-
// Ignore callback errors
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Get accumulated full text
|
|
118
|
-
*/
|
|
119
|
-
getFullText(): string {
|
|
120
|
-
return this.fullText;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Process entire stream
|
|
125
|
-
*/
|
|
126
|
-
async processStream(stream: AsyncIterable<StreamChunk>): Promise<string> {
|
|
127
|
-
for await (const chunk of stream) {
|
|
128
|
-
this.processChunk(chunk);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Flush remaining buffer
|
|
132
|
-
this.flush();
|
|
133
|
-
|
|
134
|
-
return this.fullText;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Common flush conditions
|
|
140
|
-
*/
|
|
141
|
-
export const flushConditions = {
|
|
142
|
-
/**
|
|
143
|
-
* Flush on newline
|
|
144
|
-
*/
|
|
145
|
-
onNewline: (buffer: string): boolean => buffer.includes("\n"),
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Flush when buffer reaches size
|
|
149
|
-
*/
|
|
150
|
-
onSize: (size: number) => (buffer: string): boolean => buffer.length >= size,
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Flush on pattern match
|
|
154
|
-
*/
|
|
155
|
-
onPattern: (pattern: RegExp) => (buffer: string): boolean => pattern.test(buffer),
|
|
156
|
-
};
|
|
@@ -1,11 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* React Hooks - Public API
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
// Internal - not exported from main index
|
|
6
|
-
export { useOperationManager } from "./use-operation-manager";
|
|
7
|
-
export type { OperationManager } from "./use-operation-manager";
|
|
8
|
-
|
|
9
|
-
// Public API
|
|
10
|
-
export { useGemini } from "./use-gemini";
|
|
11
|
-
export type { UseGeminiOptions, UseGeminiReturn } from "./use-gemini";
|
|
1
|
+
export { useGemini, type UseGeminiOptions, type UseGeminiReturn } from "./useGemini";
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { useState, useCallback, useMemo } from "react";
|
|
2
2
|
import type { GeminiGenerationConfig } from "../../domain/entities";
|
|
3
3
|
import { DEFAULT_MODELS } from "../../domain/entities";
|
|
4
|
-
import {
|
|
4
|
+
import { textGeneration, structuredText } from "../../infrastructure/services";
|
|
5
5
|
import { executeWithState, type AsyncStateSetters } from "../../infrastructure/utils/async";
|
|
6
6
|
import { parseJsonResponse } from "../../infrastructure/utils/json-parser.util";
|
|
7
|
-
import { useOperationManager } from "./
|
|
7
|
+
import { useOperationManager } from "./useOperationManager";
|
|
8
8
|
|
|
9
9
|
export interface UseGeminiOptions {
|
|
10
10
|
model?: string;
|
|
@@ -55,7 +55,7 @@ export function useGemini(options: UseGeminiOptions = {}): UseGeminiReturn {
|
|
|
55
55
|
setters,
|
|
56
56
|
callbacks,
|
|
57
57
|
async () => {
|
|
58
|
-
return
|
|
58
|
+
return textGeneration.generateText(
|
|
59
59
|
model,
|
|
60
60
|
prompt,
|
|
61
61
|
options.generationConfig,
|
|
@@ -95,7 +95,7 @@ export function useGemini(options: UseGeminiOptions = {}): UseGeminiReturn {
|
|
|
95
95
|
jsonCallbacks,
|
|
96
96
|
async () => {
|
|
97
97
|
if (schema) {
|
|
98
|
-
return
|
|
98
|
+
return structuredText.generateStructuredText<T>(
|
|
99
99
|
model,
|
|
100
100
|
prompt,
|
|
101
101
|
schema,
|
|
@@ -104,7 +104,7 @@ export function useGemini(options: UseGeminiOptions = {}): UseGeminiReturn {
|
|
|
104
104
|
);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
const text = await
|
|
107
|
+
const text = await textGeneration.generateText(
|
|
108
108
|
model,
|
|
109
109
|
prompt,
|
|
110
110
|
{ ...options.generationConfig, responseMimeType: "application/json" },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { geminiClient } from "../infrastructure/services/GeminiClient";
|
|
2
2
|
import { ConfigBuilder, type ProviderConfig } from "./ConfigBuilder";
|
|
3
3
|
|
|
4
4
|
// Re-export for public API
|
|
@@ -40,7 +40,7 @@ class ProviderFactory {
|
|
|
40
40
|
|
|
41
41
|
// Initialize Gemini client
|
|
42
42
|
const geminiConfig = this.builder.toGeminiConfig();
|
|
43
|
-
|
|
43
|
+
geminiClient.initialize(geminiConfig);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
@@ -1,78 +0,0 @@
|
|
|
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,30 +0,0 @@
|
|
|
1
|
-
import { BaseInterceptor, type BaseContext } from "./BaseInterceptor";
|
|
2
|
-
|
|
3
|
-
export interface RequestContext extends BaseContext {
|
|
4
|
-
payload: Record<string, unknown>;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export type RequestInterceptor = (context: RequestContext) => RequestContext | Promise<RequestContext>;
|
|
8
|
-
|
|
9
|
-
class RequestInterceptors extends BaseInterceptor<RequestContext> {
|
|
10
|
-
/**
|
|
11
|
-
* Apply all interceptors to a request context
|
|
12
|
-
* Interceptors are called in order (first registered = first called)
|
|
13
|
-
*/
|
|
14
|
-
async apply(context: RequestContext): Promise<RequestContext> {
|
|
15
|
-
let result = context;
|
|
16
|
-
|
|
17
|
-
for (const interceptor of this.interceptors) {
|
|
18
|
-
try {
|
|
19
|
-
result = await interceptor(result);
|
|
20
|
-
} catch (error) {
|
|
21
|
-
this.handleError(context, error);
|
|
22
|
-
// If we get here, strategy was "skip" or "log" - continue with previous result
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return result;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export const requestInterceptors = new RequestInterceptors();
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { BaseInterceptor, type BaseContext } from "./BaseInterceptor";
|
|
2
|
-
|
|
3
|
-
export interface ResponseContext<T = unknown> extends BaseContext {
|
|
4
|
-
data: T;
|
|
5
|
-
duration: number;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export type ResponseInterceptor<T = unknown> = (
|
|
9
|
-
context: ResponseContext<T>,
|
|
10
|
-
) => ResponseContext<T> | Promise<ResponseContext<T>>;
|
|
11
|
-
|
|
12
|
-
class ResponseInterceptors extends BaseInterceptor<ResponseContext<unknown>> {
|
|
13
|
-
/**
|
|
14
|
-
* Apply all interceptors to a response context
|
|
15
|
-
* Interceptors are called in reverse order (last registered = first called)
|
|
16
|
-
*/
|
|
17
|
-
async apply<T>(context: ResponseContext<T>): Promise<ResponseContext<T>> {
|
|
18
|
-
let result: ResponseContext<unknown> = context;
|
|
19
|
-
|
|
20
|
-
// Apply in reverse order (last added = first processed)
|
|
21
|
-
for (let i = this.interceptors.length - 1; i >= 0; i--) {
|
|
22
|
-
const interceptor = this.interceptors[i];
|
|
23
|
-
try {
|
|
24
|
-
result = await interceptor(result);
|
|
25
|
-
} catch (error) {
|
|
26
|
-
this.handleError(context, error);
|
|
27
|
-
// If we get here, strategy was "skip" or "log" - continue with previous result
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return result as ResponseContext<T>;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export const responseInterceptors = new ResponseInterceptors();
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Interceptors Module - Internal Use Only
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export { BaseInterceptor } from "./BaseInterceptor";
|
|
6
|
-
export type { BaseContext, InterceptorErrorStrategy } from "./BaseInterceptor";
|
|
7
|
-
|
|
8
|
-
export { requestInterceptors } from "./RequestInterceptors";
|
|
9
|
-
export type { RequestContext, RequestInterceptor } from "./RequestInterceptors";
|
|
10
|
-
|
|
11
|
-
export { responseInterceptors } from "./ResponseInterceptors";
|
|
12
|
-
export type { ResponseContext, ResponseInterceptor } from "./ResponseInterceptors";
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import type { GeminiConfig } from "../../domain/entities";
|
|
3
|
-
import { geminiClientCoreService } from "./gemini-client-core.service";
|
|
4
|
-
import { geminiStructuredTextService } from "./gemini-structured-text.service";
|
|
5
|
-
import { validatePrompt } from "../utils/validation.util";
|
|
6
|
-
|
|
7
|
-
export type GeminiProviderConfig = GeminiConfig;
|
|
8
|
-
|
|
9
|
-
export class GeminiProvider {
|
|
10
|
-
readonly providerId = "gemini";
|
|
11
|
-
readonly providerName = "Google Gemini";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Initialize the Gemini provider
|
|
15
|
-
*
|
|
16
|
-
* @throws {Error} If already initialized or configuration is invalid
|
|
17
|
-
*/
|
|
18
|
-
initialize(config: GeminiProviderConfig): void {
|
|
19
|
-
if (geminiClientCoreService.isInitialized()) {
|
|
20
|
-
throw new Error("Provider already initialized. Call reset() before re-initializing with new config.");
|
|
21
|
-
}
|
|
22
|
-
geminiClientCoreService.initialize(config);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Check if provider is initialized
|
|
27
|
-
*/
|
|
28
|
-
isInitialized(): boolean {
|
|
29
|
-
return geminiClientCoreService.isInitialized();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Reset the provider to uninitialized state
|
|
34
|
-
*/
|
|
35
|
-
reset(): void {
|
|
36
|
-
geminiClientCoreService.reset();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Generate structured JSON response
|
|
41
|
-
*
|
|
42
|
-
* @throws {GeminiError} For API-specific errors
|
|
43
|
-
* @throws {Error} For validation or network errors
|
|
44
|
-
*/
|
|
45
|
-
async generateStructuredText<T>(
|
|
46
|
-
prompt: string,
|
|
47
|
-
schema: Record<string, unknown>,
|
|
48
|
-
model: string,
|
|
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
|
-
|
|
58
|
-
return geminiStructuredTextService.generateStructuredText<T>(model, prompt, schema);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export const geminiProviderService = new GeminiProvider();
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Utility Functions - Internal Use Only
|
|
3
|
-
* These are internal implementation details and should not be used directly by consumers
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// Error handling (internal)
|
|
7
|
-
export {
|
|
8
|
-
mapGeminiError,
|
|
9
|
-
isGeminiErrorRetryable,
|
|
10
|
-
categorizeGeminiError,
|
|
11
|
-
createGeminiError
|
|
12
|
-
} from "./error-mapper.util";
|
|
13
|
-
|
|
14
|
-
// Data transformation (internal)
|
|
15
|
-
export { extractTextFromResponse } from "./gemini-data-transformer.util";
|
|
16
|
-
export {
|
|
17
|
-
cleanJsonText,
|
|
18
|
-
parseJsonResponse,
|
|
19
|
-
safeParseJson,
|
|
20
|
-
extractJsonFromText
|
|
21
|
-
} from "./json-parser.util";
|
|
22
|
-
export {
|
|
23
|
-
toSdkContent,
|
|
24
|
-
createTextContent,
|
|
25
|
-
transformCandidate,
|
|
26
|
-
transformResponse,
|
|
27
|
-
extractTextFromParts
|
|
28
|
-
} from "./content-mapper.util";
|
|
29
|
-
|
|
30
|
-
// Validation (internal)
|
|
31
|
-
export {
|
|
32
|
-
validateModelName,
|
|
33
|
-
validateApiKey,
|
|
34
|
-
validateSchema,
|
|
35
|
-
validatePrompt,
|
|
36
|
-
validateTimeout,
|
|
37
|
-
isValidObject,
|
|
38
|
-
validateRequiredFields
|
|
39
|
-
} from "./validation.util";
|
|
40
|
-
|
|
41
|
-
// Async state management (internal)
|
|
42
|
-
export {
|
|
43
|
-
executeWithState,
|
|
44
|
-
type AsyncStateCallbacks,
|
|
45
|
-
type AsyncStateSetters,
|
|
46
|
-
type AsyncStateConfig
|
|
47
|
-
} from "./async";
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validation Composers
|
|
3
|
-
* Composable validation rules for clean, reusable validation
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export type ValidationRule<T = unknown> = (value: T) => string | null;
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Compose multiple validation rules into one
|
|
10
|
-
*/
|
|
11
|
-
export function compose<T>(...rules: ValidationRule<T>[]): ValidationRule<T> {
|
|
12
|
-
return (value: T): string | null => {
|
|
13
|
-
for (const rule of rules) {
|
|
14
|
-
const error = rule(value);
|
|
15
|
-
if (error) return error;
|
|
16
|
-
}
|
|
17
|
-
return null;
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Validate that value is not empty
|
|
23
|
-
*/
|
|
24
|
-
export function required(fieldName: string = "Field"): ValidationRule<string> {
|
|
25
|
-
return (value: string): string | null => {
|
|
26
|
-
if (!value || typeof value !== "string" || value.trim().length === 0) {
|
|
27
|
-
return `${fieldName} is required`;
|
|
28
|
-
}
|
|
29
|
-
return null;
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Validate minimum length
|
|
35
|
-
*/
|
|
36
|
-
export function minLength(min: number, fieldName: string = "Field"): ValidationRule<string> {
|
|
37
|
-
return (value: string): string | null => {
|
|
38
|
-
if (value.trim().length < min) {
|
|
39
|
-
return `${fieldName} must be at least ${min} characters`;
|
|
40
|
-
}
|
|
41
|
-
return null;
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Validate maximum length
|
|
47
|
-
*/
|
|
48
|
-
export function maxLength(max: number, fieldName: string = "Field"): ValidationRule<string> {
|
|
49
|
-
return (value: string): string | null => {
|
|
50
|
-
if (value.length > max) {
|
|
51
|
-
return `${fieldName} must be at most ${max} characters`;
|
|
52
|
-
}
|
|
53
|
-
return null;
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Validate string starts with prefix
|
|
59
|
-
*/
|
|
60
|
-
export function startsWith(prefix: string, fieldName: string = "Field"): ValidationRule<string> {
|
|
61
|
-
return (value: string): string | null => {
|
|
62
|
-
if (!value.startsWith(prefix)) {
|
|
63
|
-
return `${fieldName} must start with "${prefix}"`;
|
|
64
|
-
}
|
|
65
|
-
return null;
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Validate number is in range
|
|
71
|
-
*/
|
|
72
|
-
export function inRange(min: number, max: number, fieldName: string = "Value"): ValidationRule<number> {
|
|
73
|
-
return (value: number): string | null => {
|
|
74
|
-
if (typeof value !== "number" || value < min || value > max) {
|
|
75
|
-
return `${fieldName} must be between ${min} and ${max}`;
|
|
76
|
-
}
|
|
77
|
-
return null;
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Validate object has required properties
|
|
83
|
-
*/
|
|
84
|
-
export function hasProperties(...props: string[]): ValidationRule<Record<string, unknown>> {
|
|
85
|
-
return (value: Record<string, unknown>): string | null => {
|
|
86
|
-
const missing = props.filter((prop) => !(prop in value) || value[prop] === undefined);
|
|
87
|
-
if (missing.length > 0) {
|
|
88
|
-
return `Missing required properties: ${missing.join(", ")}`;
|
|
89
|
-
}
|
|
90
|
-
return null;
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Validate object structure (for schemas)
|
|
96
|
-
*/
|
|
97
|
-
export function isValidSchema(): ValidationRule<Record<string, unknown>> {
|
|
98
|
-
return compose(
|
|
99
|
-
(schema): string | null => {
|
|
100
|
-
if (!schema || typeof schema !== "object") {
|
|
101
|
-
return "Schema must be a non-empty object";
|
|
102
|
-
}
|
|
103
|
-
return null;
|
|
104
|
-
},
|
|
105
|
-
(schema): string | null => {
|
|
106
|
-
if (Object.keys(schema).length === 0) {
|
|
107
|
-
return "Schema must contain at least one property";
|
|
108
|
-
}
|
|
109
|
-
return null;
|
|
110
|
-
},
|
|
111
|
-
hasProperties("type"),
|
|
112
|
-
(schema): string | null => {
|
|
113
|
-
const schemaType = schema.type;
|
|
114
|
-
if (schemaType !== "object" && schemaType !== "array") {
|
|
115
|
-
return `Schema type must be "object" or "array", got "${String(schemaType)}"`;
|
|
116
|
-
}
|
|
117
|
-
return null;
|
|
118
|
-
},
|
|
119
|
-
(schema): string | null => {
|
|
120
|
-
if (schema.type === "object" && !("properties" in schema)) {
|
|
121
|
-
return 'Object schema must have a "properties" field';
|
|
122
|
-
}
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Helper to validate and throw on error
|
|
130
|
-
*/
|
|
131
|
-
export function validateOrThrow<T>(value: T, rule: ValidationRule<T>): void {
|
|
132
|
-
const error = rule(value);
|
|
133
|
-
if (error) {
|
|
134
|
-
throw new Error(error);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Pre-built composite validators
|
|
140
|
-
*/
|
|
141
|
-
export const validators = {
|
|
142
|
-
apiKey: compose(
|
|
143
|
-
required("API key"),
|
|
144
|
-
minLength(10, "API key")
|
|
145
|
-
),
|
|
146
|
-
|
|
147
|
-
modelName: compose(
|
|
148
|
-
required("Model name"),
|
|
149
|
-
startsWith("gemini-", "Model name")
|
|
150
|
-
),
|
|
151
|
-
|
|
152
|
-
prompt: compose(
|
|
153
|
-
required("Prompt"),
|
|
154
|
-
minLength(3, "Prompt")
|
|
155
|
-
),
|
|
156
|
-
|
|
157
|
-
timeout: inRange(1, 300000, "Timeout"),
|
|
158
|
-
|
|
159
|
-
schema: isValidSchema(),
|
|
160
|
-
};
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validation Utilities (Legacy)
|
|
3
|
-
* Maintained for backward compatibility
|
|
4
|
-
* New code should use validation-composer.util.ts
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { validateOrThrow, validators } from "./validation-composer.util";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Validate model name format
|
|
11
|
-
* @throws Error if model name is invalid
|
|
12
|
-
*/
|
|
13
|
-
export function validateModelName(modelName: string): void {
|
|
14
|
-
validateOrThrow(modelName, validators.modelName);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Validate API key format
|
|
19
|
-
* @throws Error if API key is invalid
|
|
20
|
-
*/
|
|
21
|
-
export function validateApiKey(apiKey: string): void {
|
|
22
|
-
validateOrThrow(apiKey, validators.apiKey);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Validate schema object for structured generation
|
|
27
|
-
* @throws Error if schema is invalid
|
|
28
|
-
*/
|
|
29
|
-
export function validateSchema(schema: Record<string, unknown>): void {
|
|
30
|
-
validateOrThrow(schema, validators.schema);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Validate prompt text
|
|
35
|
-
* @throws Error if prompt is invalid
|
|
36
|
-
*/
|
|
37
|
-
export function validatePrompt(prompt: string): void {
|
|
38
|
-
validateOrThrow(prompt, validators.prompt);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Validate timeout value
|
|
43
|
-
* @throws Error if timeout is invalid
|
|
44
|
-
*/
|
|
45
|
-
export function validateTimeout(timeout: number): void {
|
|
46
|
-
validateOrThrow(timeout, validators.timeout);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Check if a value is a valid object
|
|
51
|
-
*/
|
|
52
|
-
export function isValidObject(value: unknown): value is Record<string, unknown> {
|
|
53
|
-
return (
|
|
54
|
-
value !== null &&
|
|
55
|
-
typeof value === "object" &&
|
|
56
|
-
!Array.isArray(value) &&
|
|
57
|
-
Object.keys(value).length > 0
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Validate required fields in an object
|
|
63
|
-
*/
|
|
64
|
-
export function validateRequiredFields(
|
|
65
|
-
obj: Record<string, unknown>,
|
|
66
|
-
requiredFields: string[]
|
|
67
|
-
): void {
|
|
68
|
-
const missing = requiredFields.filter((field) => !(field in obj) || obj[field] === undefined);
|
|
69
|
-
|
|
70
|
-
if (missing.length > 0) {
|
|
71
|
-
throw new Error(`Missing required fields: ${missing.join(", ")}`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
File without changes
|