"""Local Gemma (llama-server, OpenAI-compatible) with optional Groq fallback. Plain text only.""" from __future__ import annotations import json import os import urllib.request LOCAL = os.environ.get("NEWS_LLM_URL", "http://127.0.0.1:11434/v1/chat/completions") MODEL = os.environ.get("NEWS_LLM_MODEL", "gemma3:4b") GROQ_KEY_FILE = os.environ.get("NEWS_GROQ_KEY_FILE", "") GROQ_MODEL = os.environ.get("NEWS_GROQ_MODEL", "llama-3.1-8b-instant") def _post(url, payload, headers, timeout): req = urllib.request.Request(url, json.dumps(payload).encode(), {"Content-Type": "application/json", **headers}) with urllib.request.urlopen(req, timeout=timeout) as r: # fixed, configured endpoints only return json.loads(r.read())["choices"][0]["message"]["content"] def chat(system: str, user: str, *, max_tokens: int = 300, timeout: float = 180) -> str: msgs = [{"role": "system", "content": system}, {"role": "user", "content": user}] try: return _post(LOCAL, {"model": MODEL, "messages": msgs, "max_tokens": max_tokens, "temperature": 0.3}, {}, timeout).strip() except Exception as exc: # noqa: BLE001 if GROQ_KEY_FILE and os.path.exists(GROQ_KEY_FILE): key = open(GROQ_KEY_FILE).read().strip() return _post("https://api.groq.com/openai/v1/chat/completions", {"model": GROQ_MODEL, "messages": msgs, "max_tokens": max_tokens, "temperature": 0.3}, {"Authorization": "Bearer " + key}, 60).strip() raise RuntimeError("llm_unavailable") from exc