diff --git a/config/settings.py b/config/settings.py index e7c0c1a..8c5808d 100644 --- a/config/settings.py +++ b/config/settings.py @@ -50,6 +50,8 @@ def load_config(): os.getenv("CLOVER_MAX_HISTORY_MESSAGES", "20") ), "max_history_tokens": int(os.getenv("CLOVER_MAX_HISTORY_TOKENS", "8000")), + "max_retries": int(os.getenv("CLOVER_MAX_RETRIES", "3")), + "retry_base_delay": float(os.getenv("CLOVER_RETRY_BASE_DELAY", "2")), } # Debug output if enabled diff --git a/models/api_client.py b/models/api_client.py index b3f7d31..fc7fb80 100644 --- a/models/api_client.py +++ b/models/api_client.py @@ -5,6 +5,7 @@ Handles communication with the OpenAI-compatible LLM server import json import os +import time from typing import Any, Dict, Optional import requests @@ -55,7 +56,10 @@ class APIClient: self, endpoint: str, method: str = "GET", data: Optional[Dict] = None ) -> Dict[str, Any]: """ - Make a request to the LLM API server + Make a request to the LLM API server with retry and exponential backoff. + + Retries up to 3 times with exponential backoff (2s base delay) for + transient failures (5xx errors, connection errors, timeouts). Uses requests.Session with explicit SSL verification control. @@ -67,33 +71,74 @@ class APIClient: Returns: dict: Response from the API """ - try: - # Ensure base_url doesn't have trailing slash and endpoint has leading slash - base = self.base_url.rstrip("/") - endpoint = endpoint if endpoint.startswith("/") else f"/{endpoint}" - url = f"{base}{endpoint}" + max_retries = self.config.get("max_retries", 3) + base_delay = self.config.get("retry_base_delay", 2) - kwargs = { - "timeout": self.config.get("timeout", 300), - "verify": self.ssl_verify, + last_exception = None + + for attempt in range(max_retries + 1): + try: + kwargs = { + "timeout": self.config.get("timeout", 300), + "verify": self.ssl_verify, + } + + if method == "GET": + response = self.session.get(url, **kwargs) + elif method == "POST": + response = self.session.post(url, json=data, **kwargs) + else: + raise ValueError(f"Unsupported HTTP method: {method}") + + # Retry on 5xx server errors + if response.status_code >= 500 and attempt < max_retries: + delay = base_delay * (2**attempt) + print( + f"Server error {response.status_code}, " + f"retrying in {delay}s (attempt {attempt + 1}/{max_retries})" + ) + time.sleep(delay) + continue + + response.raise_for_status() + return {"success": True, "data": response.json()} + + except requests.exceptions.Timeout as e: + last_exception = e + if attempt < max_retries: + delay = base_delay * (2**attempt) + print( + f"Timeout, retrying in {delay}s " + f"(attempt {attempt + 1}/{max_retries})" + ) + time.sleep(delay) + continue + + except requests.exceptions.ConnectionError as e: + last_exception = e + if attempt < max_retries: + delay = base_delay * (2**attempt) + print( + f"Connection error, retrying in {delay}s " + f"(attempt {attempt + 1}/{max_retries})" + ) + time.sleep(delay) + continue + + except requests.exceptions.RequestException as e: + return {"success": False, "error": f"HTTP Request failed: {str(e)}"} + except json.JSONDecodeError as e: + return {"success": False, "error": f"Invalid JSON response: {str(e)}"} + except Exception as e: + return {"success": False, "error": f"Unexpected error: {str(e)}"} + + # All retries exhausted + if last_exception: + return { + "success": False, + "error": f"Request failed after {max_retries} retries: {str(last_exception)}", } - - if method == "GET": - response = self.session.get(url, **kwargs) - elif method == "POST": - response = self.session.post(url, json=data, **kwargs) - else: - raise ValueError(f"Unsupported HTTP method: {method}") - - response.raise_for_status() - return {"success": True, "data": response.json()} - - except requests.exceptions.RequestException as e: - return {"success": False, "error": f"HTTP Request failed: {str(e)}"} - except json.JSONDecodeError as e: - return {"success": False, "error": f"Invalid JSON response: {str(e)}"} - except Exception as e: - return {"success": False, "error": f"Unexpected error: {str(e)}"} + return {"success": False, "error": "Request failed after maximum retries"} def list_models(self) -> Dict[str, Any]: """