import time class UpstreamUnavailableError(Exception): """Raised when the LLM provider fails even after retries (e.g. persistent 503s).""" pass def invoke_with_retry(chain, payload: dict, max_retries: int = 3, base_delay: float = 1.5): """Invokes a langchain chain with exponential backoff on transient failures. The Hugging Face router + featherless-ai provider can return a 503 "Service Unavailable" HTML page instead of JSON during cold starts or overload. The OpenAI-compatible client then throws while trying to parse that as a chat completion. Retrying with backoff handles the common transient case instead of failing the whole turn on the first hiccup. """ last_error = None for attempt in range(1, max_retries + 1): try: return chain.invoke(payload) except Exception as e: last_error = e is_last = attempt == max_retries print(f"[llm_utils] attempt {attempt}/{max_retries} failed: {e}") if not is_last: time.sleep(base_delay * (2 ** (attempt - 1))) # 1.5s, 3s, 6s... raise UpstreamUnavailableError(str(last_error)) from last_error