Spaces:
Running
Running
Isaac Quarenta commited on
Commit ·
a3e8a18
1
Parent(s): e07e66f
fix: document OpenRouter multi-account keys in .env.example, remove duplicate openrouter_rotation_new.py
Browse files- .env.example: add gitakira/sandeobras/softedge/joselena/fugakusayo OpenRouter account keys documentation
- Remove duplicate modules/openrouter_rotation_new.py (not imported anywhere; openrouter_rotation.py is the canonical version)
- modules/api.py (already committed): _setup_openrouter now uses rotation system for key+account name, _call_openrouter updates client API key from rotation, 429 handler rotates to next account and retries, model fallback changed from tencent/hy3-preview:free to deepseek/deepseek-chat
- .env.example +10 -0
- modules/openrouter_rotation_new.py +0 -253
.env.example
CHANGED
|
@@ -27,6 +27,16 @@ TOGETHER_API_KEY=sua_chave_aqui
|
|
| 27 |
# Limite: Ilimitado com rate limit
|
| 28 |
HF_API_KEY=hf_sua_chave_aqui
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
# ============================================================================
|
| 31 |
# 🌐 CONFIGURAÇÕES DE SERVIDOR (OPCIONAL)
|
| 32 |
# ============================================================================
|
|
|
|
| 27 |
# Limite: Ilimitado com rate limit
|
| 28 |
HF_API_KEY=hf_sua_chave_aqui
|
| 29 |
|
| 30 |
+
# OPENROUTER (https://openrouter.ai/keys)
|
| 31 |
+
# Limite: ~1k requests/dia grátis por conta
|
| 32 |
+
# Use 5 contas distintas para rotação automática (evita 429)
|
| 33 |
+
OPENROUTER_API_KEY=sua_chave_aqui
|
| 34 |
+
gitakira_openrouter_api=sk-or-v1-...
|
| 35 |
+
sandeobras_openrouter_api=sk-or-v1-...
|
| 36 |
+
softedge_openrouter_api=sk-or-v1-...
|
| 37 |
+
joselena_openrouter_api=sk-or-v1-...
|
| 38 |
+
fugakusayo_openrouter_api=sk-or-v1-...
|
| 39 |
+
|
| 40 |
# ============================================================================
|
| 41 |
# 🌐 CONFIGURAÇÕES DE SERVIDOR (OPCIONAL)
|
| 42 |
# ============================================================================
|
modules/openrouter_rotation_new.py
DELETED
|
@@ -1,253 +0,0 @@
|
|
| 1 |
-
# type: ignore
|
| 2 |
-
"""
|
| 3 |
-
================================================================================
|
| 4 |
-
OPENROUTER MULTI-ACCOUNT ROTATION SYSTEM
|
| 5 |
-
================================================================================
|
| 6 |
-
Rotação automática entre 5 contas OpenRouter para evitar rate limit (429).
|
| 7 |
-
|
| 8 |
-
Contas Nomeadas:
|
| 9 |
-
1. gitakira (conta 1)
|
| 10 |
-
2. sandeobras (conta 2)
|
| 11 |
-
3. softedge (conta 3)
|
| 12 |
-
4. joselena (conta 4)
|
| 13 |
-
5. fugakusayo (conta 5)
|
| 14 |
-
|
| 15 |
-
Filosofia:
|
| 16 |
-
- Detecta erro 429 (rate limit exceeded)
|
| 17 |
-
- Automaticamente muda para próxima chave (com fallback na primeira)
|
| 18 |
-
- Sem interrupção para o utilizador
|
| 19 |
-
- Log detalhado de qual conta está sendo usada
|
| 20 |
-
|
| 21 |
-
Cada conta tem tier free com limite diário:
|
| 22 |
-
- ~1000 requests/dia
|
| 23 |
-
- 5 contas = ~5000 requests/dia antes de precisar esperar 24h
|
| 24 |
-
================================================================================
|
| 25 |
-
"""
|
| 26 |
-
|
| 27 |
-
import os
|
| 28 |
-
import time
|
| 29 |
-
from typing import List, Optional, Dict, Any
|
| 30 |
-
from dataclasses import dataclass, field
|
| 31 |
-
from datetime import datetime, timedelta
|
| 32 |
-
from loguru import logger
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
# Nomes das 5 contas OpenRouter (mapeado por índice)
|
| 36 |
-
ACCOUNT_NAMES = [
|
| 37 |
-
"gitakira", # 0 - Conta 1
|
| 38 |
-
"sandeobras", # 1 - Conta 2
|
| 39 |
-
"softedge", # 2 - Conta 3
|
| 40 |
-
"joselena", # 3 - Conta 4
|
| 41 |
-
"fugakusayo", # 4 - Conta 5
|
| 42 |
-
]
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
@dataclass
|
| 46 |
-
class AccountQuota:
|
| 47 |
-
"""Quota info para uma conta OpenRouter"""
|
| 48 |
-
key_index: int
|
| 49 |
-
account_name: str # Nome da conta para identificação
|
| 50 |
-
api_key: str
|
| 51 |
-
last_429_time: Optional[float] = None
|
| 52 |
-
requests_today: int = 0
|
| 53 |
-
last_reset: float = field(default_factory=time.time)
|
| 54 |
-
is_exhausted: bool = False
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
class OpenRouterAccountRotation:
|
| 58 |
-
"""
|
| 59 |
-
Gerencia rotação de 5 contas OpenRouter.
|
| 60 |
-
Detecta 429 e muda automaticamente para próxima chave.
|
| 61 |
-
Fallback na primeira conta quando chega na última.
|
| 62 |
-
"""
|
| 63 |
-
|
| 64 |
-
def __init__(self, api_keys: List[str]):
|
| 65 |
-
"""
|
| 66 |
-
Inicializa sistema de rotação.
|
| 67 |
-
|
| 68 |
-
Args:
|
| 69 |
-
api_keys: Lista de 5 chaves OpenRouter (pode incluir strings vazias)
|
| 70 |
-
"""
|
| 71 |
-
self.api_keys = [k.strip() for k in api_keys if k and k.strip()]
|
| 72 |
-
self.current_key_index = 0
|
| 73 |
-
self.accounts: Dict[int, AccountQuota] = {}
|
| 74 |
-
|
| 75 |
-
# Inicializa quota para cada chave com nome associado
|
| 76 |
-
for i, key in enumerate(self.api_keys):
|
| 77 |
-
account_name = ACCOUNT_NAMES[i] if i < len(ACCOUNT_NAMES) else f"account_{i}"
|
| 78 |
-
self.accounts[i] = AccountQuota(
|
| 79 |
-
key_index=i,
|
| 80 |
-
account_name=account_name,
|
| 81 |
-
api_key=key,
|
| 82 |
-
requests_today=0
|
| 83 |
-
)
|
| 84 |
-
|
| 85 |
-
self.logger = logger
|
| 86 |
-
self._log_initialization()
|
| 87 |
-
|
| 88 |
-
def _log_initialization(self):
|
| 89 |
-
"""Log status inicial com nomes das contas"""
|
| 90 |
-
active_keys = len(self.api_keys)
|
| 91 |
-
self.logger.success(f"✅ OpenRouter Rotation inicializado com {active_keys} contas:")
|
| 92 |
-
for i, quota in self.accounts.items():
|
| 93 |
-
status = "✅ ATIVA" if quota.api_key else "❌ VAZIA"
|
| 94 |
-
self.logger.info(f" [{i+1}] {quota.account_name.upper():<15} {status}")
|
| 95 |
-
|
| 96 |
-
if active_keys < 5:
|
| 97 |
-
self.logger.warning(f"⚠️ Apenas {active_keys}/5 contas configuradas")
|
| 98 |
-
|
| 99 |
-
def get_current_key(self) -> Optional[str]:
|
| 100 |
-
"""Retorna chave OpenRouter atual"""
|
| 101 |
-
if not self.api_keys or self.current_key_index >= len(self.api_keys):
|
| 102 |
-
return None
|
| 103 |
-
return self.api_keys[self.current_key_index]
|
| 104 |
-
|
| 105 |
-
def get_current_account_name(self) -> str:
|
| 106 |
-
"""Retorna nome da conta atual"""
|
| 107 |
-
if self.current_key_index < len(ACCOUNT_NAMES):
|
| 108 |
-
return ACCOUNT_NAMES[self.current_key_index]
|
| 109 |
-
return f"account_{self.current_key_index}"
|
| 110 |
-
|
| 111 |
-
def get_current_key_index(self) -> int:
|
| 112 |
-
"""Retorna índice da chave atual (0-4)"""
|
| 113 |
-
return self.current_key_index
|
| 114 |
-
|
| 115 |
-
def handle_429_error(self) -> bool:
|
| 116 |
-
"""
|
| 117 |
-
Lida com erro 429 (rate limit exceeded).
|
| 118 |
-
Muda para próxima chave se disponível (fallback na primeira).
|
| 119 |
-
|
| 120 |
-
Returns:
|
| 121 |
-
True se mudou para nova chave
|
| 122 |
-
False se nenhuma chave disponível
|
| 123 |
-
"""
|
| 124 |
-
# Marca conta atual como esgotada temporariamente
|
| 125 |
-
quota = self.accounts[self.current_key_index]
|
| 126 |
-
quota.last_429_time = time.time()
|
| 127 |
-
quota.is_exhausted = True
|
| 128 |
-
|
| 129 |
-
account_name = quota.account_name.upper()
|
| 130 |
-
self.logger.warning(
|
| 131 |
-
f"⚠️ [429 RATE LIMIT] Conta '{account_name}' (índice {self.current_key_index + 1}/5) esgotada. "
|
| 132 |
-
f"Procurando próxima..."
|
| 133 |
-
)
|
| 134 |
-
|
| 135 |
-
# Tenta encontrar próxima chave disponível
|
| 136 |
-
original_index = self.current_key_index
|
| 137 |
-
for _ in range(len(self.api_keys)):
|
| 138 |
-
# Move para próxima conta (com fallback na primeira)
|
| 139 |
-
self.current_key_index = (self.current_key_index + 1) % len(self.api_keys)
|
| 140 |
-
next_quota = self.accounts[self.current_key_index]
|
| 141 |
-
|
| 142 |
-
if not next_quota.is_exhausted:
|
| 143 |
-
next_account_name = next_quota.account_name.upper()
|
| 144 |
-
self.logger.success(
|
| 145 |
-
f"✅ [429 RECOVERY] Mudando de '{account_name}' para '{next_account_name}' "
|
| 146 |
-
f"(índice {self.current_key_index + 1}/5)"
|
| 147 |
-
)
|
| 148 |
-
return True
|
| 149 |
-
|
| 150 |
-
# Se chegou na última e vai voltar na primeira
|
| 151 |
-
if self.current_key_index == 0 and original_index > 0:
|
| 152 |
-
self.logger.info(
|
| 153 |
-
f"🔄 [429 ROTATION CYCLE] Completado ciclo de contas. "
|
| 154 |
-
f"Voltando na primeira: '{ACCOUNT_NAMES[0].upper()}'"
|
| 155 |
-
)
|
| 156 |
-
|
| 157 |
-
# Todas esgotadas, mas podemos tentar a original após 1 segundo
|
| 158 |
-
self.logger.error(
|
| 159 |
-
f"❌ [429 CRITICAL] Todas as {len(self.api_keys)} contas esgotadas! "
|
| 160 |
-
f"Contas: {', '.join([self.accounts[i].account_name.upper() for i in self.accounts])}"
|
| 161 |
-
)
|
| 162 |
-
return False
|
| 163 |
-
|
| 164 |
-
def reset_quotas_if_needed(self):
|
| 165 |
-
"""
|
| 166 |
-
Reseta quotas se passaram 24 horas.
|
| 167 |
-
Chamado periodicamente para permitir reutilização de contas.
|
| 168 |
-
"""
|
| 169 |
-
now = time.time()
|
| 170 |
-
reset_count = 0
|
| 171 |
-
for quota in self.accounts.values():
|
| 172 |
-
hours_since_reset = (now - quota.last_reset) / 3600
|
| 173 |
-
if hours_since_reset >= 24:
|
| 174 |
-
quota.requests_today = 0
|
| 175 |
-
quota.is_exhausted = False
|
| 176 |
-
quota.last_reset = now
|
| 177 |
-
reset_count += 1
|
| 178 |
-
self.logger.info(
|
| 179 |
-
f"🔄 [QUOTA RESET] Conta '{quota.account_name.upper()}' resetada (24h passaram)"
|
| 180 |
-
)
|
| 181 |
-
if reset_count > 0:
|
| 182 |
-
self.logger.success(f"✅ {reset_count} conta(s) resetada(s) e disponível(is)")
|
| 183 |
-
|
| 184 |
-
def record_request(self):
|
| 185 |
-
"""Registra uma request para quota tracking"""
|
| 186 |
-
self.accounts[self.current_key_index].requests_today += 1
|
| 187 |
-
|
| 188 |
-
def get_status(self) -> Dict[str, Any]:
|
| 189 |
-
"""Retorna status atual de todas as contas"""
|
| 190 |
-
status = {
|
| 191 |
-
"current_account": self.get_current_account_name(),
|
| 192 |
-
"current_index": self.current_key_index,
|
| 193 |
-
"total_accounts": len(self.api_keys),
|
| 194 |
-
"accounts": []
|
| 195 |
-
}
|
| 196 |
-
|
| 197 |
-
for i, quota in self.accounts.items():
|
| 198 |
-
status["accounts"].append({
|
| 199 |
-
"index": i + 1, # 1-indexed para display
|
| 200 |
-
"name": quota.account_name.upper(),
|
| 201 |
-
"requests_today": quota.requests_today,
|
| 202 |
-
"exhausted": quota.is_exhausted,
|
| 203 |
-
"last_429": quota.last_429_time,
|
| 204 |
-
})
|
| 205 |
-
|
| 206 |
-
return status
|
| 207 |
-
|
| 208 |
-
def print_status(self):
|
| 209 |
-
"""Printa status de quota para logging"""
|
| 210 |
-
status = self.get_status()
|
| 211 |
-
current_name = status['current_account'].upper()
|
| 212 |
-
self.logger.info(
|
| 213 |
-
f"📊 [QUOTA STATUS] Conta atual: {current_name} "
|
| 214 |
-
f"(índice {status['current_index'] + 1}/{status['total_accounts']})"
|
| 215 |
-
)
|
| 216 |
-
|
| 217 |
-
for account_info in status["accounts"]:
|
| 218 |
-
status_icon = "❌ ESGOTADA" if account_info["exhausted"] else "✅ OK"
|
| 219 |
-
self.logger.info(
|
| 220 |
-
f" [{account_info['index']}] {account_info['name']:<15} "
|
| 221 |
-
f"{account_info['requests_today']:>5} requests - {status_icon}"
|
| 222 |
-
)
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
# Singleton instance
|
| 226 |
-
_ROTATION_INSTANCE: Optional[OpenRouterAccountRotation] = None
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
def get_openrouter_rotation() -> OpenRouterAccountRotation:
|
| 230 |
-
"""Get singleton OpenRouter rotation instance"""
|
| 231 |
-
global _ROTATION_INSTANCE
|
| 232 |
-
if _ROTATION_INSTANCE is None:
|
| 233 |
-
from . import config
|
| 234 |
-
|
| 235 |
-
# Carrega 5 chaves nomeadas do config
|
| 236 |
-
# Nomes: gitakira_openrouter_api, sandeobras_openrouter_api, etc
|
| 237 |
-
keys = [
|
| 238 |
-
getattr(config, "GITAKIRA_OPENROUTER_API", ""),
|
| 239 |
-
getattr(config, "SANDEOBRAS_OPENROUTER_API", ""),
|
| 240 |
-
getattr(config, "SOFTEDGE_OPENROUTER_API", ""),
|
| 241 |
-
getattr(config, "JOSELENA_OPENROUTER_API", ""),
|
| 242 |
-
getattr(config, "FUGAKUSAYO_OPENROUTER_API", ""),
|
| 243 |
-
]
|
| 244 |
-
|
| 245 |
-
_ROTATION_INSTANCE = OpenRouterAccountRotation(keys)
|
| 246 |
-
|
| 247 |
-
return _ROTATION_INSTANCE
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
def reset_rotation_instance():
|
| 251 |
-
"""Reset singleton (para testes)"""
|
| 252 |
-
global _ROTATION_INSTANCE
|
| 253 |
-
_ROTATION_INSTANCE = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|