@@ -106,6 +106,12 @@ class RateLimiter {
106106class Ruixen {
107107 private apiKey = '' ;
108108 private model = 'openai/gpt-oss-120b:free' ;
109+ private fallbackModels : string [ ] = [
110+ 'openai/gpt-oss-120b:free' ,
111+ 'deepseek/deepseek-chat-v3.1:free' ,
112+ 'deepseek/deepseek-r1:free' ,
113+ 'meta-llama/llama-3.3-70b-instruct:free' ,
114+ ] ;
109115 private analyzer : AIAnalyzer | null = null ;
110116 private limiter = new RateLimiter ( ) ;
111117 private queue : QueueTask [ ] = [ ] ;
@@ -136,24 +142,11 @@ class Ruixen {
136142 return ;
137143 }
138144
139- // Try immediate if allowed
145+ // Try immediate if allowed (with fallback model sequence on 429)
140146 if ( this . limiter . canRequest ( this . model ) ) {
141147 this . limiter . record ( this . model ) ;
142148 this . dailyUsage . set ( loadDailyCount ( ) . count ) ;
143- this . analyzer
144- . analyzeJournalEntry ( pet , fullEntry )
145- . then ( resolve )
146- . catch ( ( e ) => {
147- console . error ( 'Ruixen immediate analysis failed:' , e ) ;
148- const msg = String ( ( e as Error ) ?. message || e ) ;
149- if ( msg . includes ( '429' ) || / R a t e l i m i t e x c e e d e d / i. test ( msg ) ) {
150- const today = new Date ( ) . toDateString ( ) ;
151- setExhausted ( today ) ;
152- saveDailyCount ( { date : today , count : DAILY_FREE_LIMIT } ) ;
153- this . dailyUsage . set ( DAILY_FREE_LIMIT ) ;
154- }
155- resolve ( this . offlineHeuristic ( pet , fullEntry ) ) ;
156- } ) ;
149+ this . tryModelsSequentially ( fullEntry , pet ) . then ( resolve ) ;
157150 return ;
158151 }
159152
@@ -225,26 +218,11 @@ class Ruixen {
225218
226219 this . limiter . record ( this . model ) ;
227220 this . dailyUsage . set ( loadDailyCount ( ) . count ) ;
228- try {
229- const res = await this . analyzer . analyzeJournalEntry (
230- next . pet ,
231- this . toJournalEntry ( next . entry , next . pet )
232- ) ;
233- next . resolve ( res ) ;
234- } catch ( e ) {
235- console . error ( 'Ruixen queued analysis failed:' , e ) ;
236- const msg = String ( ( e as Error ) ?. message || e ) ;
237- if ( msg . includes ( '429' ) || / R a t e l i m i t e x c e e d e d / i. test ( msg ) ) {
238- const today = new Date ( ) . toDateString ( ) ;
239- setExhausted ( today ) ;
240- saveDailyCount ( { date : today , count : DAILY_FREE_LIMIT } ) ;
241- this . dailyUsage . set ( DAILY_FREE_LIMIT ) ;
242- }
243- next . resolve ( this . offlineHeuristic ( next . pet , this . toJournalEntry ( next . entry , next . pet ) ) ) ;
244- } finally {
245- this . queue . shift ( ) ;
246- this . queueSize . set ( this . queue . length ) ;
247- }
221+ const journalEntry = this . toJournalEntry ( next . entry , next . pet ) ;
222+ const res = await this . tryModelsSequentially ( journalEntry , next . pet ) ;
223+ next . resolve ( res ) ;
224+ this . queue . shift ( ) ;
225+ this . queueSize . set ( this . queue . length ) ;
248226 }
249227 this . running = false ;
250228 } ;
@@ -417,6 +395,41 @@ class Ruixen {
417395 } ,
418396 ] ;
419397 }
398+
399+ /**
400+ * Attempt analysis across fallback free models sequentially.
401+ * On 429 from a model, tries the next. Other errors short-circuit to offline heuristic.
402+ * If all fail with 429, mark day exhausted and return offline heuristic.
403+ */
404+ private async tryModelsSequentially (
405+ entry : JournalEntry ,
406+ pet : PetPanelData
407+ ) : Promise < AnalysisResult > {
408+ if ( ! this . analyzer ) return this . offlineHeuristic ( pet , entry ) ;
409+ for ( let i = 0 ; i < this . fallbackModels . length ; i ++ ) {
410+ const model = this . fallbackModels [ i ] ;
411+ try {
412+ const res = await this . analyzer . analyzeJournalEntry ( pet , entry , model ) ;
413+ return res ;
414+ } catch ( e ) {
415+ const msg = String ( ( e as Error ) ?. message || e ) ;
416+ const is429 = / 4 2 9 | R a t e l i m i t e x c e e d e d / i. test ( msg ) ;
417+ if ( ! is429 ) {
418+ console . warn ( 'Model attempt failed (non-429), aborting fallbacks:' , model , e ) ;
419+ return this . offlineHeuristic ( pet , entry ) ;
420+ }
421+ console . warn ( 'Model rate-limited, trying next free model:' , model ) ;
422+ if ( i === this . fallbackModels . length - 1 ) {
423+ const today = new Date ( ) . toDateString ( ) ;
424+ setExhausted ( today ) ;
425+ saveDailyCount ( { date : today , count : DAILY_FREE_LIMIT } ) ;
426+ this . dailyUsage . set ( DAILY_FREE_LIMIT ) ;
427+ return this . offlineHeuristic ( pet , entry ) ;
428+ }
429+ }
430+ }
431+ return this . offlineHeuristic ( pet , entry ) ;
432+ }
420433}
421434
422435export const ruixen = new Ruixen ( ) ;
0 commit comments