Spaces:
Running
Running
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 | # 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 | |
| ] | |
| 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 | |