feat: group private apps into andreknie-privat
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
"""LiteLLM wrapper for model-agnostic LLM access.
|
||||
|
||||
Supports OpenAI, Anthropic, Google, Mistral, and Ollama via a unified interface.
|
||||
Implements retry logic with exponential backoff for transient failures.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ingestion.config import IngestionConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Retry configuration
|
||||
MAX_RETRIES = 3
|
||||
BASE_DELAY_SECONDS = 1.0
|
||||
MAX_DELAY_SECONDS = 30.0
|
||||
BACKOFF_FACTOR = 2.0
|
||||
|
||||
|
||||
class LLMError(Exception):
|
||||
"""Raised when an LLM call fails after all retries."""
|
||||
|
||||
def __init__(self, provider: str, model: str, message: str):
|
||||
self.provider = provider
|
||||
self.model = model
|
||||
super().__init__(
|
||||
f"LLM call failed [{provider}/{model}]: {message}"
|
||||
)
|
||||
|
||||
|
||||
class LLMClient:
|
||||
"""Model-agnostic LLM client using LiteLLM."""
|
||||
|
||||
def __init__(self, config: IngestionConfig):
|
||||
self.model = config.llm_model_string
|
||||
self.provider = config.llm_provider
|
||||
self.config = config
|
||||
|
||||
def complete(
|
||||
self,
|
||||
messages: list[dict[str, str]],
|
||||
response_format: type[BaseModel] | None = None,
|
||||
) -> str:
|
||||
"""Send a completion request with retry logic.
|
||||
|
||||
Args:
|
||||
messages: List of message dicts with 'role' and 'content' keys.
|
||||
response_format: Optional Pydantic model for structured output.
|
||||
|
||||
Returns:
|
||||
The assistant's response content as a string.
|
||||
|
||||
Raises:
|
||||
LLMError: If all retries are exhausted.
|
||||
"""
|
||||
import litellm
|
||||
|
||||
last_error: Exception | None = None
|
||||
|
||||
for attempt in range(1, MAX_RETRIES + 1):
|
||||
try:
|
||||
kwargs: dict[str, Any] = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
}
|
||||
|
||||
if response_format is not None:
|
||||
kwargs["response_format"] = response_format
|
||||
|
||||
response = litellm.completion(**kwargs)
|
||||
content = response.choices[0].message.content
|
||||
return content or ""
|
||||
|
||||
except (
|
||||
litellm.RateLimitError,
|
||||
litellm.Timeout,
|
||||
litellm.ServiceUnavailableError,
|
||||
litellm.InternalServerError,
|
||||
) as e:
|
||||
last_error = e
|
||||
delay = min(
|
||||
BASE_DELAY_SECONDS * (BACKOFF_FACTOR ** (attempt - 1)),
|
||||
MAX_DELAY_SECONDS,
|
||||
)
|
||||
logger.warning(
|
||||
"LLM call attempt %d/%d failed (%s: %s), retrying in %.1fs",
|
||||
attempt,
|
||||
MAX_RETRIES,
|
||||
type(e).__name__,
|
||||
str(e),
|
||||
delay,
|
||||
)
|
||||
time.sleep(delay)
|
||||
|
||||
except Exception as e:
|
||||
# Non-retryable error
|
||||
raise LLMError(
|
||||
provider=self.provider,
|
||||
model=self.model,
|
||||
message=f"{type(e).__name__}: {e}",
|
||||
) from e
|
||||
|
||||
# All retries exhausted
|
||||
raise LLMError(
|
||||
provider=self.provider,
|
||||
model=self.model,
|
||||
message=f"All {MAX_RETRIES} retries exhausted. Last error: {last_error}",
|
||||
)
|
||||
Reference in New Issue
Block a user