fix: add retry with exponential backoff for API calls (issue #9)
- Retry up to 3 times with exponential backoff (2s base delay) - Retries on 5xx errors, timeouts, and connection errors - Configurable via CLOVER_MAX_RETRIES and CLOVER_RETRY_BASE_DELAY - Logs retry attempts with delay and attempt number
This commit is contained in:
parent
b25fb3c1c6
commit
4a676a604a
@ -43,6 +43,8 @@ def load_config():
|
|||||||
"project_root": os.getenv("CLOVER_PROJECT_ROOT", "."),
|
"project_root": os.getenv("CLOVER_PROJECT_ROOT", "."),
|
||||||
"summary_file": os.getenv("CLOVER_SUMMARY_FILE", "clover.md"),
|
"summary_file": os.getenv("CLOVER_SUMMARY_FILE", "clover.md"),
|
||||||
"structure_file": os.getenv("CLOVER_STRUCTURE_FILE", "structure.md"),
|
"structure_file": os.getenv("CLOVER_STRUCTURE_FILE", "structure.md"),
|
||||||
|
"max_retries": int(os.getenv("CLOVER_MAX_RETRIES", "3")),
|
||||||
|
"retry_base_delay": float(os.getenv("CLOVER_RETRY_BASE_DELAY", "2")),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Debug output if enabled
|
# Debug output if enabled
|
||||||
|
|||||||
@ -5,6 +5,7 @@ Handles communication with the OpenAI-compatible LLM server
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@ -27,7 +28,10 @@ class APIClient:
|
|||||||
self, endpoint: str, method: str = "GET", data: Optional[Dict] = None
|
self, endpoint: str, method: str = "GET", data: Optional[Dict] = None
|
||||||
) -> Dict[str, Any]:
|
) -> 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).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
endpoint (str): API endpoint
|
endpoint (str): API endpoint
|
||||||
@ -37,6 +41,12 @@ class APIClient:
|
|||||||
Returns:
|
Returns:
|
||||||
dict: Response from the API
|
dict: Response from the API
|
||||||
"""
|
"""
|
||||||
|
max_retries = self.config.get("max_retries", 3)
|
||||||
|
base_delay = self.config.get("retry_base_delay", 2)
|
||||||
|
|
||||||
|
last_exception = None
|
||||||
|
|
||||||
|
for attempt in range(max_retries + 1):
|
||||||
try:
|
try:
|
||||||
# Ensure base_url doesn't have trailing slash and endpoint has leading slash
|
# Ensure base_url doesn't have trailing slash and endpoint has leading slash
|
||||||
base = self.base_url.rstrip("/")
|
base = self.base_url.rstrip("/")
|
||||||
@ -55,7 +65,9 @@ class APIClient:
|
|||||||
|
|
||||||
if method == "GET":
|
if method == "GET":
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
url, headers=headers, timeout=self.config.get("timeout", 300)
|
url,
|
||||||
|
headers=headers,
|
||||||
|
timeout=self.config.get("timeout", 300),
|
||||||
)
|
)
|
||||||
elif method == "POST":
|
elif method == "POST":
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
@ -67,9 +79,41 @@ class APIClient:
|
|||||||
else:
|
else:
|
||||||
raise ValueError(f"Unsupported HTTP method: {method}")
|
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()
|
response.raise_for_status()
|
||||||
return {"success": True, "data": response.json()}
|
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:
|
except requests.exceptions.RequestException as e:
|
||||||
return {"success": False, "error": f"HTTP Request failed: {str(e)}"}
|
return {"success": False, "error": f"HTTP Request failed: {str(e)}"}
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
@ -77,6 +121,14 @@ class APIClient:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"success": False, "error": f"Unexpected error: {str(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)}",
|
||||||
|
}
|
||||||
|
return {"success": False, "error": "Request failed after maximum retries"}
|
||||||
|
|
||||||
def list_models(self) -> Dict[str, Any]:
|
def list_models(self) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Get list of available models from the LLM server
|
Get list of available models from the LLM server
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user