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 +1 -1
- aisbf/config.py +20 -0
- aisbf/handlers.py +285 -55
- aisbf/providers.py +42 -0
- {aisbf-0.2.3.data → aisbf-0.2.5.data}/data/share/aisbf/aisbf/__init__.py +1 -1
- {aisbf-0.2.3.data → aisbf-0.2.5.data}/data/share/aisbf/aisbf/config.py +20 -0
- {aisbf-0.2.3.data → aisbf-0.2.5.data}/data/share/aisbf/aisbf/handlers.py +285 -55
- {aisbf-0.2.3.data → aisbf-0.2.5.data}/data/share/aisbf/aisbf/providers.py +42 -0
- {aisbf-0.2.3.data → aisbf-0.2.5.data}/data/share/aisbf/main.py +164 -1
- {aisbf-0.2.3.dist-info → aisbf-0.2.5.dist-info}/METADATA +1 -1
- aisbf-0.2.5.dist-info/RECORD +24 -0
- aisbf-0.2.3.dist-info/RECORD +0 -24
- {aisbf-0.2.3.data → aisbf-0.2.5.data}/data/share/aisbf/aisbf/models.py +0 -0
- {aisbf-0.2.3.data → aisbf-0.2.5.data}/data/share/aisbf/aisbf.sh +0 -0
- {aisbf-0.2.3.data → aisbf-0.2.5.data}/data/share/aisbf/autoselect.json +0 -0
- {aisbf-0.2.3.data → aisbf-0.2.5.data}/data/share/aisbf/autoselect.md +0 -0
- {aisbf-0.2.3.data → aisbf-0.2.5.data}/data/share/aisbf/providers.json +0 -0
- {aisbf-0.2.3.data → aisbf-0.2.5.data}/data/share/aisbf/requirements.txt +0 -0
- {aisbf-0.2.3.data → aisbf-0.2.5.data}/data/share/aisbf/rotations.json +0 -0
- {aisbf-0.2.3.dist-info → aisbf-0.2.5.dist-info}/WHEEL +0 -0
- {aisbf-0.2.3.dist-info → aisbf-0.2.5.dist-info}/entry_points.txt +0 -0
- {aisbf-0.2.3.dist-info → aisbf-0.2.5.dist-info}/licenses/LICENSE.txt +0 -0
- {aisbf-0.2.3.dist-info → aisbf-0.2.5.dist-info}/top_level.txt +0 -0
aisbf/__init__.py
CHANGED
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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"
|
|
193
|
-
logger.info(f"
|
|
194
|
-
logger.info(f"
|
|
195
|
-
|
|
196
|
-
logger.info(f"
|
|
197
|
-
|
|
198
|
-
logger.info(f"
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
logger.info(f"
|
|
208
|
-
logger.info(f"
|
|
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
|
-
#
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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):
|
|
@@ -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):
|