TenderIQ / core /llm_client.py
JaydeepR's picture
Step 4: LLM client — DeepSeek wrapper with retry and LLMUnavailable
c419ba2
import base64
import json
import time
from pathlib import Path
import openai
from core.config import DEEPSEEK_API_KEY, DEEPSEEK_BASE_URL, MODEL_NAME
class LLMUnavailable(Exception):
pass
class LLM:
def __init__(self, api_key: str | None = None):
resolved = api_key if api_key is not None else DEEPSEEK_API_KEY
self._api_key = resolved
self._client = openai.OpenAI(
api_key=resolved or "no-key",
base_url=DEEPSEEK_BASE_URL,
)
def _check_key(self) -> None:
if not self._api_key:
raise LLMUnavailable("No API key configured")
def chat_json(self, system: str, user: str, max_retries: int = 2) -> dict:
self._check_key()
last_exc: Exception | None = None
for attempt in range(max_retries + 1):
try:
resp = self._client.chat.completions.create(
model=MODEL_NAME,
messages=[
{"role": "system", "content": system},
{"role": "user", "content": user},
],
temperature=0,
response_format={"type": "json_object"},
)
content = resp.choices[0].message.content or ""
try:
return json.loads(content)
except json.JSONDecodeError:
if attempt < max_retries:
system = system + " Respond ONLY with valid JSON, no prose."
continue
raise LLMUnavailable("Malformed JSON after retries")
except openai.AuthenticationError as e:
raise LLMUnavailable("Invalid API key") from e
except (openai.APIStatusError, openai.APIConnectionError) as e:
last_exc = e
if attempt < max_retries:
time.sleep(2 ** attempt)
continue
raise LLMUnavailable(f"API error after retries: {last_exc}") from last_exc
def chat_vision(
self,
system: str,
user_text: str,
image: bytes | str | Path,
max_retries: int = 2,
) -> str:
self._check_key()
if isinstance(image, (str, Path)):
raw = Path(image).read_bytes()
else:
raw = image
b64 = base64.b64encode(raw).decode()
data_uri = f"data:image/png;base64,{b64}"
last_exc: Exception | None = None
for attempt in range(max_retries + 1):
try:
resp = self._client.chat.completions.create(
model=MODEL_NAME,
messages=[
{"role": "system", "content": system},
{"role": "user", "content": [
{"type": "text", "text": user_text},
{"type": "image_url", "image_url": {"url": data_uri}},
]},
],
temperature=0,
)
return resp.choices[0].message.content or ""
except openai.AuthenticationError as e:
raise LLMUnavailable("Invalid API key") from e
except (openai.APIStatusError, openai.APIConnectionError) as e:
last_exc = e
if attempt < max_retries:
time.sleep(2 ** attempt)
continue
raise LLMUnavailable(f"API error after retries: {last_exc}") from last_exc