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