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

Files changed (2) hide show
  1. .env.example +10 -0
  2. 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