@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.
Files changed (89) hide show
  1. package/package.json +1 -1
  2. package/src/application/builders/config-builder.ts +102 -0
  3. package/src/application/builders/index.ts +8 -0
  4. package/src/application/dtos/generation-request.dto.ts +89 -0
  5. package/src/application/dtos/index.ts +8 -0
  6. package/src/application/index.ts +16 -0
  7. package/src/application/providers/gemini-provider.ts +135 -0
  8. package/src/application/providers/index.ts +6 -0
  9. package/src/application/use-cases/generate-json.use-case.ts +73 -0
  10. package/src/application/use-cases/generate-text.use-case.ts +81 -0
  11. package/src/application/use-cases/index.ts +20 -0
  12. package/src/application/use-cases/stream-content.use-case.ts +46 -0
  13. package/src/domain/entities/error.types.ts +0 -5
  14. package/src/domain/entities/gemini.types.ts +3 -1
  15. package/src/domain/index.ts +16 -0
  16. package/src/domain/repositories/index.ts +19 -0
  17. package/src/domain/repositories/streaming.repository.ts +41 -0
  18. package/src/domain/repositories/structured-text.repository.ts +41 -0
  19. package/src/domain/repositories/text-generation.repository.ts +38 -0
  20. package/src/domain/services/validation.service.ts +157 -0
  21. package/src/domain/value-objects/api-key.vo.ts +55 -0
  22. package/src/domain/value-objects/index.ts +8 -0
  23. package/src/domain/value-objects/model-name.vo.ts +66 -0
  24. package/src/domain/value-objects/timeout.vo.ts +69 -0
  25. package/src/index.ts +110 -25
  26. package/src/infrastructure/external/gemini-client.singleton.ts +49 -0
  27. package/src/infrastructure/external/gemini-sdk.adapter.ts +143 -0
  28. package/src/infrastructure/external/index.ts +7 -0
  29. package/src/infrastructure/index.ts +16 -0
  30. package/src/infrastructure/mappers/content.mapper.ts +80 -0
  31. package/src/infrastructure/mappers/error.mapper.ts +152 -0
  32. package/src/infrastructure/mappers/index.ts +7 -0
  33. package/src/infrastructure/mappers/response.mapper.ts +165 -0
  34. package/src/infrastructure/repositories/base-gemini.repository.ts +94 -0
  35. package/src/infrastructure/repositories/gemini-streaming.repository.impl.ts +119 -0
  36. package/src/infrastructure/repositories/gemini-structured-text.repository.impl.ts +108 -0
  37. package/src/infrastructure/repositories/gemini-text.repository.impl.ts +76 -0
  38. package/src/infrastructure/repositories/index.ts +10 -0
  39. package/src/infrastructure/utils/index.ts +6 -0
  40. package/src/presentation/hooks/index.ts +8 -0
  41. package/src/presentation/hooks/use-gemini.hook.ts +181 -0
  42. package/src/presentation/hooks/use-operation-manager.hook.ts +67 -0
  43. package/src/presentation/index.ts +10 -0
  44. package/src/presentation/providers/gemini-provider.tsx +93 -0
  45. package/src/presentation/providers/index.ts +10 -0
  46. package/dist/domain/entities/error.types.d.ts +0 -96
  47. package/dist/domain/entities/gemini.types.d.ts +0 -128
  48. package/dist/domain/entities/index.d.ts +0 -6
  49. package/dist/domain/entities/models.d.ts +0 -23
  50. package/dist/index.d.ts +0 -15
  51. package/dist/infrastructure/services/BaseService.d.ts +0 -29
  52. package/dist/infrastructure/services/ChatSession.d.ts +0 -63
  53. package/dist/infrastructure/services/GeminiClient.d.ts +0 -16
  54. package/dist/infrastructure/services/GeminiProvider.d.ts +0 -10
  55. package/dist/infrastructure/services/Streaming.d.ts +0 -7
  56. package/dist/infrastructure/services/StructuredText.d.ts +0 -6
  57. package/dist/infrastructure/services/TextGeneration.d.ts +0 -8
  58. package/dist/infrastructure/services/index.d.ts +0 -6
  59. package/dist/infrastructure/telemetry/TelemetryHooks.d.ts +0 -41
  60. package/dist/infrastructure/telemetry/index.d.ts +0 -4
  61. package/dist/infrastructure/utils/async/execute-state.util.d.ts +0 -49
  62. package/dist/infrastructure/utils/async/index.d.ts +0 -4
  63. package/dist/infrastructure/utils/content-mapper.util.d.ts +0 -45
  64. package/dist/infrastructure/utils/error-mapper.util.d.ts +0 -2
  65. package/dist/infrastructure/utils/gemini-data-transformer.util.d.ts +0 -2
  66. package/dist/infrastructure/utils/json-parser.util.d.ts +0 -9
  67. package/dist/infrastructure/utils/stream-processor.util.d.ts +0 -14
  68. package/dist/presentation/hooks/index.d.ts +0 -1
  69. package/dist/presentation/hooks/useGemini.d.ts +0 -17
  70. package/dist/presentation/hooks/useOperationManager.d.ts +0 -23
  71. package/dist/providers/ConfigBuilder.d.ts +0 -46
  72. package/dist/providers/ProviderFactory.d.ts +0 -25
  73. package/dist/providers/index.d.ts +0 -7
  74. package/src/infrastructure/services/BaseService.ts +0 -53
  75. package/src/infrastructure/services/ChatSession.ts +0 -199
  76. package/src/infrastructure/services/GeminiClient.ts +0 -112
  77. package/src/infrastructure/services/Streaming.ts +0 -56
  78. package/src/infrastructure/services/StructuredText.ts +0 -57
  79. package/src/infrastructure/services/TextGeneration.ts +0 -57
  80. package/src/infrastructure/telemetry/TelemetryHooks.ts +0 -110
  81. package/src/infrastructure/utils/async/execute-state.util.ts +0 -93
  82. package/src/infrastructure/utils/content-mapper.util.ts +0 -175
  83. package/src/infrastructure/utils/error-mapper.util.ts +0 -145
  84. package/src/infrastructure/utils/gemini-data-transformer.util.ts +0 -40
  85. package/src/infrastructure/utils/text-calculations.util.ts +0 -51
  86. package/src/presentation/hooks/useGemini.ts +0 -125
  87. package/src/presentation/hooks/useOperationManager.ts +0 -88
  88. package/src/providers/ConfigBuilder.ts +0 -112
  89. 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,7 @@
1
+ /**
2
+ * Infrastructure External
3
+ * SDK adapter and client
4
+ */
5
+
6
+ export { GeminiSDKAdapter } from "./gemini-sdk.adapter";
7
+ export { GeminiClient, geminiClient } from "./gemini-client.singleton";
@@ -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,7 @@
1
+ /**
2
+ * Infrastructure Mappers
3
+ */
4
+
5
+ export { ContentMapper } from "./content.mapper";
6
+ export { ResponseMapper } from "./response.mapper";
7
+ export { ErrorMapper } from "./error.mapper";
@@ -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
+ }