@umituz/react-native-ai-gemini-provider 3.0.41 → 3.0.43
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/application/builders/config-builder.ts +102 -0
- package/src/application/builders/index.ts +8 -0
- package/src/application/dtos/generation-request.dto.ts +89 -0
- package/src/application/dtos/index.ts +8 -0
- package/src/application/index.ts +16 -0
- package/src/application/providers/gemini-provider.ts +135 -0
- package/src/application/providers/index.ts +6 -0
- package/src/application/use-cases/generate-json.use-case.ts +73 -0
- package/src/application/use-cases/generate-text.use-case.ts +81 -0
- package/src/application/use-cases/index.ts +20 -0
- package/src/application/use-cases/stream-content.use-case.ts +46 -0
- package/src/domain/entities/error.types.ts +0 -5
- package/src/domain/entities/gemini.types.ts +3 -1
- package/src/domain/index.ts +16 -0
- package/src/domain/repositories/index.ts +19 -0
- package/src/domain/repositories/streaming.repository.ts +41 -0
- package/src/domain/repositories/structured-text.repository.ts +41 -0
- package/src/domain/repositories/text-generation.repository.ts +38 -0
- package/src/domain/services/validation.service.ts +157 -0
- package/src/domain/value-objects/api-key.vo.ts +55 -0
- package/src/domain/value-objects/index.ts +8 -0
- package/src/domain/value-objects/model-name.vo.ts +66 -0
- package/src/domain/value-objects/timeout.vo.ts +69 -0
- package/src/index.ts +110 -25
- package/src/infrastructure/external/gemini-client.singleton.ts +49 -0
- package/src/infrastructure/external/gemini-sdk.adapter.ts +143 -0
- package/src/infrastructure/external/index.ts +7 -0
- package/src/infrastructure/index.ts +16 -0
- package/src/infrastructure/mappers/content.mapper.ts +80 -0
- package/src/infrastructure/mappers/error.mapper.ts +152 -0
- package/src/infrastructure/mappers/index.ts +7 -0
- package/src/infrastructure/mappers/response.mapper.ts +165 -0
- package/src/infrastructure/repositories/base-gemini.repository.ts +94 -0
- package/src/infrastructure/repositories/gemini-streaming.repository.impl.ts +119 -0
- package/src/infrastructure/repositories/gemini-structured-text.repository.impl.ts +108 -0
- package/src/infrastructure/repositories/gemini-text.repository.impl.ts +76 -0
- package/src/infrastructure/repositories/index.ts +10 -0
- package/src/infrastructure/utils/index.ts +6 -0
- package/src/presentation/hooks/index.ts +8 -0
- package/src/presentation/hooks/use-gemini.hook.ts +181 -0
- package/src/presentation/hooks/use-operation-manager.hook.ts +67 -0
- package/src/presentation/index.ts +10 -0
- package/src/presentation/providers/gemini-provider.tsx +93 -0
- package/src/presentation/providers/index.ts +10 -0
- package/dist/domain/entities/error.types.d.ts +0 -96
- package/dist/domain/entities/gemini.types.d.ts +0 -128
- package/dist/domain/entities/index.d.ts +0 -6
- package/dist/domain/entities/models.d.ts +0 -23
- package/dist/index.d.ts +0 -15
- package/dist/infrastructure/services/BaseService.d.ts +0 -29
- package/dist/infrastructure/services/ChatSession.d.ts +0 -63
- package/dist/infrastructure/services/GeminiClient.d.ts +0 -16
- package/dist/infrastructure/services/GeminiProvider.d.ts +0 -10
- package/dist/infrastructure/services/Streaming.d.ts +0 -7
- package/dist/infrastructure/services/StructuredText.d.ts +0 -6
- package/dist/infrastructure/services/TextGeneration.d.ts +0 -8
- package/dist/infrastructure/services/index.d.ts +0 -6
- package/dist/infrastructure/telemetry/TelemetryHooks.d.ts +0 -41
- package/dist/infrastructure/telemetry/index.d.ts +0 -4
- package/dist/infrastructure/utils/async/execute-state.util.d.ts +0 -49
- package/dist/infrastructure/utils/async/index.d.ts +0 -4
- package/dist/infrastructure/utils/content-mapper.util.d.ts +0 -45
- package/dist/infrastructure/utils/error-mapper.util.d.ts +0 -2
- package/dist/infrastructure/utils/gemini-data-transformer.util.d.ts +0 -2
- package/dist/infrastructure/utils/json-parser.util.d.ts +0 -9
- package/dist/infrastructure/utils/stream-processor.util.d.ts +0 -14
- package/dist/presentation/hooks/index.d.ts +0 -1
- package/dist/presentation/hooks/useGemini.d.ts +0 -17
- package/dist/presentation/hooks/useOperationManager.d.ts +0 -23
- package/dist/providers/ConfigBuilder.d.ts +0 -46
- package/dist/providers/ProviderFactory.d.ts +0 -25
- package/dist/providers/index.d.ts +0 -7
- package/src/infrastructure/services/BaseService.ts +0 -53
- package/src/infrastructure/services/ChatSession.ts +0 -199
- package/src/infrastructure/services/GeminiClient.ts +0 -112
- package/src/infrastructure/services/Streaming.ts +0 -56
- package/src/infrastructure/services/StructuredText.ts +0 -57
- package/src/infrastructure/services/TextGeneration.ts +0 -57
- package/src/infrastructure/telemetry/TelemetryHooks.ts +0 -110
- package/src/infrastructure/utils/async/execute-state.util.ts +0 -93
- package/src/infrastructure/utils/content-mapper.util.ts +0 -175
- package/src/infrastructure/utils/error-mapper.util.ts +0 -145
- package/src/infrastructure/utils/gemini-data-transformer.util.ts +0 -40
- package/src/infrastructure/utils/text-calculations.util.ts +0 -51
- package/src/presentation/hooks/useGemini.ts +0 -125
- package/src/presentation/hooks/useOperationManager.ts +0 -88
- package/src/providers/ConfigBuilder.ts +0 -112
- package/src/providers/ProviderFactory.ts +0 -65
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini SDK Adapter
|
|
3
|
+
* Wrapper around Google Gemini SDK
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
GoogleGenerativeAI,
|
|
8
|
+
HarmCategory,
|
|
9
|
+
HarmBlockThreshold,
|
|
10
|
+
type GenerativeModel,
|
|
11
|
+
type SafetySetting,
|
|
12
|
+
} from "@google/generative-ai";
|
|
13
|
+
import { DEFAULT_MODELS } from "../../domain/entities";
|
|
14
|
+
import type { GeminiModelOptions } from "../../domain/entities";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Permissive safety settings (BLOCK_NONE for all categories)
|
|
18
|
+
*/
|
|
19
|
+
const PERMISSIVE_SAFETY: SafetySetting[] = [
|
|
20
|
+
{
|
|
21
|
+
category: HarmCategory.HARM_CATEGORY_HARASSMENT,
|
|
22
|
+
threshold: HarmBlockThreshold.BLOCK_NONE,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
category: HarmCategory.HARM_CATEGORY_HATE_SPEECH,
|
|
26
|
+
threshold: HarmBlockThreshold.BLOCK_NONE,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
|
30
|
+
threshold: HarmBlockThreshold.BLOCK_NONE,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
|
34
|
+
threshold: HarmBlockThreshold.BLOCK_NONE,
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export class GeminiSDKAdapter {
|
|
39
|
+
private client: GoogleGenerativeAI | null = null;
|
|
40
|
+
private initialized = false;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Initialize the SDK with API key
|
|
44
|
+
*/
|
|
45
|
+
initialize(apiKey: string): void {
|
|
46
|
+
if (!apiKey || apiKey.length < 10) {
|
|
47
|
+
throw new Error("API key must be at least 10 characters");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!apiKey.startsWith("AIza")) {
|
|
51
|
+
throw new Error('Invalid API key format. Must start with "AIza"');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.client = new GoogleGenerativeAI(apiKey);
|
|
55
|
+
this.initialized = true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get a configured GenerativeModel
|
|
60
|
+
*/
|
|
61
|
+
getModel(options?: string | GeminiModelOptions): GenerativeModel {
|
|
62
|
+
if (!this.client || !this.initialized) {
|
|
63
|
+
throw new Error("SDK not initialized. Call initialize() first.");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Normalize options
|
|
67
|
+
const opts: GeminiModelOptions =
|
|
68
|
+
typeof options === "string" ? { model: options } : options ?? {};
|
|
69
|
+
|
|
70
|
+
const effectiveModel = opts.model || DEFAULT_MODELS.TEXT;
|
|
71
|
+
|
|
72
|
+
if (!effectiveModel.startsWith("gemini-")) {
|
|
73
|
+
throw new Error('Model name must start with "gemini-"');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Map safety settings
|
|
77
|
+
const sdkSafety = opts.safetySettings
|
|
78
|
+
? this.mapSafetySettings(opts.safetySettings)
|
|
79
|
+
: PERMISSIVE_SAFETY;
|
|
80
|
+
|
|
81
|
+
return this.client.getGenerativeModel({
|
|
82
|
+
model: effectiveModel,
|
|
83
|
+
...(opts.systemInstruction && {
|
|
84
|
+
systemInstruction: opts.systemInstruction,
|
|
85
|
+
}),
|
|
86
|
+
safetySettings: sdkSafety,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Map domain safety settings to SDK format
|
|
92
|
+
*/
|
|
93
|
+
private mapSafetySettings(
|
|
94
|
+
settings: GeminiModelOptions["safetySettings"]
|
|
95
|
+
): SafetySetting[] {
|
|
96
|
+
if (!settings) return PERMISSIVE_SAFETY;
|
|
97
|
+
|
|
98
|
+
return settings.map((s) => {
|
|
99
|
+
const validCategories = [
|
|
100
|
+
HarmCategory.HARM_CATEGORY_HARASSMENT,
|
|
101
|
+
HarmCategory.HARM_CATEGORY_HATE_SPEECH,
|
|
102
|
+
HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
|
103
|
+
HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
const validThresholds = [
|
|
107
|
+
HarmBlockThreshold.BLOCK_NONE,
|
|
108
|
+
HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
|
|
109
|
+
HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
|
|
110
|
+
HarmBlockThreshold.BLOCK_ONLY_HIGH,
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const category = validCategories.includes(
|
|
114
|
+
s.category as unknown as HarmCategory
|
|
115
|
+
)
|
|
116
|
+
? (s.category as unknown as HarmCategory)
|
|
117
|
+
: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT;
|
|
118
|
+
|
|
119
|
+
const threshold = validThresholds.includes(
|
|
120
|
+
s.threshold as unknown as HarmBlockThreshold
|
|
121
|
+
)
|
|
122
|
+
? (s.threshold as unknown as HarmBlockThreshold)
|
|
123
|
+
: HarmBlockThreshold.BLOCK_NONE;
|
|
124
|
+
|
|
125
|
+
return { category, threshold };
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Check if SDK is initialized
|
|
131
|
+
*/
|
|
132
|
+
isInitialized(): boolean {
|
|
133
|
+
return this.initialized;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Reset the SDK
|
|
138
|
+
*/
|
|
139
|
+
reset(): void {
|
|
140
|
+
this.client = null;
|
|
141
|
+
this.initialized = false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure Layer
|
|
3
|
+
* External integrations and repository implementations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Mappers
|
|
7
|
+
export * from "./mappers";
|
|
8
|
+
|
|
9
|
+
// External Services
|
|
10
|
+
export * from "./external";
|
|
11
|
+
|
|
12
|
+
// Repository Implementations
|
|
13
|
+
export * from "./repositories";
|
|
14
|
+
|
|
15
|
+
// Utilities
|
|
16
|
+
export { parseJsonResponse } from "./utils/json-parser.util";
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Mapper
|
|
3
|
+
* Transforms between domain and SDK content formats
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Part } from "@google/generative-ai";
|
|
7
|
+
import type { GeminiContent, GeminiPart } from "../../domain/entities";
|
|
8
|
+
|
|
9
|
+
export class ContentMapper {
|
|
10
|
+
/**
|
|
11
|
+
* Convert domain content to SDK format
|
|
12
|
+
*/
|
|
13
|
+
toSdk(content: GeminiContent): { role: string; parts: Part[] } {
|
|
14
|
+
return {
|
|
15
|
+
role: content.role || "user",
|
|
16
|
+
parts: content.parts.map(this.mapPartToSdk),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Convert domain content array to SDK format
|
|
22
|
+
*/
|
|
23
|
+
toSdkArray(contents: GeminiContent[]): Array<{ role: string; parts: Part[] }> {
|
|
24
|
+
return contents.map((c) => this.toSdk(c));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Convert SDK content to domain format
|
|
29
|
+
*/
|
|
30
|
+
toDomain(sdk: { role: string; parts: Part[] }): GeminiContent {
|
|
31
|
+
return {
|
|
32
|
+
role: sdk.role as "user" | "model",
|
|
33
|
+
parts: sdk.parts.map(this.mapSdkPartToDomain),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Map a single domain part to SDK format
|
|
39
|
+
*/
|
|
40
|
+
private mapPartToSdk(part: GeminiPart): Part {
|
|
41
|
+
if ("text" in part) {
|
|
42
|
+
return { text: part.text };
|
|
43
|
+
}
|
|
44
|
+
if ("inlineData" in part) {
|
|
45
|
+
return { inlineData: part.inlineData };
|
|
46
|
+
}
|
|
47
|
+
throw new Error("Unknown part type");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Map a single SDK part to domain format
|
|
52
|
+
*/
|
|
53
|
+
private mapSdkPartToDomain(part: Part): GeminiPart {
|
|
54
|
+
if ("text" in part && typeof part.text === "string") {
|
|
55
|
+
return { text: part.text };
|
|
56
|
+
}
|
|
57
|
+
if ("inlineData" in part) {
|
|
58
|
+
return { inlineData: part.inlineData };
|
|
59
|
+
}
|
|
60
|
+
return { text: "" };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Create a simple text content
|
|
65
|
+
*/
|
|
66
|
+
createTextContent(text: string, role: "user" | "model" = "user"): GeminiContent {
|
|
67
|
+
return {
|
|
68
|
+
parts: [{ text }],
|
|
69
|
+
role,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Extract text from content parts
|
|
75
|
+
*/
|
|
76
|
+
extractText(parts: GeminiPart[] | undefined): string {
|
|
77
|
+
if (!parts || parts.length === 0) return "";
|
|
78
|
+
return parts.map((p) => ("text" in p ? (p.text || "") : "")).join("");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Mapper
|
|
3
|
+
* Maps unknown errors to domain GeminiError
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { GeminiError, GeminiErrorType } from "../../domain/entities";
|
|
7
|
+
|
|
8
|
+
const ERROR_PATTERNS: Array<{
|
|
9
|
+
pattern: string[];
|
|
10
|
+
type: GeminiErrorType;
|
|
11
|
+
retryable: boolean;
|
|
12
|
+
}> = [
|
|
13
|
+
{
|
|
14
|
+
pattern: ["quota", "resource exhausted", "429"],
|
|
15
|
+
type: GeminiErrorType.QUOTA_EXCEEDED,
|
|
16
|
+
retryable: true,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
pattern: ["rate limit", "too many requests"],
|
|
20
|
+
type: GeminiErrorType.RATE_LIMIT,
|
|
21
|
+
retryable: true,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
pattern: ["unauthorized", "invalid api key", "401", "403", "permission"],
|
|
25
|
+
type: GeminiErrorType.AUTHENTICATION,
|
|
26
|
+
retryable: false,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
pattern: ["safety", "safety filter", "harmful", "blocked by safety"],
|
|
30
|
+
type: GeminiErrorType.SAFETY,
|
|
31
|
+
retryable: false,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
pattern: ["model not found", "404", "not found"],
|
|
35
|
+
type: GeminiErrorType.MODEL_NOT_FOUND,
|
|
36
|
+
retryable: false,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
pattern: ["network", "fetch failed", "connection", "socket"],
|
|
40
|
+
type: GeminiErrorType.NETWORK,
|
|
41
|
+
retryable: true,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
pattern: ["timeout", "timed out"],
|
|
45
|
+
type: GeminiErrorType.TIMEOUT,
|
|
46
|
+
retryable: true,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
pattern: ["500", "502", "503", "504", "internal server", "unavailable"],
|
|
50
|
+
type: GeminiErrorType.SERVER,
|
|
51
|
+
retryable: true,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
pattern: ["invalid", "bad request", "400"],
|
|
55
|
+
type: GeminiErrorType.VALIDATION,
|
|
56
|
+
retryable: false,
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const STATUS_CODE_MAP: Record<
|
|
61
|
+
number,
|
|
62
|
+
{ type: GeminiErrorType; retryable: boolean }
|
|
63
|
+
> = {
|
|
64
|
+
400: { type: GeminiErrorType.VALIDATION, retryable: false },
|
|
65
|
+
401: { type: GeminiErrorType.AUTHENTICATION, retryable: false },
|
|
66
|
+
403: { type: GeminiErrorType.AUTHENTICATION, retryable: false },
|
|
67
|
+
404: { type: GeminiErrorType.MODEL_NOT_FOUND, retryable: false },
|
|
68
|
+
429: { type: GeminiErrorType.RATE_LIMIT, retryable: true },
|
|
69
|
+
500: { type: GeminiErrorType.SERVER, retryable: true },
|
|
70
|
+
502: { type: GeminiErrorType.SERVER, retryable: true },
|
|
71
|
+
503: { type: GeminiErrorType.SERVER, retryable: true },
|
|
72
|
+
504: { type: GeminiErrorType.SERVER, retryable: true },
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export class ErrorMapper {
|
|
76
|
+
/**
|
|
77
|
+
* Map unknown error to GeminiError
|
|
78
|
+
*/
|
|
79
|
+
static map(error: unknown, context: string): GeminiError {
|
|
80
|
+
if (error instanceof GeminiError) {
|
|
81
|
+
return error;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
85
|
+
const statusCode = ErrorMapper.extractStatusCode(error);
|
|
86
|
+
|
|
87
|
+
// Primary: classify by HTTP status code
|
|
88
|
+
if (statusCode && STATUS_CODE_MAP[statusCode]) {
|
|
89
|
+
const { type, retryable } = STATUS_CODE_MAP[statusCode];
|
|
90
|
+
return GeminiError.fromError(error, {
|
|
91
|
+
type,
|
|
92
|
+
messageKey: `error.gemini.${type.toLowerCase()}`,
|
|
93
|
+
retryable,
|
|
94
|
+
originalError: error,
|
|
95
|
+
statusCode,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Secondary: classify by message pattern
|
|
100
|
+
for (const { pattern, type, retryable } of ERROR_PATTERNS) {
|
|
101
|
+
if (ErrorMapper.matchesPattern(message, pattern)) {
|
|
102
|
+
return GeminiError.fromError(error, {
|
|
103
|
+
type,
|
|
104
|
+
messageKey: `error.gemini.${type.toLowerCase()}`,
|
|
105
|
+
retryable,
|
|
106
|
+
originalError: error,
|
|
107
|
+
statusCode,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Default: unknown error
|
|
113
|
+
return GeminiError.fromError(error, {
|
|
114
|
+
type: GeminiErrorType.UNKNOWN,
|
|
115
|
+
messageKey: "error.gemini.unknown",
|
|
116
|
+
retryable: false,
|
|
117
|
+
originalError: error,
|
|
118
|
+
statusCode,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Extract HTTP status code from error
|
|
124
|
+
*/
|
|
125
|
+
private static extractStatusCode(error: unknown): number | undefined {
|
|
126
|
+
if (error && typeof error === "object") {
|
|
127
|
+
const err = error as Record<string, unknown>;
|
|
128
|
+
if (typeof err.status === "number") return err.status;
|
|
129
|
+
if (typeof err.statusCode === "number") return err.statusCode;
|
|
130
|
+
|
|
131
|
+
const response = err.response as
|
|
132
|
+
| { error?: { code?: number } }
|
|
133
|
+
| undefined;
|
|
134
|
+
if (response?.error?.code) return response.error.code;
|
|
135
|
+
}
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check if message matches any pattern
|
|
141
|
+
*/
|
|
142
|
+
private static matchesPattern(message: string, patterns: string[]): boolean {
|
|
143
|
+
const lower = message.toLowerCase();
|
|
144
|
+
return patterns.some((pattern) => {
|
|
145
|
+
const words = pattern.toLowerCase().split(/\s+/);
|
|
146
|
+
return words.every((word) => {
|
|
147
|
+
const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
148
|
+
return new RegExp(`\\b${escaped}\\b`, "i").test(lower);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response Mapper
|
|
3
|
+
* Transforms SDK responses to domain format
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
GeminiResponse,
|
|
8
|
+
GeminiCandidate,
|
|
9
|
+
GeminiContent,
|
|
10
|
+
GeminiFinishReason,
|
|
11
|
+
GeminiHarmCategory,
|
|
12
|
+
GeminiSafetyRating,
|
|
13
|
+
} from "../../domain/entities";
|
|
14
|
+
|
|
15
|
+
const VALID_FINISH_REASONS = [
|
|
16
|
+
"FINISH_REASON_UNSPECIFIED",
|
|
17
|
+
"STOP",
|
|
18
|
+
"MAX_TOKENS",
|
|
19
|
+
"SAFETY",
|
|
20
|
+
"RECITATION",
|
|
21
|
+
"OTHER",
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
const VALID_HARM_CATEGORIES = [
|
|
25
|
+
"HARM_CATEGORY_HARASSMENT",
|
|
26
|
+
"HARM_CATEGORY_HATE_SPEECH",
|
|
27
|
+
"HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
|
28
|
+
"HARM_CATEGORY_DANGEROUS_CONTENT",
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
const VALID_PROBABILITIES = ["NEGLIGIBLE", "LOW", "MEDIUM", "HIGH"] as const;
|
|
32
|
+
|
|
33
|
+
export class ResponseMapper {
|
|
34
|
+
/**
|
|
35
|
+
* Convert SDK response to domain format
|
|
36
|
+
*/
|
|
37
|
+
toDomain(response: {
|
|
38
|
+
candidates?: Array<{
|
|
39
|
+
content: { parts: Array<{ text?: string }>; role?: string };
|
|
40
|
+
finishReason?: string;
|
|
41
|
+
safetyRatings?: Array<{ category: string; probability: string }>;
|
|
42
|
+
}>;
|
|
43
|
+
usageMetadata?: {
|
|
44
|
+
promptTokenCount?: number;
|
|
45
|
+
candidatesTokenCount?: number;
|
|
46
|
+
totalTokenCount?: number;
|
|
47
|
+
};
|
|
48
|
+
}): GeminiResponse {
|
|
49
|
+
return {
|
|
50
|
+
candidates: response.candidates?.map((c) => this.mapCandidate(c)),
|
|
51
|
+
usageMetadata: response.usageMetadata
|
|
52
|
+
? {
|
|
53
|
+
promptTokenCount: response.usageMetadata.promptTokenCount,
|
|
54
|
+
candidatesTokenCount: response.usageMetadata.candidatesTokenCount,
|
|
55
|
+
totalTokenCount: response.usageMetadata.totalTokenCount,
|
|
56
|
+
}
|
|
57
|
+
: undefined,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Map a single candidate
|
|
63
|
+
*/
|
|
64
|
+
private mapCandidate(
|
|
65
|
+
candidate: {
|
|
66
|
+
content: { parts: Array<{ text?: string }>; role?: string };
|
|
67
|
+
finishReason?: string;
|
|
68
|
+
safetyRatings?: Array<{ category: string; probability: string }>;
|
|
69
|
+
}
|
|
70
|
+
): GeminiCandidate {
|
|
71
|
+
return {
|
|
72
|
+
content: this.mapCandidateContent(candidate.content),
|
|
73
|
+
finishReason: this.mapFinishReason(candidate.finishReason),
|
|
74
|
+
safetyRatings: this.mapSafetyRatings(candidate.safetyRatings),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Map candidate content
|
|
80
|
+
*/
|
|
81
|
+
private mapCandidateContent(content: {
|
|
82
|
+
parts: Array<{ text?: string }>;
|
|
83
|
+
role?: string;
|
|
84
|
+
}): GeminiContent {
|
|
85
|
+
return {
|
|
86
|
+
parts: content.parts
|
|
87
|
+
.filter((p) => p.text)
|
|
88
|
+
.map((p) => ({ text: p.text || "" })),
|
|
89
|
+
role: content.role === "user" || content.role === "model" ? content.role : "model",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Map finish reason with validation
|
|
95
|
+
*/
|
|
96
|
+
private mapFinishReason(value: string | undefined): GeminiFinishReason | undefined {
|
|
97
|
+
if (!value) return undefined;
|
|
98
|
+
return VALID_FINISH_REASONS.includes(value as GeminiFinishReason)
|
|
99
|
+
? (value as GeminiFinishReason)
|
|
100
|
+
: undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Map safety ratings with validation
|
|
105
|
+
*/
|
|
106
|
+
private mapSafetyRatings(
|
|
107
|
+
ratings: Array<{ category: string; probability: string }> | undefined
|
|
108
|
+
): GeminiSafetyRating[] | undefined {
|
|
109
|
+
if (!ratings) return undefined;
|
|
110
|
+
|
|
111
|
+
return ratings
|
|
112
|
+
.filter((r) => this.isValidHarmCategory(r.category) && this.isValidProbability(r.probability))
|
|
113
|
+
.map((r) => ({
|
|
114
|
+
category: r.category as GeminiHarmCategory,
|
|
115
|
+
probability: r.probability as GeminiSafetyRating["probability"],
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Validate harm category
|
|
121
|
+
*/
|
|
122
|
+
private isValidHarmCategory(value: string): value is GeminiHarmCategory {
|
|
123
|
+
return VALID_HARM_CATEGORIES.includes(value as GeminiHarmCategory);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Validate probability
|
|
128
|
+
*/
|
|
129
|
+
private isValidProbability(value: string): value is GeminiSafetyRating["probability"] {
|
|
130
|
+
return VALID_PROBABILITIES.includes(value as GeminiSafetyRating["probability"]);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Extract text from response
|
|
135
|
+
*/
|
|
136
|
+
extractText(response: GeminiResponse): string {
|
|
137
|
+
if (!response.candidates || response.candidates.length === 0) {
|
|
138
|
+
throw new Error("No response candidates");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const candidate = response.candidates[0];
|
|
142
|
+
|
|
143
|
+
// Handle finish reasons
|
|
144
|
+
if (candidate.finishReason === "SAFETY") {
|
|
145
|
+
throw new Error("Content blocked by safety filters");
|
|
146
|
+
}
|
|
147
|
+
if (candidate.finishReason === "RECITATION") {
|
|
148
|
+
throw new Error("Content blocked due to recitation concerns");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!candidate.content?.parts) {
|
|
152
|
+
throw new Error("No content in response candidate");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const textPart = candidate.content.parts.find(
|
|
156
|
+
(p): p is { text: string } => "text" in p && typeof p.text === "string"
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (!textPart) {
|
|
160
|
+
throw new Error("No text in response");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return textPart.text;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Gemini Repository
|
|
3
|
+
* Common functionality for all Gemini repositories
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { GenerativeModel } from "@google/generative-ai";
|
|
7
|
+
import type { GeminiContent, GeminiGenerationConfig } from "../../domain/entities";
|
|
8
|
+
import { ValidationService } from "../../domain/services/validation.service";
|
|
9
|
+
import { ContentMapper } from "../mappers/content.mapper";
|
|
10
|
+
import { ErrorMapper } from "../mappers/error.mapper";
|
|
11
|
+
|
|
12
|
+
export abstract class BaseGeminiRepository {
|
|
13
|
+
protected constructor(
|
|
14
|
+
protected getModel: (name: string) => GenerativeModel,
|
|
15
|
+
protected readonly validator: ValidationService,
|
|
16
|
+
protected readonly contentMapper: ContentMapper
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Prepare request parameters
|
|
21
|
+
* @returns SDK model and formatted contents
|
|
22
|
+
*/
|
|
23
|
+
protected prepareRequest(params: {
|
|
24
|
+
model: string;
|
|
25
|
+
contents: GeminiContent[];
|
|
26
|
+
generationConfig?: GeminiGenerationConfig;
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
}): {
|
|
29
|
+
genModel: GenerativeModel;
|
|
30
|
+
sdkContents: Array<{ role: string; parts: unknown[] }>;
|
|
31
|
+
config: GeminiGenerationConfig | undefined;
|
|
32
|
+
signal: AbortSignal | undefined;
|
|
33
|
+
} {
|
|
34
|
+
// Validate inputs
|
|
35
|
+
this.validator.validateModelName(params.model);
|
|
36
|
+
this.validator.validateContents(params.contents);
|
|
37
|
+
this.validator.validateConfig(params.generationConfig);
|
|
38
|
+
|
|
39
|
+
// Check abort signal
|
|
40
|
+
if (params.signal?.aborted) {
|
|
41
|
+
throw new Error("Request was aborted");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Get SDK model
|
|
45
|
+
const genModel = this.getModel(params.model);
|
|
46
|
+
|
|
47
|
+
// Convert contents to SDK format
|
|
48
|
+
const sdkContents = this.contentMapper.toSdkArray(params.contents);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
genModel,
|
|
52
|
+
sdkContents,
|
|
53
|
+
config: params.generationConfig,
|
|
54
|
+
signal: params.signal,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create request options for SDK
|
|
60
|
+
*/
|
|
61
|
+
protected createRequestOptions(
|
|
62
|
+
sdkContents: Array<{ role: string; parts: unknown[] }>,
|
|
63
|
+
generationConfig?: GeminiGenerationConfig
|
|
64
|
+
): {
|
|
65
|
+
contents: Array<{ role: string; parts: unknown[] }>;
|
|
66
|
+
generationConfig?: GeminiGenerationConfig;
|
|
67
|
+
} {
|
|
68
|
+
return {
|
|
69
|
+
contents: sdkContents,
|
|
70
|
+
generationConfig,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Handle errors consistently
|
|
76
|
+
*/
|
|
77
|
+
protected handleError(error: unknown, context: string): never {
|
|
78
|
+
throw ErrorMapper.map(error, context);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Execute with automatic error handling
|
|
83
|
+
*/
|
|
84
|
+
protected async executeWithErrorHandling<T>(
|
|
85
|
+
operation: () => Promise<T>,
|
|
86
|
+
context: string
|
|
87
|
+
): Promise<T> {
|
|
88
|
+
try {
|
|
89
|
+
return await operation();
|
|
90
|
+
} catch (error) {
|
|
91
|
+
this.handleError(error, context);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|