import logging import random import requests from .model_map import build_model_index, get_shuffled_combos, build_provider_index logger = logging.getLogger(__name__) class APIHub: def __init__(self, providers: list): self.providers = build_provider_index(providers) self.model_index = build_model_index(providers) self._call_history = [] def chat_completion(self, slot: str, messages: list[dict], params: dict, max_tokens: int = 900) -> str: def _call(provider, model): return provider.chat_completion(model, messages, params, max_tokens) return self._try_combos(slot, _call) def text_completion(self, slot: str, prompt: str, params: dict, max_tokens: int = 900) -> str: def _call(provider, model): return provider.text_completion(model, prompt, params, max_tokens) return self._try_combos(slot, _call) def _try_combos(self, slot: str, fn) -> str: combos = get_shuffled_combos(self.model_index, slot) if not combos: raise RuntimeError(f'no models available for slot "{slot}"') tried = set() for pname, actual_model in combos: key = (pname, actual_model) if key in tried: continue tried.add(key) provider = self.providers.get(pname) if not provider: continue for attempt in range(5): try: result = fn(provider, actual_model) self._call_history.append({ 'provider': pname, 'model': actual_model, 'slot': slot, }) return result except requests.HTTPError as e: is_429 = e.response.status_code == 429 if hasattr(e, 'response') else False if is_429: logger.warning(f'{pname}/{actual_model} 429 rate limit (attempt {attempt+1})') else: logger.warning(f'{pname}/{actual_model} HTTP {e.response.status_code} (attempt {attempt+1})') provider._backoff(attempt, is_429=is_429) except Exception as e: logger.warning(f'{pname}/{actual_model} attempt {attempt+1} failed: {e}') provider._backoff(attempt) raise RuntimeError(f'all models/providers failed for slot "{slot}"')