aisbf 0.2.3__py3-none-any.whl → 0.2.5__py3-none-any.whl

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.
aisbf/__init__.py CHANGED
@@ -43,7 +43,7 @@ from .providers import (
43
43
  )
44
44
  from .handlers import RequestHandler, RotationHandler, AutoselectHandler
45
45
 
46
- __version__ = "0.2.0"
46
+ __version__ = "0.2.5"
47
47
  __all__ = [
48
48
  # Config
49
49
  "config",
aisbf/config.py CHANGED
@@ -163,6 +163,26 @@ class Config:
163
163
  data = json.load(f)
164
164
  self.rotations = {k: RotationConfig(**v) for k, v in data['rotations'].items()}
165
165
  logger.info(f"Loaded {len(self.rotations)} rotations: {list(self.rotations.keys())}")
166
+
167
+ # Validate that all providers referenced in rotations exist
168
+ logger.info(f"=== VALIDATING ROTATION PROVIDERS ===")
169
+ available_providers = list(self.providers.keys())
170
+ logger.info(f"Available providers: {available_providers}")
171
+
172
+ for rotation_id, rotation_config in self.rotations.items():
173
+ logger.info(f"Validating rotation: {rotation_id}")
174
+ for provider in rotation_config.providers:
175
+ provider_id = provider['provider_id']
176
+ if provider_id not in self.providers:
177
+ logger.warning(f"!!! CONFIGURATION WARNING !!!")
178
+ logger.warning(f"Rotation '{rotation_id}' references provider '{provider_id}' which is NOT defined in providers.json")
179
+ logger.warning(f"Available providers: {available_providers}")
180
+ logger.warning(f"This provider will be SKIPPED during rotation requests")
181
+ logger.warning(f"Please add the provider to providers.json or remove it from the rotation configuration")
182
+ logger.warning(f"!!! END WARNING !!!")
183
+ else:
184
+ logger.info(f" ✓ Provider '{provider_id}' is available")
185
+
166
186
  logger.info(f"=== Config._load_rotations END ===")
167
187
 
168
188
  def _load_autoselect(self):
aisbf/handlers.py CHANGED
@@ -164,71 +164,201 @@ class RotationHandler:
164
164
  logger.error(f"Rotation {rotation_id} not found")
165
165
  raise HTTPException(status_code=400, detail=f"Rotation {rotation_id} not found")
166
166
 
167
- logger.info(f"Rotation config: {rotation_config}")
167
+ logger.info(f"Rotation config loaded successfully")
168
168
  providers = rotation_config.providers
169
169
  logger.info(f"Number of providers in rotation: {len(providers)}")
170
170
 
171
- weighted_models = []
171
+ # Collect all available models with their weights
172
+ available_models = []
173
+ skipped_providers = []
174
+ total_models_considered = 0
172
175
 
176
+ logger.info(f"=== MODEL SELECTION PROCESS START ===")
177
+ logger.info(f"Scanning providers for available models...")
178
+
173
179
  for provider in providers:
174
- logger.info(f"Processing provider: {provider['provider_id']}")
180
+ provider_id = provider['provider_id']
181
+ logger.info(f"")
182
+ logger.info(f"--- Processing provider: {provider_id} ---")
183
+
184
+ # Check if provider exists in configuration
185
+ provider_config = self.config.get_provider(provider_id)
186
+ if not provider_config:
187
+ logger.error(f" [ERROR] Provider {provider_id} not found in providers configuration")
188
+ logger.error(f" Available providers: {list(self.config.providers.keys())}")
189
+ logger.error(f" Skipping this provider")
190
+ skipped_providers.append(provider_id)
191
+ continue
192
+
193
+ # Check if provider is rate limited/deactivated
194
+ provider_handler = get_provider_handler(provider_id, provider.get('api_key'))
195
+ if provider_handler.is_rate_limited():
196
+ logger.warning(f" [SKIPPED] Provider {provider_id} is rate limited/deactivated")
197
+ logger.warning(f" Reason: Provider has exceeded failure threshold or is in cooldown period")
198
+ skipped_providers.append(provider_id)
199
+ continue
200
+
201
+ logger.info(f" [AVAILABLE] Provider {provider_id} is active and ready")
202
+
203
+ models_in_provider = len(provider['models'])
204
+ total_models_considered += models_in_provider
205
+ logger.info(f" Found {models_in_provider} model(s) in this provider")
206
+
175
207
  for model in provider['models']:
176
- logger.info(f" Model: {model['name']}, weight: {model['weight']}")
177
- weighted_models.extend([model] * model['weight'])
178
-
179
- logger.info(f"Total weighted models: {len(weighted_models)}")
180
- if not weighted_models:
181
- logger.error("No models available in rotation")
182
- raise HTTPException(status_code=400, detail="No models available in rotation")
208
+ model_name = model['name']
209
+ model_weight = model['weight']
210
+ model_rate_limit = model.get('rate_limit', 'N/A')
211
+
212
+ logger.info(f" - Model: {model_name}")
213
+ logger.info(f" Weight (Priority): {model_weight}")
214
+ logger.info(f" Rate Limit: {model_rate_limit}")
215
+
216
+ # Add provider_id and api_key to model for later use
217
+ model_with_provider = model.copy()
218
+ model_with_provider['provider_id'] = provider_id
219
+ model_with_provider['api_key'] = provider.get('api_key')
220
+ available_models.append(model_with_provider)
221
+
222
+ logger.info(f"")
223
+ logger.info(f"=== MODEL SELECTION SUMMARY ===")
224
+ logger.info(f"Total providers scanned: {len(providers)}")
225
+ logger.info(f"Providers skipped (rate limited): {len(skipped_providers)}")
226
+ if skipped_providers:
227
+ logger.info(f"Skipped providers: {', '.join(skipped_providers)}")
228
+ logger.info(f"Total models considered: {total_models_considered}")
229
+ logger.info(f"Total models available: {len(available_models)}")
230
+
231
+ if not available_models:
232
+ logger.error("No models available in rotation (all providers may be rate limited)")
233
+ logger.error("All providers in this rotation are currently deactivated")
234
+ raise HTTPException(status_code=503, detail="No models available in rotation (all providers may be rate limited)")
183
235
 
236
+ # Sort models by weight in descending order (higher weight = higher priority)
237
+ available_models.sort(key=lambda m: m['weight'], reverse=True)
238
+
239
+ logger.info(f"")
240
+ logger.info(f"=== PRIORITY-BASED SELECTION ===")
241
+ logger.info(f"Models sorted by weight (descending priority):")
242
+ for idx, model in enumerate(available_models, 1):
243
+ logger.info(f" {idx}. {model['name']} (provider: {model['provider_id']}, weight: {model['weight']})")
244
+
245
+ # Find the highest weight
246
+ highest_weight = available_models[0]['weight']
247
+ logger.info(f"")
248
+ logger.info(f"Highest priority weight: {highest_weight}")
249
+
250
+ # Filter models with the highest weight
251
+ highest_weight_models = [m for m in available_models if m['weight'] == highest_weight]
252
+ logger.info(f"Models with highest priority ({highest_weight}): {len(highest_weight_models)}")
253
+ for model in highest_weight_models:
254
+ logger.info(f" - {model['name']} (provider: {model['provider_id']})")
255
+
256
+ # If multiple models have the same highest weight, randomly select among them
184
257
  import random
185
- selected_model = random.choice(weighted_models)
186
- logger.info(f"Selected model: {selected_model}")
187
-
188
- provider_id = selected_model['provider_id']
189
- api_key = selected_model.get('api_key')
190
- model_name = selected_model['name']
258
+ if len(highest_weight_models) > 1:
259
+ logger.info(f"Multiple models with same highest priority - performing random selection")
260
+ selected_model = random.choice(highest_weight_models)
261
+ logger.info(f"Randomly selected from {len(highest_weight_models)} candidates")
262
+ else:
263
+ selected_model = highest_weight_models[0]
264
+ logger.info(f"Single model with highest priority - deterministic selection")
191
265
 
192
- logger.info(f"Selected provider_id: {provider_id}")
193
- logger.info(f"Selected model_name: {model_name}")
194
- logger.info(f"API key present: {bool(api_key)}")
195
-
196
- logger.info(f"Getting provider handler for {provider_id}")
197
- handler = get_provider_handler(provider_id, api_key)
198
- logger.info(f"Provider handler obtained: {handler.__class__.__name__}")
199
-
200
- if handler.is_rate_limited():
201
- raise HTTPException(status_code=503, detail="All providers temporarily unavailable")
202
-
203
- try:
204
- logger.info(f"Model requested: {model_name}")
205
- logger.info(f"Messages count: {len(request_data.get('messages', []))}")
206
- logger.info(f"Max tokens: {request_data.get('max_tokens')}")
207
- logger.info(f"Temperature: {request_data.get('temperature', 1.0)}")
208
- logger.info(f"Stream: {request_data.get('stream', False)}")
266
+ logger.info(f"")
267
+ logger.info(f"=== FINAL SELECTION ===")
268
+ logger.info(f"Selected model: {selected_model['name']}")
269
+ logger.info(f"Selected provider: {selected_model['provider_id']}")
270
+ logger.info(f"Model weight (priority): {selected_model['weight']}")
271
+ logger.info(f"Model rate limit: {selected_model.get('rate_limit', 'N/A')}")
272
+ logger.info(f"=== MODEL SELECTION PROCESS END ===")
273
+
274
+ # Retry logic: Try up to 2 times with different models
275
+ max_retries = 2
276
+ tried_models = []
277
+ last_error = None
278
+ successful_model = None
279
+
280
+ for attempt in range(max_retries):
281
+ logger.info(f"")
282
+ logger.info(f"=== ATTEMPT {attempt + 1}/{max_retries} ===")
209
283
 
210
- # Apply rate limiting with model-specific rate limit if available
211
- rate_limit = selected_model.get('rate_limit')
212
- logger.info(f"Model-specific rate limit: {rate_limit}")
213
- logger.info("Applying rate limiting...")
214
- await handler.apply_rate_limit(rate_limit)
215
- logger.info("Rate limiting applied")
284
+ # Select a model that hasn't been tried yet
285
+ remaining_models = [m for m in available_models if m not in tried_models]
286
+
287
+ if not remaining_models:
288
+ logger.error(f"No more models available to try")
289
+ logger.error(f"All {len(available_models)} models have been attempted")
290
+ break
291
+
292
+ # Sort remaining models by weight and select the best one
293
+ remaining_models.sort(key=lambda m: m['weight'], reverse=True)
294
+ current_model = remaining_models[0]
295
+ tried_models.append(current_model)
296
+
297
+ logger.info(f"Trying model: {current_model['name']} (provider: {current_model['provider_id']})")
298
+ logger.info(f"Attempt {attempt + 1} of {max_retries}")
299
+
300
+ provider_id = current_model['provider_id']
301
+ api_key = current_model.get('api_key')
302
+ model_name = current_model['name']
303
+
304
+ logger.info(f"Getting provider handler for {provider_id}")
305
+ handler = get_provider_handler(provider_id, api_key)
306
+ logger.info(f"Provider handler obtained: {handler.__class__.__name__}")
216
307
 
217
- logger.info(f"Sending request to provider handler...")
218
- response = await handler.handle_request(
219
- model=model_name,
220
- messages=request_data['messages'],
221
- max_tokens=request_data.get('max_tokens'),
222
- temperature=request_data.get('temperature', 1.0),
223
- stream=request_data.get('stream', False)
224
- )
225
- logger.info(f"Response received from provider")
226
- handler.record_success()
227
- logger.info(f"=== RotationHandler.handle_rotation_request END ===")
228
- return response
229
- except Exception as e:
230
- handler.record_failure()
231
- raise HTTPException(status_code=500, detail=str(e))
308
+ if handler.is_rate_limited():
309
+ logger.warning(f"Provider {provider_id} is rate limited, skipping to next model")
310
+ continue
311
+
312
+ try:
313
+ logger.info(f"Model requested: {model_name}")
314
+ logger.info(f"Messages count: {len(request_data.get('messages', []))}")
315
+ logger.info(f"Max tokens: {request_data.get('max_tokens')}")
316
+ logger.info(f"Temperature: {request_data.get('temperature', 1.0)}")
317
+ logger.info(f"Stream: {request_data.get('stream', False)}")
318
+
319
+ # Apply rate limiting with model-specific rate limit if available
320
+ rate_limit = current_model.get('rate_limit')
321
+ logger.info(f"Model-specific rate limit: {rate_limit}")
322
+ logger.info("Applying rate limiting...")
323
+ await handler.apply_rate_limit(rate_limit)
324
+ logger.info("Rate limiting applied")
325
+
326
+ logger.info(f"Sending request to provider handler...")
327
+ response = await handler.handle_request(
328
+ model=model_name,
329
+ messages=request_data['messages'],
330
+ max_tokens=request_data.get('max_tokens'),
331
+ temperature=request_data.get('temperature', 1.0),
332
+ stream=request_data.get('stream', False)
333
+ )
334
+ logger.info(f"Response received from provider")
335
+ handler.record_success()
336
+
337
+ # Update successful_model to the one that worked
338
+ successful_model = current_model
339
+
340
+ logger.info(f"=== RotationHandler.handle_rotation_request END ===")
341
+ logger.info(f"Request succeeded on attempt {attempt + 1}")
342
+ logger.info(f"Successfully used model: {successful_model['name']} (provider: {successful_model['provider_id']})")
343
+ return response
344
+ except Exception as e:
345
+ last_error = str(e)
346
+ handler.record_failure()
347
+ logger.error(f"Attempt {attempt + 1} failed: {str(e)}")
348
+ logger.error(f"Error type: {type(e).__name__}")
349
+ logger.error(f"Will try next model...")
350
+ continue
351
+
352
+ # All retries exhausted
353
+ logger.error(f"")
354
+ logger.error(f"=== ALL RETRIES EXHAUSTED ===")
355
+ logger.error(f"Attempted {len(tried_models)} different model(s): {[m['name'] for m in tried_models]}")
356
+ logger.error(f"Last error: {last_error}")
357
+ logger.error(f"Max retries ({max_retries}) reached without success")
358
+ raise HTTPException(
359
+ status_code=503,
360
+ detail=f"All providers in rotation failed after {max_retries} attempts. Last error: {last_error}"
361
+ )
232
362
 
233
363
  async def handle_rotation_model_list(self, rotation_id: str) -> List[Dict]:
234
364
  rotation_config = self.config.get_rotation(rotation_id)
@@ -310,6 +440,11 @@ class AutoselectHandler:
310
440
 
311
441
  async def _get_model_selection(self, prompt: str) -> str:
312
442
  """Send the autoselect prompt to a model and get the selection"""
443
+ import logging
444
+ logger = logging.getLogger(__name__)
445
+ logger.info(f"=== AUTOSELECT MODEL SELECTION START ===")
446
+ logger.info(f"Using 'general' rotation for model selection")
447
+
313
448
  # Use the first available provider/model for the selection
314
449
  # This is a simple implementation - could be enhanced to use a specific selection model
315
450
  rotation_handler = RotationHandler()
@@ -322,27 +457,64 @@ class AutoselectHandler:
322
457
  "stream": False
323
458
  }
324
459
 
460
+ logger.info(f"Selection request parameters:")
461
+ logger.info(f" Temperature: 0.1 (low for deterministic selection)")
462
+ logger.info(f" Max tokens: 100 (short response expected)")
463
+ logger.info(f" Stream: False")
464
+
325
465
  # Use the fallback rotation for the selection
326
466
  try:
467
+ logger.info(f"Sending selection request to rotation handler...")
327
468
  response = await rotation_handler.handle_rotation_request("general", selection_request)
469
+ logger.info(f"Selection response received")
470
+
328
471
  content = response.get('choices', [{}])[0].get('message', {}).get('content', '')
472
+ logger.info(f"Raw response content: {content[:200]}..." if len(content) > 200 else f"Raw response content: {content}")
473
+
329
474
  model_id = self._extract_model_selection(content)
475
+
476
+ if model_id:
477
+ logger.info(f"=== AUTOSELECT MODEL SELECTION SUCCESS ===")
478
+ logger.info(f"Selected model ID: {model_id}")
479
+ else:
480
+ logger.warning(f"=== AUTOSELECT MODEL SELECTION FAILED ===")
481
+ logger.warning(f"Could not extract model ID from response")
482
+ logger.warning(f"Response content: {content}")
483
+
330
484
  return model_id
331
485
  except Exception as e:
486
+ logger.error(f"=== AUTOSELECT MODEL SELECTION ERROR ===")
487
+ logger.error(f"Error during model selection: {str(e)}")
488
+ logger.error(f"Will use fallback model")
332
489
  # If selection fails, we'll handle it in the main handler
333
490
  return None
334
491
 
335
492
  async def handle_autoselect_request(self, autoselect_id: str, request_data: Dict) -> Dict:
336
493
  """Handle an autoselect request"""
494
+ import logging
495
+ logger = logging.getLogger(__name__)
496
+ logger.info(f"=== AUTOSELECT REQUEST START ===")
497
+ logger.info(f"Autoselect ID: {autoselect_id}")
498
+
337
499
  autoselect_config = self.config.get_autoselect(autoselect_id)
338
500
  if not autoselect_config:
501
+ logger.error(f"Autoselect {autoselect_id} not found")
339
502
  raise HTTPException(status_code=400, detail=f"Autoselect {autoselect_id} not found")
340
503
 
504
+ logger.info(f"Autoselect config loaded")
505
+ logger.info(f"Available models for selection: {len(autoselect_config.available_models)}")
506
+ for model_info in autoselect_config.available_models:
507
+ logger.info(f" - {model_info.model_id}: {model_info.description}")
508
+ logger.info(f"Fallback model: {autoselect_config.fallback}")
509
+
341
510
  # Extract the user prompt from the request
342
511
  user_messages = request_data.get('messages', [])
343
512
  if not user_messages:
513
+ logger.error("No messages provided")
344
514
  raise HTTPException(status_code=400, detail="No messages provided")
345
515
 
516
+ logger.info(f"User messages count: {len(user_messages)}")
517
+
346
518
  # Build a string representation of the user prompt
347
519
  user_prompt = ""
348
520
  for msg in user_messages:
@@ -353,37 +525,73 @@ class AutoselectHandler:
353
525
  content = str(content)
354
526
  user_prompt += f"{role}: {content}\n"
355
527
 
528
+ logger.info(f"User prompt length: {len(user_prompt)} characters")
529
+ logger.info(f"User prompt preview: {user_prompt[:200]}..." if len(user_prompt) > 200 else f"User prompt: {user_prompt}")
530
+
356
531
  # Build the autoselect prompt
532
+ logger.info(f"Building autoselect prompt...")
357
533
  autoselect_prompt = self._build_autoselect_prompt(user_prompt, autoselect_config)
534
+ logger.info(f"Autoselect prompt built (length: {len(autoselect_prompt)} characters)")
358
535
 
359
536
  # Get the model selection
537
+ logger.info(f"Requesting model selection from AI...")
360
538
  selected_model_id = await self._get_model_selection(autoselect_prompt)
361
539
 
362
540
  # Validate the selected model
541
+ logger.info(f"=== MODEL VALIDATION ===")
363
542
  if not selected_model_id:
364
543
  # Fallback to the configured fallback model
544
+ logger.warning(f"No model ID returned from selection")
545
+ logger.warning(f"Using fallback model: {autoselect_config.fallback}")
365
546
  selected_model_id = autoselect_config.fallback
366
547
  else:
367
548
  # Check if the selected model is in the available models list
368
549
  available_ids = [m.model_id for m in autoselect_config.available_models]
369
550
  if selected_model_id not in available_ids:
551
+ logger.warning(f"Selected model '{selected_model_id}' not in available models list")
552
+ logger.warning(f"Available models: {available_ids}")
553
+ logger.warning(f"Using fallback model: {autoselect_config.fallback}")
370
554
  selected_model_id = autoselect_config.fallback
555
+ else:
556
+ logger.info(f"Selected model '{selected_model_id}' is valid and available")
557
+
558
+ logger.info(f"=== FINAL MODEL CHOICE ===")
559
+ logger.info(f"Selected model ID: {selected_model_id}")
560
+ logger.info(f"Selection method: {'AI-selected' if selected_model_id != autoselect_config.fallback else 'Fallback'}")
371
561
 
372
562
  # Now proxy the actual request to the selected rotation
563
+ logger.info(f"Proxying request to rotation: {selected_model_id}")
373
564
  rotation_handler = RotationHandler()
374
- return await rotation_handler.handle_rotation_request(selected_model_id, request_data)
565
+ response = await rotation_handler.handle_rotation_request(selected_model_id, request_data)
566
+ logger.info(f"=== AUTOSELECT REQUEST END ===")
567
+ return response
375
568
 
376
569
  async def handle_autoselect_streaming_request(self, autoselect_id: str, request_data: Dict):
377
570
  """Handle an autoselect streaming request"""
571
+ import logging
572
+ logger = logging.getLogger(__name__)
573
+ logger.info(f"=== AUTOSELECT STREAMING REQUEST START ===")
574
+ logger.info(f"Autoselect ID: {autoselect_id}")
575
+
378
576
  autoselect_config = self.config.get_autoselect(autoselect_id)
379
577
  if not autoselect_config:
578
+ logger.error(f"Autoselect {autoselect_id} not found")
380
579
  raise HTTPException(status_code=400, detail=f"Autoselect {autoselect_id} not found")
381
580
 
581
+ logger.info(f"Autoselect config loaded")
582
+ logger.info(f"Available models for selection: {len(autoselect_config.available_models)}")
583
+ for model_info in autoselect_config.available_models:
584
+ logger.info(f" - {model_info.model_id}: {model_info.description}")
585
+ logger.info(f"Fallback model: {autoselect_config.fallback}")
586
+
382
587
  # Extract the user prompt from the request
383
588
  user_messages = request_data.get('messages', [])
384
589
  if not user_messages:
590
+ logger.error("No messages provided")
385
591
  raise HTTPException(status_code=400, detail="No messages provided")
386
592
 
593
+ logger.info(f"User messages count: {len(user_messages)}")
594
+
387
595
  # Build a string representation of the user prompt
388
596
  user_prompt = ""
389
597
  for msg in user_messages:
@@ -393,21 +601,41 @@ class AutoselectHandler:
393
601
  content = str(content)
394
602
  user_prompt += f"{role}: {content}\n"
395
603
 
604
+ logger.info(f"User prompt length: {len(user_prompt)} characters")
605
+ logger.info(f"User prompt preview: {user_prompt[:200]}..." if len(user_prompt) > 200 else f"User prompt: {user_prompt}")
606
+
396
607
  # Build the autoselect prompt
608
+ logger.info(f"Building autoselect prompt...")
397
609
  autoselect_prompt = self._build_autoselect_prompt(user_prompt, autoselect_config)
610
+ logger.info(f"Autoselect prompt built (length: {len(autoselect_prompt)} characters)")
398
611
 
399
612
  # Get the model selection
613
+ logger.info(f"Requesting model selection from AI...")
400
614
  selected_model_id = await self._get_model_selection(autoselect_prompt)
401
615
 
402
616
  # Validate the selected model
617
+ logger.info(f"=== MODEL VALIDATION ===")
403
618
  if not selected_model_id:
619
+ logger.warning(f"No model ID returned from selection")
620
+ logger.warning(f"Using fallback model: {autoselect_config.fallback}")
404
621
  selected_model_id = autoselect_config.fallback
405
622
  else:
406
623
  available_ids = [m.model_id for m in autoselect_config.available_models]
407
624
  if selected_model_id not in available_ids:
625
+ logger.warning(f"Selected model '{selected_model_id}' not in available models list")
626
+ logger.warning(f"Available models: {available_ids}")
627
+ logger.warning(f"Using fallback model: {autoselect_config.fallback}")
408
628
  selected_model_id = autoselect_config.fallback
629
+ else:
630
+ logger.info(f"Selected model '{selected_model_id}' is valid and available")
631
+
632
+ logger.info(f"=== FINAL MODEL CHOICE ===")
633
+ logger.info(f"Selected model ID: {selected_model_id}")
634
+ logger.info(f"Selection method: {'AI-selected' if selected_model_id != autoselect_config.fallback else 'Fallback'}")
635
+ logger.info(f"Request mode: Streaming")
409
636
 
410
637
  # Now proxy the actual streaming request to the selected rotation
638
+ logger.info(f"Proxying streaming request to rotation: {selected_model_id}")
411
639
  rotation_handler = RotationHandler()
412
640
 
413
641
  async def stream_generator():
@@ -419,8 +647,10 @@ class AutoselectHandler:
419
647
  for chunk in response:
420
648
  yield f"data: {chunk}\n\n".encode('utf-8')
421
649
  except Exception as e:
650
+ logger.error(f"Error in streaming response: {str(e)}")
422
651
  yield f"data: {str(e)}\n\n".encode('utf-8')
423
652
 
653
+ logger.info(f"=== AUTOSELECT STREAMING REQUEST END ===")
424
654
  return StreamingResponse(stream_generator(), media_type="text/event-stream")
425
655
 
426
656
  async def handle_autoselect_model_list(self, autoselect_id: str) -> List[Dict]:
aisbf/providers.py CHANGED
@@ -62,14 +62,56 @@ class BaseProviderHandler:
62
62
  self.last_request_time = time.time()
63
63
 
64
64
  def record_failure(self):
65
+ import logging
66
+ logger = logging.getLogger(__name__)
67
+
65
68
  self.error_tracking['failures'] += 1
66
69
  self.error_tracking['last_failure'] = time.time()
70
+
71
+ failure_count = self.error_tracking['failures']
72
+ logger.warning(f"=== PROVIDER FAILURE RECORDED ===")
73
+ logger.warning(f"Provider: {self.provider_id}")
74
+ logger.warning(f"Failure count: {failure_count}/3")
75
+ logger.warning(f"Last failure time: {self.error_tracking['last_failure']}")
76
+
67
77
  if self.error_tracking['failures'] >= 3:
68
78
  self.error_tracking['disabled_until'] = time.time() + 300 # 5 minutes
79
+ disabled_until_time = self.error_tracking['disabled_until']
80
+ cooldown_remaining = int(disabled_until_time - time.time())
81
+ logger.error(f"!!! PROVIDER DISABLED !!!")
82
+ logger.error(f"Provider: {self.provider_id}")
83
+ logger.error(f"Reason: 3 consecutive failures reached")
84
+ logger.error(f"Disabled until: {disabled_until_time}")
85
+ logger.error(f"Cooldown period: {cooldown_remaining} seconds (5 minutes)")
86
+ logger.error(f"Provider will be automatically re-enabled after cooldown")
87
+ else:
88
+ remaining_failures = 3 - failure_count
89
+ logger.warning(f"Provider still active. {remaining_failures} more failure(s) will disable it")
90
+ logger.warning(f"=== END FAILURE RECORDING ===")
69
91
 
70
92
  def record_success(self):
93
+ import logging
94
+ logger = logging.getLogger(__name__)
95
+
96
+ was_disabled = self.error_tracking['disabled_until'] is not None
97
+ previous_failures = self.error_tracking['failures']
98
+
71
99
  self.error_tracking['failures'] = 0
72
100
  self.error_tracking['disabled_until'] = None
101
+
102
+ logger.info(f"=== PROVIDER SUCCESS RECORDED ===")
103
+ logger.info(f"Provider: {self.provider_id}")
104
+ logger.info(f"Previous failure count: {previous_failures}")
105
+ logger.info(f"Failure count reset to: 0")
106
+
107
+ if was_disabled:
108
+ logger.info(f"!!! PROVIDER RE-ENABLED !!!")
109
+ logger.info(f"Provider: {self.provider_id}")
110
+ logger.info(f"Reason: Successful request after cooldown period")
111
+ logger.info(f"Provider is now active and available for requests")
112
+ else:
113
+ logger.info(f"Provider remains active")
114
+ logger.info(f"=== END SUCCESS RECORDING ===")
73
115
 
74
116
  class GoogleProviderHandler(BaseProviderHandler):
75
117
  def __init__(self, provider_id: str, api_key: str):
@@ -43,7 +43,7 @@ from .providers import (
43
43
  )
44
44
  from .handlers import RequestHandler, RotationHandler, AutoselectHandler
45
45
 
46
- __version__ = "0.2.0"
46
+ __version__ = "0.2.5"
47
47
  __all__ = [
48
48
  # Config
49
49
  "config",
@@ -163,6 +163,26 @@ class Config:
163
163
  data = json.load(f)
164
164
  self.rotations = {k: RotationConfig(**v) for k, v in data['rotations'].items()}
165
165
  logger.info(f"Loaded {len(self.rotations)} rotations: {list(self.rotations.keys())}")
166
+
167
+ # Validate that all providers referenced in rotations exist
168
+ logger.info(f"=== VALIDATING ROTATION PROVIDERS ===")
169
+ available_providers = list(self.providers.keys())
170
+ logger.info(f"Available providers: {available_providers}")
171
+
172
+ for rotation_id, rotation_config in self.rotations.items():
173
+ logger.info(f"Validating rotation: {rotation_id}")
174
+ for provider in rotation_config.providers:
175
+ provider_id = provider['provider_id']
176
+ if provider_id not in self.providers:
177
+ logger.warning(f"!!! CONFIGURATION WARNING !!!")
178
+ logger.warning(f"Rotation '{rotation_id}' references provider '{provider_id}' which is NOT defined in providers.json")
179
+ logger.warning(f"Available providers: {available_providers}")
180
+ logger.warning(f"This provider will be SKIPPED during rotation requests")
181
+ logger.warning(f"Please add the provider to providers.json or remove it from the rotation configuration")
182
+ logger.warning(f"!!! END WARNING !!!")
183
+ else:
184
+ logger.info(f" ✓ Provider '{provider_id}' is available")
185
+
166
186
  logger.info(f"=== Config._load_rotations END ===")
167
187
 
168
188
  def _load_autoselect(self):