190 lines
6.5 KiB
Python
190 lines
6.5 KiB
Python
import httpx
|
|
import warnings
|
|
from typing import Optional
|
|
from config import settings
|
|
from logger import get_logger
|
|
|
|
warnings.filterwarnings("ignore", message="Unverified HTTPS request")
|
|
|
|
log = get_logger("ollama_client")
|
|
|
|
PROVIDER_BASE_URLS = {
|
|
"openrouter": "https://openrouter.ai/api/v1",
|
|
"openai": "https://api.openai.com/v1",
|
|
"groq": "https://api.groq.com/openai/v1",
|
|
"opencode": "https://opencode.ai/zen/v1",
|
|
}
|
|
|
|
|
|
class LLMClient:
|
|
def __init__(self):
|
|
self.provider = settings.LLM_PROVIDER
|
|
self.timeout = httpx.Timeout(300.0, connect=30.0)
|
|
log.info(f"Initializing LLMClient: provider={self.provider}")
|
|
|
|
if self.provider == "ollama":
|
|
self.host = settings.OLLAMA_HOST
|
|
log.debug(f"Ollama host: {self.host}")
|
|
self.client = httpx.Client(timeout=self.timeout, verify=False)
|
|
else:
|
|
base_url = settings.LLM_BASE_URL or PROVIDER_BASE_URLS.get(self.provider, "")
|
|
api_key = settings.LLM_API_KEY or ""
|
|
log.debug(f"External API: {self.provider} -> {base_url}")
|
|
self.client = httpx.Client(
|
|
timeout=self.timeout,
|
|
base_url=base_url,
|
|
headers={
|
|
"Authorization": f"Bearer {api_key}",
|
|
"Content-Type": "application/json"
|
|
}
|
|
)
|
|
if self.provider == "openrouter":
|
|
self.client.headers["HTTP-Referer"] = "https://github.com/ragger"
|
|
self.client.headers["X-Title"] = "RAGGER"
|
|
|
|
def embed(self, text: str) -> list[float]:
|
|
model = settings.OLLAMA_EMBED_MODEL
|
|
log.debug(f"Embedding: model={model}, text_len={len(text)}")
|
|
|
|
url = f"{settings.OLLAMA_HOST.rstrip('/')}/api/embeddings"
|
|
log.debug(f"POST {url}")
|
|
|
|
try:
|
|
response = self.client.post(
|
|
url,
|
|
json={"model": model, "prompt": text}
|
|
)
|
|
response.raise_for_status()
|
|
embedding = response.json()["embedding"]
|
|
log.debug(f"Embedding success: {len(embedding)} dims")
|
|
return embedding
|
|
except httpx.HTTPStatusError as e:
|
|
log.error(f"HTTP error: {e.response.status_code} - {e.response.text}")
|
|
raise
|
|
except Exception as e:
|
|
log.error(f"Embedding error: {e}")
|
|
raise
|
|
|
|
def generate(
|
|
self,
|
|
prompt: str,
|
|
system: Optional[str] = None,
|
|
context: Optional[str] = None
|
|
) -> str:
|
|
log.debug(f"Generate: provider={self.provider}, prompt_len={len(prompt)}")
|
|
|
|
if self.provider == "ollama":
|
|
return self._generate_ollama(prompt, system, context)
|
|
else:
|
|
return self._generate_openai(prompt, system, context)
|
|
|
|
def _generate_ollama(
|
|
self,
|
|
prompt: str,
|
|
system: Optional[str] = None,
|
|
context: Optional[str] = None
|
|
) -> str:
|
|
model = settings.OLLAMA_MODEL
|
|
log.debug(f"Ollama generate: model={model}")
|
|
|
|
full_prompt = ""
|
|
if system:
|
|
full_prompt += f"System: {system}\n\n"
|
|
if context:
|
|
full_prompt += f"Contesto:\n{context}\n\n"
|
|
full_prompt += prompt
|
|
|
|
log.debug(f"Full prompt length: {len(full_prompt)} chars")
|
|
|
|
url = f"{settings.OLLAMA_HOST.rstrip('/')}/api/generate"
|
|
log.debug(f"POST {url}")
|
|
|
|
try:
|
|
response = self.client.post(
|
|
url,
|
|
json={"model": model, "prompt": full_prompt, "stream": False}
|
|
)
|
|
response.raise_for_status()
|
|
result = response.json()["response"]
|
|
log.debug(f"Response length: {len(result)} chars")
|
|
log.info(f"Generated response ({len(result)} chars)")
|
|
return result
|
|
except httpx.HTTPStatusError as e:
|
|
log.error(f"HTTP error: {e.response.status_code} - {e.response.text}")
|
|
raise
|
|
except Exception as e:
|
|
log.error(f"Generation error: {e}")
|
|
raise
|
|
|
|
def _generate_openai(
|
|
self,
|
|
prompt: str,
|
|
system: Optional[str] = None,
|
|
context: Optional[str] = None
|
|
) -> str:
|
|
model = settings.LLM_MODEL or "openrouter/auto"
|
|
log.debug(f"OpenAI generate: model={model}")
|
|
|
|
messages = []
|
|
if system:
|
|
messages.append({"role": "system", "content": system})
|
|
|
|
user_content = ""
|
|
if context:
|
|
user_content += f"Contesto:\n{context}\n\n"
|
|
user_content += prompt
|
|
messages.append({"role": "user", "content": user_content})
|
|
|
|
log.debug(f"Messages: {len(messages)} items")
|
|
|
|
try:
|
|
response = self.client.post(
|
|
"/chat/completions",
|
|
json={"model": model, "messages": messages}
|
|
)
|
|
response.raise_for_status()
|
|
result = response.json()["choices"][0]["message"]["content"]
|
|
log.info(f"Generated response ({len(result)} chars)")
|
|
return result
|
|
except httpx.HTTPStatusError as e:
|
|
log.error(f"HTTP error: {e.response.status_code} - {e.response.text}")
|
|
raise
|
|
except Exception as e:
|
|
log.error(f"Generation error: {e}")
|
|
raise
|
|
|
|
def is_available(self) -> bool:
|
|
log.debug("Checking availability...")
|
|
try:
|
|
if self.provider == "ollama":
|
|
url = f"{settings.OLLAMA_HOST.rstrip('/')}/api/tags"
|
|
log.debug(f"GET {url}")
|
|
response = self.client.get(url, timeout=5.0)
|
|
available = response.status_code == 200
|
|
log.debug(f"Ollama available: {available}")
|
|
return available
|
|
else:
|
|
log.debug("External API - assuming available")
|
|
return True
|
|
except Exception as e:
|
|
log.warning(f"Availability check failed: {e}")
|
|
return False
|
|
|
|
def list_models(self) -> list[str]:
|
|
if self.provider != "ollama":
|
|
return []
|
|
|
|
try:
|
|
url = f"{settings.OLLAMA_HOST.rstrip('/')}/api/tags"
|
|
response = self.client.get(url, timeout=5.0)
|
|
response.raise_for_status()
|
|
models = response.json().get("models", [])
|
|
return [m["name"] for m in models]
|
|
except Exception as e:
|
|
log.error(f"Error listing models: {e}")
|
|
return []
|
|
|
|
|
|
llm = LLMClient()
|
|
ollama = llm
|