Spaces:
Sleeping
Sleeping
| """ | |
| gemini_key_manager.py | |
| ---------------------- | |
| Round-robin API key rotation untuk Gemini via LiteLLM, dengan fallback | |
| otomatis dan cooldown khusus untuk limit RPD (Requests Per Day). | |
| CARA PAKAI DI HUGGING FACE SPACES: | |
| - Buka Settings > Variables and secrets pada Space Anda. | |
| - Tambahkan secret dengan Name & Value: | |
| GEMINI_API_KEY_1 = AIza...key1 | |
| GEMINI_API_KEY_2 = AIza...key2 | |
| GEMINI_API_KEY_3 = AIza...key3 | |
| GEMINI_API_KEY_4 = AIza...key4 | |
| GEMINI_API_KEY_5 = AIza...key5 | |
| (boleh 1 sampai berapa pun key, penomoran harus berurutan mulai dari 1, | |
| tidak boleh ada nomor yang loncat/kosong di tengah). | |
| - Modul ini otomatis mendeteksi semua GEMINI_API_KEY_<n> yang ada. | |
| """ | |
| import os | |
| import re | |
| import threading | |
| import logging | |
| from datetime import datetime, timedelta, timezone | |
| from typing import Dict, List, Optional | |
| logger = logging.getLogger(__name__) | |
| # Jam reset kuota harian Gemini (RPD) menurut Google: 08:05 UTC | |
| RPD_RESET_HOUR_UTC = 8 | |
| RPD_RESET_MINUTE_UTC = 5 | |
| class GeminiKeyManager: | |
| def __init__(self, prefix: str = "GEMINI_API_KEY_"): | |
| keys: List[str] = [] | |
| i = 1 | |
| while True: | |
| val = os.environ.get(f"{prefix}{i}") | |
| if not val: | |
| break | |
| keys.append(val.strip()) | |
| i += 1 | |
| # fallback: kalau tidak ada GEMINI_API_KEY_1, coba GEMINI_API_KEY tunggal | |
| if not keys: | |
| single = os.environ.get("GEMINI_API_KEY") | |
| if single: | |
| keys = [single.strip()] | |
| if not keys: | |
| raise RuntimeError( | |
| "Tidak ada Gemini API key ditemukan. " | |
| "Set GEMINI_API_KEY_1, GEMINI_API_KEY_2, ... di Variables and secrets." | |
| ) | |
| self.keys = keys | |
| self.n = len(keys) | |
| self._lock = threading.Lock() | |
| self._idx = 0 | |
| # key -> datetime (UTC) sampai kapan key ini "istirahat" karena RPD habis | |
| self._cooldown_until: Dict[str, Optional[datetime]] = {k: None for k in keys} | |
| logger.info(f"GeminiKeyManager aktif dengan {self.n} API key.") | |
| # ------------------------------------------------------------------ | |
| # Helper waktu reset | |
| # ------------------------------------------------------------------ | |
| def _next_rpd_reset(self) -> datetime: | |
| now = datetime.now(timezone.utc) | |
| reset = now.replace( | |
| hour=RPD_RESET_HOUR_UTC, minute=RPD_RESET_MINUTE_UTC, | |
| second=0, microsecond=0, | |
| ) | |
| if now >= reset: | |
| reset += timedelta(days=1) | |
| return reset | |
| def _is_available(self, key: str) -> bool: | |
| until = self._cooldown_until.get(key) | |
| if until is None: | |
| return True | |
| if datetime.now(timezone.utc) >= until: | |
| self._cooldown_until[key] = None # cooldown habis, key aktif lagi | |
| return True | |
| return False | |
| # ------------------------------------------------------------------ | |
| # API utama | |
| # ------------------------------------------------------------------ | |
| def get_rotation_order(self) -> List[str]: | |
| """ | |
| Kembalikan daftar key sesuai urutan giliran, dimulai dari key | |
| berikutnya dalam rotasi. Pointer selalu maju setiap kali method | |
| ini dipanggil (baik request berhasil maupun gagal), sehingga | |
| beban merata di semua user/panggilan. | |
| """ | |
| with self._lock: | |
| order = [self.keys[(self._idx + i) % self.n] for i in range(self.n)] | |
| self._idx = (self._idx + 1) % self.n | |
| return order | |
| def mark_rpd_exhausted(self, key: str) -> None: | |
| """Tandai key kena limit harian (RPD) -> istirahat sampai reset berikutnya.""" | |
| with self._lock: | |
| reset_at = self._next_rpd_reset() | |
| self._cooldown_until[key] = reset_at | |
| logger.warning(f"Key ...{key[-6:]} kena limit RPD, istirahat sampai {reset_at.isoformat()}") | |
| def status(self) -> Dict[str, str]: | |
| now = datetime.now(timezone.utc) | |
| out = {} | |
| for k in self.keys: | |
| until = self._cooldown_until.get(k) | |
| if until and now < until: | |
| out[f"...{k[-6:]}"] = f"cooldown until {until.isoformat()}" | |
| else: | |
| out[f"...{k[-6:]}"] = "available" | |
| return out | |
| # ------------------------------------------------------------------ | |
| # Deteksi jenis error dari Gemini / LiteLLM | |
| # ------------------------------------------------------------------ | |
| # Catatan: format pesan error Google untuk RPD biasanya mengandung kata | |
| # "PerDay" pada quotaId, mis. "GenerateRequestsPerDayPerProjectPerModel-FreeTier". | |
| # Untuk RPM/TPM mengandung "PerMinute". Kalau tidak yakin (ambigu), kita | |
| # anggap BUKAN rpd (lebih aman -> key tidak dikunci seharian secara keliru). | |
| RPD_PATTERNS = [r"PerDay", r"per[_ ]?day", r"daily limit", r"requests per day"] | |
| RATE_LIMIT_PATTERNS = [r"RESOURCE_EXHAUSTED", r"429", r"rate limit", r"quota"] | |
| def classify_gemini_error(exc: Exception) -> str: | |
| """Return 'rpd', 'rpm', atau 'other'.""" | |
| msg = str(exc) | |
| is_rate_limit = any(re.search(p, msg, re.IGNORECASE) for p in RATE_LIMIT_PATTERNS) | |
| if not is_rate_limit: | |
| return "other" | |
| if any(re.search(p, msg, re.IGNORECASE) for p in RPD_PATTERNS): | |
| return "rpd" | |
| return "rpm" | |