@umituz/react-native-ai-gemini-provider 3.0.40 → 3.0.42
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 -4
- 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 -70
- 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
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
export interface TelemetryEvent {
|
|
3
|
-
type: "error";
|
|
4
|
-
timestamp: number;
|
|
5
|
-
model?: string;
|
|
6
|
-
feature?: string;
|
|
7
|
-
metadata?: Record<string, unknown>;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export type TelemetryListener = (event: TelemetryEvent) => void;
|
|
11
|
-
|
|
12
|
-
class TelemetryHooks {
|
|
13
|
-
private listeners: TelemetryListener[] = [];
|
|
14
|
-
private failedListeners: Set<TelemetryListener> = new Set();
|
|
15
|
-
private readonly MAX_FAILURES = 3;
|
|
16
|
-
private listenerFailureCounts = new Map<TelemetryListener, number>();
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Register a telemetry listener
|
|
20
|
-
*
|
|
21
|
-
* @returns Unsubscribe function - IMPORTANT: Call this when done listening to prevent memory leaks
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* ```ts
|
|
25
|
-
* const unsubscribe = telemetryHooks.subscribe((event) => console.log(event));
|
|
26
|
-
* // ... later when done
|
|
27
|
-
* unsubscribe(); // Prevents memory leak
|
|
28
|
-
* ```
|
|
29
|
-
*/
|
|
30
|
-
subscribe(listener: TelemetryListener): () => void {
|
|
31
|
-
this.listeners.push(listener);
|
|
32
|
-
// Remove from failed listeners on new subscription (in case it's being re-added)
|
|
33
|
-
this.failedListeners.delete(listener);
|
|
34
|
-
this.listenerFailureCounts.set(listener, 0);
|
|
35
|
-
|
|
36
|
-
return () => {
|
|
37
|
-
const index = this.listeners.indexOf(listener);
|
|
38
|
-
if (index > -1) {
|
|
39
|
-
this.listeners.splice(index, 1);
|
|
40
|
-
}
|
|
41
|
-
// Clean up failure tracking when listener is removed
|
|
42
|
-
this.failedListeners.delete(listener);
|
|
43
|
-
this.listenerFailureCounts.delete(listener);
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Emit a telemetry event to all listeners
|
|
49
|
-
*/
|
|
50
|
-
emit(event: TelemetryEvent): void {
|
|
51
|
-
// Snapshot to prevent mutation during iteration
|
|
52
|
-
const snapshot = [...this.listeners];
|
|
53
|
-
for (const listener of snapshot) {
|
|
54
|
-
// Skip listeners that have failed too many times
|
|
55
|
-
if (this.failedListeners.has(listener)) {
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
listener(event);
|
|
61
|
-
// Reset failure count and remove from failed listeners on success
|
|
62
|
-
const previousFailures = this.listenerFailureCounts.get(listener) || 0;
|
|
63
|
-
if (previousFailures > 0) {
|
|
64
|
-
this.listenerFailureCounts.set(listener, 0);
|
|
65
|
-
this.failedListeners.delete(listener);
|
|
66
|
-
}
|
|
67
|
-
} catch (error) {
|
|
68
|
-
// Track failures
|
|
69
|
-
const failureCount = (this.listenerFailureCounts.get(listener) || 0) + 1;
|
|
70
|
-
this.listenerFailureCounts.set(listener, failureCount);
|
|
71
|
-
|
|
72
|
-
// If listener fails too many times, blacklist it
|
|
73
|
-
if (failureCount >= this.MAX_FAILURES) {
|
|
74
|
-
this.failedListeners.add(listener);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Log error
|
|
82
|
-
*/
|
|
83
|
-
logError(model: string, error: Error, feature?: string): void {
|
|
84
|
-
this.emit({
|
|
85
|
-
type: "error",
|
|
86
|
-
timestamp: Date.now(),
|
|
87
|
-
model,
|
|
88
|
-
feature,
|
|
89
|
-
metadata: {
|
|
90
|
-
error: error.message,
|
|
91
|
-
errorType: error.name,
|
|
92
|
-
},
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Remove a specific listener
|
|
98
|
-
*/
|
|
99
|
-
unsubscribe(listener: TelemetryListener): void {
|
|
100
|
-
const index = this.listeners.indexOf(listener);
|
|
101
|
-
if (index > -1) {
|
|
102
|
-
this.listeners.splice(index, 1);
|
|
103
|
-
}
|
|
104
|
-
// Clean up failure tracking
|
|
105
|
-
this.failedListeners.delete(listener);
|
|
106
|
-
this.listenerFailureCounts.delete(listener);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export const telemetryHooks = new TelemetryHooks();
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Async State Execution Utilities
|
|
3
|
-
* Utilities for managing asynchronous operation state
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Callbacks for async operation outcomes
|
|
8
|
-
*/
|
|
9
|
-
export interface AsyncStateCallbacks<T = string> {
|
|
10
|
-
onSuccess?: (result: T) => void;
|
|
11
|
-
onError?: (error: string) => void;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Setter functions for updating state
|
|
16
|
-
*/
|
|
17
|
-
export interface AsyncStateSetters<T = string, U = unknown> {
|
|
18
|
-
setIsLoading: (value: boolean) => void;
|
|
19
|
-
setError: (value: string | null) => void;
|
|
20
|
-
setResult: (value: T | null) => void;
|
|
21
|
-
setSecondaryResult?: (value: U | null) => void;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Configuration for executeWithState
|
|
26
|
-
*/
|
|
27
|
-
export interface AsyncStateConfig<T = string> {
|
|
28
|
-
resetState?: boolean;
|
|
29
|
-
transformResult?: (result: T) => T;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Execute an async operation with automatic state management
|
|
34
|
-
*
|
|
35
|
-
* @param setters - State setter functions
|
|
36
|
-
* @param callbacks - Optional callbacks for success/error
|
|
37
|
-
* @param execute - The async operation to execute
|
|
38
|
-
* @param onResult - Function to handle successful result
|
|
39
|
-
* @param config - Optional configuration
|
|
40
|
-
*
|
|
41
|
-
* @returns The result or null if failed/aborted
|
|
42
|
-
*
|
|
43
|
-
* @example
|
|
44
|
-
* ```ts
|
|
45
|
-
* const result = await executeWithState(
|
|
46
|
-
* { setIsLoading, setError, setResult },
|
|
47
|
-
* { onSuccess: console.log },
|
|
48
|
-
* () => apiCall(),
|
|
49
|
-
* (data) => setResult(data)
|
|
50
|
-
* );
|
|
51
|
-
* ```
|
|
52
|
-
*/
|
|
53
|
-
export async function executeWithState<T, U = unknown>(
|
|
54
|
-
setters: AsyncStateSetters<T, U>,
|
|
55
|
-
callbacks: AsyncStateCallbacks<T>,
|
|
56
|
-
execute: () => Promise<T>,
|
|
57
|
-
onResult: (result: T) => void,
|
|
58
|
-
config: AsyncStateConfig<T> = {},
|
|
59
|
-
): Promise<T | null> {
|
|
60
|
-
const {
|
|
61
|
-
resetState = true,
|
|
62
|
-
transformResult,
|
|
63
|
-
} = config;
|
|
64
|
-
|
|
65
|
-
if (resetState) {
|
|
66
|
-
setters.setError(null);
|
|
67
|
-
setters.setResult(null);
|
|
68
|
-
setters.setSecondaryResult?.(null);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
setters.setIsLoading(true);
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
const result = await execute();
|
|
75
|
-
|
|
76
|
-
// Apply transformation if provided
|
|
77
|
-
const finalResult = transformResult ? transformResult(result) : result;
|
|
78
|
-
|
|
79
|
-
onResult(finalResult);
|
|
80
|
-
callbacks.onSuccess?.(finalResult);
|
|
81
|
-
|
|
82
|
-
return finalResult;
|
|
83
|
-
} catch (err) {
|
|
84
|
-
const errorMessage = err instanceof Error ? err.message : "Operation failed";
|
|
85
|
-
|
|
86
|
-
setters.setError(errorMessage);
|
|
87
|
-
callbacks.onError?.(errorMessage);
|
|
88
|
-
|
|
89
|
-
return null;
|
|
90
|
-
} finally {
|
|
91
|
-
setters.setIsLoading(false);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Content Mapper Utilities
|
|
3
|
-
* Handles transformation between domain content and SDK format
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { Part } from "@google/generative-ai";
|
|
7
|
-
import type {
|
|
8
|
-
GeminiContent,
|
|
9
|
-
GeminiPart,
|
|
10
|
-
GeminiFinishReason,
|
|
11
|
-
GeminiSafetyRating,
|
|
12
|
-
GeminiResponse,
|
|
13
|
-
GeminiHarmCategory,
|
|
14
|
-
} from "../../domain/entities";
|
|
15
|
-
|
|
16
|
-
const VALID_FINISH_REASONS: readonly string[] = [
|
|
17
|
-
"FINISH_REASON_UNSPECIFIED",
|
|
18
|
-
"STOP",
|
|
19
|
-
"MAX_TOKENS",
|
|
20
|
-
"SAFETY",
|
|
21
|
-
"RECITATION",
|
|
22
|
-
"OTHER",
|
|
23
|
-
] as const;
|
|
24
|
-
|
|
25
|
-
const VALID_HARM_CATEGORIES: readonly string[] = [
|
|
26
|
-
"HARM_CATEGORY_HARASSMENT",
|
|
27
|
-
"HARM_CATEGORY_HATE_SPEECH",
|
|
28
|
-
"HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
|
29
|
-
"HARM_CATEGORY_DANGEROUS_CONTENT",
|
|
30
|
-
] as const;
|
|
31
|
-
|
|
32
|
-
const VALID_PROBABILITIES: readonly string[] = [
|
|
33
|
-
"NEGLIGIBLE",
|
|
34
|
-
"LOW",
|
|
35
|
-
"MEDIUM",
|
|
36
|
-
"HIGH",
|
|
37
|
-
] as const;
|
|
38
|
-
|
|
39
|
-
function isValidFinishReason(value: string): value is GeminiFinishReason {
|
|
40
|
-
return VALID_FINISH_REASONS.includes(value);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function isValidHarmCategory(value: string): value is GeminiHarmCategory {
|
|
44
|
-
return VALID_HARM_CATEGORIES.includes(value);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function isValidProbability(value: string): value is GeminiSafetyRating["probability"] {
|
|
48
|
-
return VALID_PROBABILITIES.includes(value);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Convert domain content to SDK format
|
|
53
|
-
* Preserves both text and inlineData parts
|
|
54
|
-
*/
|
|
55
|
-
export function toSdkContent(contents: GeminiContent[]): Array<{
|
|
56
|
-
role: string;
|
|
57
|
-
parts: Part[];
|
|
58
|
-
}> {
|
|
59
|
-
return contents.map((content) => ({
|
|
60
|
-
role: content.role || "user",
|
|
61
|
-
parts: content.parts.map((part) => {
|
|
62
|
-
if ("text" in part) {
|
|
63
|
-
return { text: part.text } as Part;
|
|
64
|
-
}
|
|
65
|
-
// For inlineData and other part types, cast to SDK Part type
|
|
66
|
-
return part as Part;
|
|
67
|
-
}),
|
|
68
|
-
}));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Create a simple text content
|
|
73
|
-
*/
|
|
74
|
-
export function createTextContent(
|
|
75
|
-
text: string,
|
|
76
|
-
role: "user" | "model" = "user"
|
|
77
|
-
): GeminiContent {
|
|
78
|
-
return {
|
|
79
|
-
parts: [{ text }],
|
|
80
|
-
role,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Transform SDK candidate to domain format
|
|
86
|
-
*/
|
|
87
|
-
function transformCandidate(
|
|
88
|
-
candidate: {
|
|
89
|
-
content: { parts: Array<{ text?: string }>; role?: string };
|
|
90
|
-
finishReason?: string;
|
|
91
|
-
safetyRatings?: Array<{
|
|
92
|
-
category: string;
|
|
93
|
-
probability: string;
|
|
94
|
-
}>;
|
|
95
|
-
}
|
|
96
|
-
): {
|
|
97
|
-
content: GeminiContent;
|
|
98
|
-
finishReason?: GeminiFinishReason;
|
|
99
|
-
safetyRatings?: GeminiSafetyRating[];
|
|
100
|
-
} {
|
|
101
|
-
const transformedParts: GeminiPart[] = [];
|
|
102
|
-
|
|
103
|
-
for (const part of candidate.content.parts) {
|
|
104
|
-
if ("text" in part && typeof part.text === "string") {
|
|
105
|
-
transformedParts.push({ text: part.text });
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const finishReason: GeminiFinishReason | undefined =
|
|
110
|
-
candidate.finishReason && isValidFinishReason(candidate.finishReason)
|
|
111
|
-
? candidate.finishReason
|
|
112
|
-
: undefined;
|
|
113
|
-
|
|
114
|
-
const safetyRatings: GeminiSafetyRating[] | undefined = candidate.safetyRatings
|
|
115
|
-
? candidate.safetyRatings
|
|
116
|
-
.filter((rating) => isValidHarmCategory(rating.category) && isValidProbability(rating.probability))
|
|
117
|
-
.map((rating) => ({
|
|
118
|
-
category: rating.category as GeminiHarmCategory,
|
|
119
|
-
probability: rating.probability as GeminiSafetyRating["probability"],
|
|
120
|
-
}))
|
|
121
|
-
: undefined;
|
|
122
|
-
|
|
123
|
-
const role = candidate.content.role === "user" || candidate.content.role === "model"
|
|
124
|
-
? candidate.content.role
|
|
125
|
-
: "model";
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
content: {
|
|
129
|
-
parts: transformedParts,
|
|
130
|
-
role,
|
|
131
|
-
},
|
|
132
|
-
finishReason,
|
|
133
|
-
safetyRatings,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Transform SDK response to domain format
|
|
139
|
-
*/
|
|
140
|
-
export function transformResponse(
|
|
141
|
-
response: {
|
|
142
|
-
candidates?: Array<{
|
|
143
|
-
content: { parts: Array<{ text?: string }>; role?: string };
|
|
144
|
-
finishReason?: string;
|
|
145
|
-
safetyRatings?: Array<{
|
|
146
|
-
category: string;
|
|
147
|
-
probability: string;
|
|
148
|
-
}>;
|
|
149
|
-
}>;
|
|
150
|
-
usageMetadata?: {
|
|
151
|
-
promptTokenCount?: number;
|
|
152
|
-
candidatesTokenCount?: number;
|
|
153
|
-
totalTokenCount?: number;
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
): GeminiResponse {
|
|
157
|
-
return {
|
|
158
|
-
candidates: response.candidates?.map(transformCandidate),
|
|
159
|
-
usageMetadata: response.usageMetadata ? {
|
|
160
|
-
promptTokenCount: response.usageMetadata.promptTokenCount,
|
|
161
|
-
candidatesTokenCount: response.usageMetadata.candidatesTokenCount,
|
|
162
|
-
totalTokenCount: response.usageMetadata.totalTokenCount,
|
|
163
|
-
} : undefined,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Extract text from content parts
|
|
169
|
-
*/
|
|
170
|
-
export function extractTextFromParts(parts: GeminiPart[] | undefined): string {
|
|
171
|
-
if (!parts || parts.length === 0) return "";
|
|
172
|
-
return parts
|
|
173
|
-
.map((part) => ("text" in part ? (part.text || "") : ""))
|
|
174
|
-
.join("");
|
|
175
|
-
}
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
GeminiErrorType,
|
|
4
|
-
type GeminiErrorInfo,
|
|
5
|
-
type GeminiApiError,
|
|
6
|
-
GeminiError,
|
|
7
|
-
} from "../../domain/entities";
|
|
8
|
-
|
|
9
|
-
const ERROR_PATTERNS: Array<{
|
|
10
|
-
pattern: RegExp | string[];
|
|
11
|
-
type: GeminiErrorType;
|
|
12
|
-
retryable: boolean;
|
|
13
|
-
}> = [
|
|
14
|
-
{
|
|
15
|
-
pattern: ["quota", "resource exhausted", "429"],
|
|
16
|
-
type: GeminiErrorType.QUOTA_EXCEEDED,
|
|
17
|
-
retryable: true,
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
pattern: ["rate limit", "too many requests"],
|
|
21
|
-
type: GeminiErrorType.RATE_LIMIT,
|
|
22
|
-
retryable: true,
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
pattern: ["unauthorized", "invalid api key", "401", "403", "permission"],
|
|
26
|
-
type: GeminiErrorType.AUTHENTICATION,
|
|
27
|
-
retryable: false,
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
pattern: ["safety", "safety filter", "harmful", "blocked by safety"],
|
|
31
|
-
type: GeminiErrorType.SAFETY,
|
|
32
|
-
retryable: false,
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
pattern: ["model not found", "404", "not found"],
|
|
36
|
-
type: GeminiErrorType.MODEL_NOT_FOUND,
|
|
37
|
-
retryable: false,
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
pattern: ["network", "fetch failed", "connection", "socket"],
|
|
41
|
-
type: GeminiErrorType.NETWORK,
|
|
42
|
-
retryable: true,
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
pattern: ["timeout", "timed out"],
|
|
46
|
-
type: GeminiErrorType.TIMEOUT,
|
|
47
|
-
retryable: true,
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
pattern: ["500", "502", "503", "504", "internal server", "unavailable"],
|
|
51
|
-
type: GeminiErrorType.SERVER,
|
|
52
|
-
retryable: true,
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
pattern: ["invalid", "bad request", "400"],
|
|
56
|
-
type: GeminiErrorType.VALIDATION,
|
|
57
|
-
retryable: false,
|
|
58
|
-
},
|
|
59
|
-
];
|
|
60
|
-
|
|
61
|
-
function getStatusCode(error: unknown): number | undefined {
|
|
62
|
-
if (error && typeof error === "object") {
|
|
63
|
-
const err = error as Record<string, unknown>;
|
|
64
|
-
if (typeof err.status === "number") return err.status;
|
|
65
|
-
if (typeof err.statusCode === "number") return err.statusCode;
|
|
66
|
-
|
|
67
|
-
const response = err.response as GeminiApiError | undefined;
|
|
68
|
-
if (response?.error?.code) return response.error.code;
|
|
69
|
-
}
|
|
70
|
-
return undefined;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function matchesPattern(message: string, patterns: string[]): boolean {
|
|
74
|
-
const lower = message.toLowerCase();
|
|
75
|
-
|
|
76
|
-
return patterns.some((pattern) => {
|
|
77
|
-
const lowerPattern = pattern.toLowerCase();
|
|
78
|
-
|
|
79
|
-
// Use word boundary matching for accuracy
|
|
80
|
-
// This prevents "invalid" from matching "valid"
|
|
81
|
-
const words = lowerPattern.split(/\s+/);
|
|
82
|
-
return words.every((word) => {
|
|
83
|
-
const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
84
|
-
return new RegExp(`\\b${escaped}\\b`, 'i').test(lower);
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Map HTTP status codes to error types as a primary classification signal */
|
|
90
|
-
const STATUS_CODE_MAP: Record<number, { type: GeminiErrorType; retryable: boolean }> = {
|
|
91
|
-
400: { type: GeminiErrorType.VALIDATION, retryable: false },
|
|
92
|
-
401: { type: GeminiErrorType.AUTHENTICATION, retryable: false },
|
|
93
|
-
403: { type: GeminiErrorType.AUTHENTICATION, retryable: false },
|
|
94
|
-
404: { type: GeminiErrorType.MODEL_NOT_FOUND, retryable: false },
|
|
95
|
-
429: { type: GeminiErrorType.RATE_LIMIT, retryable: true },
|
|
96
|
-
500: { type: GeminiErrorType.SERVER, retryable: true },
|
|
97
|
-
502: { type: GeminiErrorType.SERVER, retryable: true },
|
|
98
|
-
503: { type: GeminiErrorType.SERVER, retryable: true },
|
|
99
|
-
504: { type: GeminiErrorType.SERVER, retryable: true },
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
function mapGeminiError(error: unknown): GeminiErrorInfo {
|
|
103
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
104
|
-
const statusCode = getStatusCode(error);
|
|
105
|
-
|
|
106
|
-
// Primary: classify by HTTP status code when available
|
|
107
|
-
if (statusCode && STATUS_CODE_MAP[statusCode]) {
|
|
108
|
-
const { type, retryable } = STATUS_CODE_MAP[statusCode];
|
|
109
|
-
return {
|
|
110
|
-
type,
|
|
111
|
-
messageKey: `error.gemini.${type.toLowerCase()}`,
|
|
112
|
-
retryable,
|
|
113
|
-
originalError: error,
|
|
114
|
-
statusCode,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Secondary: classify by error message pattern
|
|
119
|
-
for (const { pattern, type, retryable } of ERROR_PATTERNS) {
|
|
120
|
-
const patterns = Array.isArray(pattern) ? pattern : [pattern.source];
|
|
121
|
-
|
|
122
|
-
if (matchesPattern(message, patterns)) {
|
|
123
|
-
return {
|
|
124
|
-
type,
|
|
125
|
-
messageKey: `error.gemini.${type.toLowerCase()}`,
|
|
126
|
-
retryable,
|
|
127
|
-
originalError: error,
|
|
128
|
-
statusCode,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
type: GeminiErrorType.UNKNOWN,
|
|
135
|
-
messageKey: "error.gemini.unknown",
|
|
136
|
-
retryable: false,
|
|
137
|
-
originalError: error,
|
|
138
|
-
statusCode,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function createGeminiError(error: unknown): GeminiError {
|
|
143
|
-
const errorInfo = mapGeminiError(error);
|
|
144
|
-
return GeminiError.fromError(error, errorInfo);
|
|
145
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import type { GeminiResponse } from "../../domain/entities";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export function extractTextFromResponse(response: GeminiResponse): string {
|
|
6
|
-
if (!response.candidates || response.candidates.length === 0) {
|
|
7
|
-
throw new Error("No response candidates");
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const candidate = response.candidates[0];
|
|
11
|
-
|
|
12
|
-
// Handle all finish reasons appropriately
|
|
13
|
-
switch (candidate.finishReason) {
|
|
14
|
-
case "SAFETY":
|
|
15
|
-
throw new Error("Content blocked by safety filters");
|
|
16
|
-
case "RECITATION":
|
|
17
|
-
throw new Error("Content blocked due to recitation concerns");
|
|
18
|
-
case "MAX_TOKENS":
|
|
19
|
-
case "FINISH_REASON_UNSPECIFIED":
|
|
20
|
-
case "OTHER":
|
|
21
|
-
case "STOP":
|
|
22
|
-
case undefined:
|
|
23
|
-
// Continue to extract text
|
|
24
|
-
break;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (!candidate.content?.parts) {
|
|
28
|
-
throw new Error("No content in response candidate");
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const textPart = candidate.content.parts.find(
|
|
32
|
-
(p): p is { text: string } => "text" in p && typeof p.text === "string",
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
if (!textPart) {
|
|
36
|
-
throw new Error("No text in response");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return textPart.text;
|
|
40
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Text Calculation Utilities
|
|
3
|
-
* Reusable functions for text length and character budget calculations
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Calculate total character count in an array of text objects
|
|
8
|
-
* @param items - Array of objects with text content
|
|
9
|
-
* @param getText - Function to extract text from each item
|
|
10
|
-
* @returns Total character count
|
|
11
|
-
*/
|
|
12
|
-
export function calculateTotalChars<T>(
|
|
13
|
-
items: readonly T[],
|
|
14
|
-
getItemLength: (item: T) => number
|
|
15
|
-
): number {
|
|
16
|
-
return items.reduce((sum, item) => sum + getItemLength(item), 0);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Check if adding an item would exceed the character budget
|
|
21
|
-
* @param currentTotal - Current total character count
|
|
22
|
-
* @param itemLength - Length of item to add
|
|
23
|
-
* @param maxBudget - Maximum allowed budget
|
|
24
|
-
* @returns true if within budget, false otherwise
|
|
25
|
-
*/
|
|
26
|
-
export function fitsWithinBudget(
|
|
27
|
-
currentTotal: number,
|
|
28
|
-
itemLength: number,
|
|
29
|
-
maxBudget: number
|
|
30
|
-
): boolean {
|
|
31
|
-
return currentTotal + itemLength <= maxBudget;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Trim array to fit within character budget while keeping minimum items
|
|
36
|
-
* Keeps the last `minItems` regardless of budget, then adds as many as possible from the end
|
|
37
|
-
*
|
|
38
|
-
* @param items - Array to trim
|
|
39
|
-
* @param getItemLength - Function to get length of each item
|
|
40
|
-
* @param maxBudget - Maximum total length allowed
|
|
41
|
-
* @param minItems - Minimum number of items to keep (from the end)
|
|
42
|
-
* @returns Trimmed array
|
|
43
|
-
*/
|
|
44
|
-
export function trimArrayByCharBudget<T>(
|
|
45
|
-
items: readonly T[],
|
|
46
|
-
getItemLength: (item: T) => number,
|
|
47
|
-
maxBudget: number,
|
|
48
|
-
minItems: number
|
|
49
|
-
): T[] {
|
|
50
|
-
if (items.length <= minItems) return [...items];
|
|
51
|
-
|
|
52
|
-
// First, guarantee minimum items (from the end)
|
|
53
|
-
const guaranteedMin = items.slice(-minItems);
|
|
54
|
-
const remaining = items.slice(0, -minItems);
|
|
55
|
-
|
|
56
|
-
// Calculate current total
|
|
57
|
-
let totalChars = calculateTotalChars(guaranteedMin, getItemLength);
|
|
58
|
-
const trimmed: T[] = [...guaranteedMin];
|
|
59
|
-
|
|
60
|
-
// Add older items in reverse order until budget is exceeded
|
|
61
|
-
for (let i = remaining.length - 1; i >= 0; i--) {
|
|
62
|
-
const itemLength = getItemLength(remaining[i]);
|
|
63
|
-
if (!fitsWithinBudget(totalChars, itemLength, maxBudget)) break;
|
|
64
|
-
|
|
65
|
-
trimmed.unshift(remaining[i]);
|
|
66
|
-
totalChars += itemLength;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return trimmed;
|
|
70
|
-
}
|