Sub / app /utils /api_key_runtime.py
Himankpro's picture
Upload 93 files
3a6c182 verified
Raw History Blame Contribute Delete
4.27 kB
"""
Shared LLM + Zep API key slot (same index for both) with rate-limit handling:
try primary → on limit try secondary → on limit sleep 60s → retry from primary once more.
"""
from __future__ import annotations
import contextvars
import time
from typing import Any, Callable, TypeVar
from ..config import Config
from ..utils.logger import get_logger
logger = get_logger("mirofish.api_key_runtime")
T = TypeVar("T")
_key_slot: contextvars.ContextVar[int] = contextvars.ContextVar("miro_api_key_slot", default=0)
def reset_pipeline_api_keys() -> None:
"""Call at the start of each full pipeline run (worker thread)."""
_key_slot.set(0)
def current_key_slot() -> int:
return _key_slot.get()
def _max_key_slots() -> int:
lk = Config.get_llm_api_keys()
zk = Config.get_zep_api_keys()
n = max(len(lk or []), len(zk or []), 1)
return n
def set_key_slot(i: int) -> None:
n = _max_key_slots()
_key_slot.set(max(0, min(i, n - 1)))
def get_llm_key() -> str | None:
keys = Config.get_llm_api_keys()
if not keys:
return None
i = min(_key_slot.get(), len(keys) - 1)
return keys[i]
def get_zep_key() -> str | None:
keys = Config.get_zep_api_keys()
if not keys:
return None
i = min(_key_slot.get(), len(keys) - 1)
return keys[i]
def is_rate_limit_error(exc: BaseException) -> bool:
try:
import openai
if isinstance(exc, getattr(openai, "RateLimitError", ())):
return True
if isinstance(exc, getattr(openai, "APIStatusError", ())):
if getattr(exc, "status_code", None) == 429:
return True
except Exception:
pass
st = getattr(exc, "status_code", None)
if st == 429:
return True
msg = str(exc).lower()
for phrase in (
"rate limit",
"429",
"too many requests",
"quota",
"resource_exhausted",
"resource exhausted",
"throttl",
"over capacity",
"capacity",
):
if phrase in msg:
return True
return False
def is_auth_or_invalid_key_error(exc: BaseException) -> bool:
"""401 / invalid API key — rotate to next paired slot when LLM_2/ZEP_2 exist."""
try:
import openai
if isinstance(exc, getattr(openai, "AuthenticationError", ())):
return True
if isinstance(exc, getattr(openai, "APIStatusError", ())):
if getattr(exc, "status_code", None) == 401:
return True
except Exception:
pass
if getattr(exc, "status_code", None) == 401:
return True
msg = str(exc).lower()
if "invalid api key" in msg or "invalid_api_key" in msg:
return True
if "401" in msg and ("unauthorized" in msg or "authentication" in msg):
return True
return False
def should_rotate_api_key(exc: BaseException) -> bool:
return is_rate_limit_error(exc) or is_auth_or_invalid_key_error(exc)
def call_with_limit_rotation(op: Callable[[], T]) -> T:
"""
Run op() using keys at the current slot. On rate-limit:
advance to next key slot if available; else sleep 60s, reset to slot 0, try again;
if still rate-limited after that full cycle, re-raise.
"""
cooldown_used = False
last_exc: BaseException | None = None
while True:
try:
return op()
except BaseException as e:
if not should_rotate_api_key(e):
raise
last_exc = e
idx = current_key_slot()
n = _max_key_slots()
if idx + 1 < n:
set_key_slot(idx + 1)
logger.warning(
"Rate limit hit; switching API key slot to %s (LLM+Zep)", current_key_slot()
)
continue
if not cooldown_used:
logger.warning("Rate limit on all API key slots; sleeping 60s then retrying from slot 0")
time.sleep(60)
cooldown_used = True
set_key_slot(0)
continue
raise last_exc from None