# type: ignore """ ================================================================================ OPENROUTER MULTI-ACCOUNT ROTATION SYSTEM ================================================================================ Rotação automática entre 5 contas OpenRouter para evitar rate limit (429). Contas Nomeadas: 1. gitbelmira (conta 1) 2. sandeobras (conta 2) 3. softedge (conta 3) 4. joselena (conta 4) 5. fugakusayo (conta 5) Filosofia: - Detecta erro 429 (rate limit exceeded) - Automaticamente muda para próxima chave (com fallback na primeira) - Sem interrupção para o utilizador - Log detalhado de qual conta está sendo usada Cada conta tem tier free com limite diário: - ~1000 requests/dia - 5 contas = ~5000 requests/dia antes de precisar esperar 24h ================================================================================ """ import os import time from typing import List, Optional, Dict, Any from dataclasses import dataclass, field from datetime import datetime, timedelta from loguru import logger # Nomes das 5 contas OpenRouter (mapeado por índice) ACCOUNT_NAMES = [ "gitbelmira", # 0 - Conta 1 "sandeobras", # 1 - Conta 2 "softedge", # 2 - Conta 3 "joselena", # 3 - Conta 4 "fugakusayo", # 4 - Conta 5 ] @dataclass class AccountQuota: """Quota info para uma conta OpenRouter""" key_index: int account_name: str # ← NOVO: Nome da conta para identificação api_key: str last_429_time: Optional[float] = None requests_today: int = 0 last_reset: float = field(default_factory=time.time) is_exhausted: bool = False class OpenRouterAccountRotation: """ Gerencia rotação de 5 contas OpenRouter. Detecta 429 e muda automaticamente para próxima chave. Fallback na primeira conta quando chega na última. """ def __init__(self, api_keys: List[str]): """ Inicializa sistema de rotação. Args: api_keys: Lista de 5 chaves OpenRouter (pode incluir strings vazias) """ self.api_keys = [k.strip() for k in api_keys if k and k.strip()] self.current_key_index = 0 self.accounts: Dict[int, AccountQuota] = {} # Inicializa quota para cada chave com nome associado for i, key in enumerate(self.api_keys): account_name = ACCOUNT_NAMES[i] if i < len(ACCOUNT_NAMES) else f"account_{i}" self.accounts[i] = AccountQuota( key_index=i, account_name=account_name, api_key=key, requests_today=0 ) self.logger = logger self._log_initialization() def _log_initialization(self): """Log status inicial com nomes das contas""" active_keys = len(self.api_keys) self.logger.success(f"✅ OpenRouter Rotation inicializado com {active_keys} contas:") for i, quota in self.accounts.items(): status = "✅ ATIVA" if quota.api_key else "❌ VAZIA" self.logger.info(f" [{i+1}] {quota.account_name.upper():<15} {status}") if active_keys < 5: self.logger.warning(f"⚠️ Apenas {active_keys}/5 contas configuradas") def get_current_key(self) -> Optional[str]: """Retorna chave OpenRouter atual""" if not self.api_keys or self.current_key_index >= len(self.api_keys): return None return self.api_keys[self.current_key_index] def get_current_account_name(self) -> str: """Retorna nome da conta atual""" if not self.api_keys or self.current_key_index >= len(self.api_keys): return "unknown" if self.current_key_index < len(ACCOUNT_NAMES): return ACCOUNT_NAMES[self.current_key_index] return f"account_{self.current_key_index}" def get_current_key_index(self) -> int: """Retorna índice da chave atual (0-4)""" return self.current_key_index def rotate_on_429(self) -> Optional[str]: """Rotaciona a conta após 429 e retorna a nova chave se disponível.""" if self.handle_429_error(): return self.get_current_key() return None COOLDOWN_SECONDS = 60 def _is_account_available(self, quota: AccountQuota) -> bool: if not quota.is_exhausted: return True if quota.last_429_time and (time.time() - quota.last_429_time) >= self.COOLDOWN_SECONDS: quota.is_exhausted = False self.logger.info(f"🔄 [OR Cooldown] Conta '{quota.account_name.upper()}' disponível novamente após {self.COOLDOWN_SECONDS}s") return True return False def handle_429_error(self) -> bool: """Lida com erro 429 e busca a próxima chave disponível.""" if not self.api_keys: return False quota = self.accounts[self.current_key_index] quota.last_429_time = time.time() quota.is_exhausted = True account_name = quota.account_name.upper() self.logger.warning( f"⚠️ [429 RATE LIMIT] Conta '{account_name}' (índice {self.current_key_index + 1}/{len(self.api_keys)}) esgotada. " f"Procurando próxima..." ) original_index = self.current_key_index for _ in range(len(self.api_keys)): self.current_key_index = (self.current_key_index + 1) % len(self.api_keys) next_quota = self.accounts[self.current_key_index] if self._is_account_available(next_quota): next_account_name = next_quota.account_name.upper() self.logger.success( f"✅ [429 RECOVERY] Mudando de '{account_name}' para '{next_account_name}' " f"(índice {self.current_key_index + 1}/{len(self.api_keys)})" ) return True if self.current_key_index == 0 and original_index > 0: self.logger.info( f"🔄 [429 ROTATION CYCLE] Completado ciclo de contas. " f"Voltando na primeira: '{ACCOUNT_NAMES[0].upper()}'" ) self.logger.error( f"❌ [429 CRITICAL] Todas as {len(self.api_keys)} contas esgotadas! " f"Contas: {', '.join([self.accounts[i].account_name.upper() for i in self.accounts])}" ) return False def reset_quotas_if_needed(self): """ Reseta quotas se passaram 24 horas. Chamado periodicamente para permitir reutilização de contas. """ now = time.time() reset_count = 0 for quota in self.accounts.values(): hours_since_reset = (now - quota.last_reset) / 3600 if hours_since_reset >= 24: quota.requests_today = 0 quota.is_exhausted = False quota.last_reset = now reset_count += 1 self.logger.info( f"🔄 [QUOTA RESET] Conta '{quota.account_name.upper()}' resetada (24h passaram)" ) if reset_count > 0: self.logger.success(f"✅ {reset_count} conta(s) resetada(s) e disponível(is)") def record_request(self): """Registra uma request para quota tracking""" self.accounts[self.current_key_index].requests_today += 1 def get_status(self) -> Dict[str, Any]: """Retorna status atual de todas as contas""" status = { "current_account": self.get_current_account_name(), "current_index": self.current_key_index, "total_accounts": len(self.api_keys), "accounts": [] } for i, quota in self.accounts.items(): status["accounts"].append({ "index": i + 1, # 1-indexed para display "name": quota.account_name.upper(), "requests_today": quota.requests_today, "exhausted": quota.is_exhausted, "last_429": quota.last_429_time, }) return status def print_status(self): """Printa status de quota para logging""" status = self.get_status() current_name = status['current_account'].upper() self.logger.info( f"📊 [QUOTA STATUS] Conta atual: {current_name} " f"(índice {status['current_index'] + 1}/{status['total_accounts']})" ) for account_info in status["accounts"]: status_icon = "❌ ESGOTADA" if account_info["exhausted"] else "✅ OK" self.logger.info( f" [{account_info['index']}] {account_info['name']:<15} " f"{account_info['requests_today']:>5} requests - {status_icon}" ) # Singleton instance _ROTATION_INSTANCE: Optional[OpenRouterAccountRotation] = None def _resolve_cfg(config, name: str) -> str: for attr in (name, name.upper(), name.lower()): v = getattr(config, attr, None) if v and str(v).strip(): return str(v).strip() for env in (name, name.upper(), name.lower()): v = os.getenv(env, "").strip() if v: if len(v) >= 2 and ((v.startswith('"') and v.endswith('"')) or (v.startswith("'") and v.endswith("'"))): v = v[1:-1].strip() if v: return v return "" def get_openrouter_rotation() -> OpenRouterAccountRotation: """Get singleton OpenRouter rotation instance""" global _ROTATION_INSTANCE if _ROTATION_INSTANCE is None: from . import config keys = [ _resolve_cfg(config, "GITBELMIRA_OPENROUTER_API"), _resolve_cfg(config, "SANDEOBRAS_OPENROUTER_API"), _resolve_cfg(config, "SOFTEDGE_OPENROUTER_API"), _resolve_cfg(config, "JOSELENA_OPENROUTER_API"), _resolve_cfg(config, "FUGAKUSAYO_OPENROUTER_API"), ] _ROTATION_INSTANCE = OpenRouterAccountRotation(keys) return _ROTATION_INSTANCE def reset_rotation_instance(): """Reset singleton (para testes)""" global _ROTATION_INSTANCE _ROTATION_INSTANCE = None