Skip to content

Commit ba10115

Browse files
author
Developer
committed
Integrate Google Gemini API for image analysis
1 parent 69ecb5a commit ba10115

File tree

3 files changed

+584
-0
lines changed

3 files changed

+584
-0
lines changed

.env.example

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Environment Variables Template
2+
3+
# Copy this file to .env.local and fill in your actual values
4+
# Never commit .env.local to version control
5+
6+
# Required - Google Gemini API Key
7+
# Get from: https://makersuite.google.com/app/apikey
8+
GOOGLE_API_KEY=your_google_gemini_api_key_here
9+
10+
# Optional - Environment (development/production)
11+
NODE_ENV=development
12+
13+
# Optional - Custom port (default: 3000)
14+
# PORT=3000
15+
16+
# Optional - API Configuration
17+
# API_TIMEOUT=300000
18+
19+
# Optional - File Upload Configuration
20+
# MAX_FILE_SIZE=52428800

lib/api-manager.ts

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
import { GoogleGenerativeAI } from '@google/generative-ai';
2+
3+
interface APIConfig {
4+
id: string;
5+
type: 'gemini' | 'perplexity';
6+
key: string;
7+
baseUrl?: string;
8+
maxRetries: number;
9+
timeout: number;
10+
isActive: boolean;
11+
errorCount: number;
12+
lastError?: Date;
13+
rateLimitReset?: Date;
14+
}
15+
16+
interface APIResponse {
17+
success: boolean;
18+
data?: any;
19+
error?: string;
20+
apiUsed?: string;
21+
retryAfter?: number;
22+
}
23+
24+
export interface APIStatus {
25+
id: string;
26+
type: 'gemini' | 'perplexity';
27+
isActive: boolean;
28+
errorCount: number;
29+
lastError?: string;
30+
lastErrorTime?: Date;
31+
lastSuccessTime?: Date;
32+
}
33+
34+
class APIManager {
35+
private apis: APIConfig[] = [];
36+
private currentGeminiIndex = 0;
37+
private readonly MAX_ERROR_COUNT = 3;
38+
private readonly ERROR_RESET_TIME = 5 * 60 * 1000; // 5 minutes
39+
private geminiClients: Map<string, GoogleGenerativeAI> = new Map(); // Cache AI clients
40+
private lastHealthCheck = 0;
41+
private readonly HEALTH_CHECK_INTERVAL = 60000; // 1 minute
42+
43+
constructor() {
44+
this.initializeAPIs();
45+
}
46+
47+
private initializeAPIs() {
48+
// Initialize Gemini APIs
49+
const geminiKeys = [
50+
process.env.GEMINI_API_KEY_1,
51+
process.env.GEMINI_API_KEY_2,
52+
process.env.GEMINI_API_KEY_3,
53+
].filter(Boolean);
54+
55+
geminiKeys.forEach((key, index) => {
56+
if (key) {
57+
this.apis.push({
58+
id: `gemini_${index + 1}`,
59+
type: 'gemini',
60+
key,
61+
maxRetries: 2,
62+
timeout: 30000,
63+
isActive: true,
64+
errorCount: 0,
65+
});
66+
}
67+
});
68+
69+
// Initialize Perplexity API
70+
if (process.env.PERPLEXITY_API_KEY) {
71+
this.apis.push({
72+
id: 'perplexity_1',
73+
type: 'perplexity',
74+
key: process.env.PERPLEXITY_API_KEY,
75+
baseUrl: 'https://api.perplexity.ai',
76+
maxRetries: 2,
77+
timeout: 30000,
78+
isActive: true,
79+
errorCount: 0,
80+
});
81+
}
82+
83+
console.log(`Initialized ${this.apis.length} APIs:`,
84+
this.apis.map(api => `${api.id} (${api.type})`));
85+
}
86+
87+
private resetErrorCount(api: APIConfig) {
88+
const now = new Date();
89+
if (api.lastError && (now.getTime() - api.lastError.getTime()) > this.ERROR_RESET_TIME) {
90+
api.errorCount = 0;
91+
api.isActive = true;
92+
api.lastError = undefined;
93+
}
94+
}
95+
96+
private markAPIError(api: APIConfig, error: string) {
97+
api.errorCount++;
98+
api.lastError = new Date();
99+
100+
if (api.errorCount >= this.MAX_ERROR_COUNT) {
101+
api.isActive = false;
102+
console.warn(`API ${api.id} temporarily disabled due to errors:`, error);
103+
}
104+
}
105+
106+
private getAvailableGeminiAPIs(): APIConfig[] {
107+
return this.apis
108+
.filter(api => api.type === 'gemini')
109+
.map(api => {
110+
this.resetErrorCount(api);
111+
return api;
112+
})
113+
.filter(api => api.isActive);
114+
}
115+
116+
private getAvailablePerplexityAPIs(): APIConfig[] {
117+
return this.apis
118+
.filter(api => api.type === 'perplexity')
119+
.map(api => {
120+
this.resetErrorCount(api);
121+
return api;
122+
})
123+
.filter(api => api.isActive);
124+
}
125+
126+
async analyzeImageWithGemini(imageBuffer: Buffer, mimeType: string, prompt: string): Promise<APIResponse> {
127+
const availableAPIs = this.getAvailableGeminiAPIs();
128+
129+
if (availableAPIs.length === 0) {
130+
return {
131+
success: false,
132+
error: 'No available Gemini APIs. All APIs are temporarily disabled.',
133+
};
134+
}
135+
136+
// Round-robin through available APIs
137+
for (let attempt = 0; attempt < availableAPIs.length; attempt++) {
138+
const apiIndex = (this.currentGeminiIndex + attempt) % availableAPIs.length;
139+
const api = availableAPIs[apiIndex];
140+
141+
try {
142+
console.log(`Attempting image analysis with ${api.id}...`);
143+
144+
const genAI = new GoogleGenerativeAI(api.key);
145+
// Use the basic flash model that should work
146+
const model = genAI.getGenerativeModel({
147+
model: 'models/gemini-2.0-flash',
148+
generationConfig: {
149+
maxOutputTokens: 4096,
150+
}
151+
});
152+
153+
const imagePart = {
154+
inlineData: {
155+
data: imageBuffer.toString('base64'),
156+
mimeType: mimeType,
157+
},
158+
};
159+
160+
const result = await Promise.race([
161+
model.generateContent([prompt, imagePart]),
162+
new Promise((_, reject) =>
163+
setTimeout(() => reject(new Error('Request timeout')), api.timeout)
164+
),
165+
]) as any;
166+
167+
const response = await result.response;
168+
const text = response.text();
169+
170+
// Update current index for next request
171+
this.currentGeminiIndex = (apiIndex + 1) % availableAPIs.length;
172+
173+
return {
174+
success: true,
175+
data: text,
176+
apiUsed: api.id,
177+
};
178+
179+
} catch (error: any) {
180+
console.error(`Error with ${api.id}:`, error.message);
181+
182+
// Check for rate limit errors
183+
if (error.message?.includes('quota') || error.message?.includes('limit') || error.status === 429) {
184+
api.rateLimitReset = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
185+
this.markAPIError(api, `Rate limit: ${error.message}`);
186+
} else {
187+
this.markAPIError(api, error.message);
188+
}
189+
190+
// If this is the last attempt, continue to try other APIs
191+
if (attempt === availableAPIs.length - 1) {
192+
return {
193+
success: false,
194+
error: `All Gemini APIs failed. Last error: ${error.message}`,
195+
};
196+
}
197+
}
198+
}
199+
200+
return {
201+
success: false,
202+
error: 'Unexpected error in image analysis',
203+
};
204+
}
205+
206+
async getInsightsWithPerplexity(query: string): Promise<APIResponse> {
207+
const availableAPIs = this.getAvailablePerplexityAPIs();
208+
209+
if (availableAPIs.length === 0) {
210+
return {
211+
success: false,
212+
error: 'No available Perplexity APIs',
213+
};
214+
}
215+
216+
const api = availableAPIs[0]; // Use first available Perplexity API
217+
218+
try {
219+
console.log(`Getting insights with ${api.id}...`);
220+
221+
const response = await Promise.race([
222+
fetch(`${api.baseUrl}/chat/completions`, {
223+
method: 'POST',
224+
headers: {
225+
'Authorization': `Bearer ${api.key}`,
226+
'Content-Type': 'application/json',
227+
},
228+
body: JSON.stringify({
229+
model: 'llama-3.1-sonar-large-128k-online',
230+
messages: [
231+
{
232+
role: 'system',
233+
content: 'You are a helpful research assistant. Provide detailed insights and analysis about the given topic.'
234+
},
235+
{
236+
role: 'user',
237+
content: query
238+
}
239+
],
240+
max_tokens: 2000,
241+
temperature: 0.2,
242+
top_p: 0.9,
243+
}),
244+
}),
245+
new Promise<never>((_, reject) =>
246+
setTimeout(() => reject(new Error('Request timeout')), api.timeout)
247+
),
248+
]);
249+
250+
if (!response.ok) {
251+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
252+
}
253+
254+
const data = await response.json();
255+
256+
return {
257+
success: true,
258+
data: data.choices[0]?.message?.content || 'No insights generated',
259+
apiUsed: api.id,
260+
};
261+
262+
} catch (error: any) {
263+
console.error(`Error with ${api.id}:`, error.message);
264+
265+
if (error.message?.includes('quota') || error.message?.includes('limit') || error.status === 429) {
266+
api.rateLimitReset = new Date(Date.now() + 60 * 60 * 1000);
267+
this.markAPIError(api, `Rate limit: ${error.message}`);
268+
} else {
269+
this.markAPIError(api, error.message);
270+
}
271+
272+
return {
273+
success: false,
274+
error: error.message,
275+
};
276+
}
277+
}
278+
279+
getAPIStatus(): APIStatus[] {
280+
return this.apis.map(api => ({
281+
id: api.id,
282+
type: api.type,
283+
isActive: api.isActive,
284+
errorCount: api.errorCount,
285+
lastError: api.lastError?.toString(),
286+
lastErrorTime: api.lastError,
287+
lastSuccessTime: undefined, // We'll need to track this separately
288+
}));
289+
}
290+
291+
// Method to manually reset an API
292+
resetAPI(apiId: string) {
293+
const api = this.apis.find(a => a.id === apiId);
294+
if (api) {
295+
api.errorCount = 0;
296+
api.isActive = true;
297+
api.lastError = undefined;
298+
api.rateLimitReset = undefined;
299+
console.log(`Manually reset API: ${apiId}`);
300+
}
301+
}
302+
}
303+
304+
// Singleton instance
305+
export const apiManager = new APIManager();
306+
export type { APIResponse };

0 commit comments

Comments
 (0)