Merge pull request #19: fix: add conversation history management (closes #9)

This commit is contained in:
opencode 2026-07-05 07:16:53 +00:00
commit 55ec1d663c
2 changed files with 73 additions and 26 deletions

View File

@ -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

View File

@ -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]:
"""