AKIRA-SOFTEDGE / modules /openrouter_rotation.py
Belmira SoftEdge
fix: reforca prompt curta Belmira (RESPOSTA_CURTA 3-5 palavras anti-rodeio Que tal...) + case-insensitive Secrets (3 Mistral ATIVAS) + COT suavizado sem mudar max_tokens
d81b155
Raw
History Blame Contribute Delete
10.4 kB
# 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