| | """ |
| | GroqKeyManager — round-robin key rotation with immediate failover on 429. |
| | |
| | Usage: |
| | mgr = GroqKeyManager([KEY_1, KEY_2]) |
| | key = mgr.current() # get current key |
| | key = mgr.rotate() # advance to next key (call on 429) |
| | llm = mgr.build_llm(model) # ChatGroq with current key |
| | """ |
| | import threading |
| | import time |
| | import logging |
| | from typing import List |
| |
|
| | from langchain_groq import ChatGroq |
| |
|
| | logger = logging.getLogger(__name__) |
| |
|
| |
|
| | class GroqKeyManager: |
| | """Thread-safe round-robin Groq API key manager.""" |
| |
|
| | def __init__(self, keys: List[str], model: str = "llama-3.3-70b-versatile"): |
| | self._keys = [k.strip() for k in keys if k and k.strip()] |
| | if not self._keys: |
| | raise ValueError("GroqKeyManager: no valid API keys provided") |
| | self._model = model |
| | self._idx = 0 |
| | self._lock = threading.Lock() |
| | |
| | self._cooldown: dict[str, float] = {} |
| | logger.info(f"[KeyManager] {len(self._keys)} Groq key(s) loaded, model={model}") |
| |
|
| | def current(self) -> str: |
| | with self._lock: |
| | return self._keys[self._idx % len(self._keys)] |
| |
|
| | def rotate(self) -> str: |
| | """Advance to next available (non-cooled-down) key. Returns the new key.""" |
| | with self._lock: |
| | now = time.time() |
| | for _ in range(len(self._keys)): |
| | self._idx = (self._idx + 1) % len(self._keys) |
| | key = self._keys[self._idx] |
| | if now >= self._cooldown.get(key, 0): |
| | logger.warning(f"[KeyManager] Rotated to key index {self._idx}") |
| | return key |
| | |
| | logger.warning("[KeyManager] All keys on cooldown, returning current key") |
| | return self._keys[self._idx % len(self._keys)] |
| |
|
| | def mark_rate_limited(self, key: str, cooldown_secs: int = 62): |
| | """Mark a key as rate-limited for cooldown_secs seconds.""" |
| | with self._lock: |
| | self._cooldown[key] = time.time() + cooldown_secs |
| | logger.warning(f"[KeyManager] Key ...{key[-6:]} cooled down for {cooldown_secs}s") |
| |
|
| | def build_llm(self, temperature: float = 0) -> ChatGroq: |
| | """Return a ChatGroq instance using the current key.""" |
| | return ChatGroq( |
| | model=self._model, |
| | api_key=self.current(), |
| | temperature=temperature, |
| | max_tokens=800, |
| | ) |
| |
|