""" llm_client.py — Тонкий HTTP-клиент к LLM API. Только отправить промпт → получить ответ. Никакой бизнес-логики. Бизнес-логика (промпты, парсинг ответа) → extractor.py. Использует curl (HTTP/2) — api.aillm.ru за ddos-guard, требует HTTP/2. curl есть в любом контейнере и гарантированно работает с HTTP/2. Переменные окружения: LLM_API_URL — URL API (по умолчанию https://api.aillm.ru/v1/chat/completions) LLM_API_KEY — ключ авторизации """ import os import json import subprocess # ── Конфигурация ──────────────────────────────────────────────── DEFAULT_URL = "https://api.aillm.ru/v1/chat/completions" DEFAULT_MODEL = "gpt-oss-120b" # 120B модель через LiteLLM-прокси def ask(prompt: str, model: str = None, max_tokens: int = 8000) -> dict: """ Отправить промпт к LLM API через curl (HTTP/2). Args: prompt: текст промпта model: модель (None = DEFAULT_MODEL) max_tokens: максимум токенов в ответе Returns: {"text": "ответ модели", "model": "...", "usage": {...}} или {"error": "описание ошибки"} """ api_url = os.getenv("LLM_API_URL", DEFAULT_URL) api_key = os.getenv("LLM_API_KEY", "") if not api_key: return {"error": "LLM_API_KEY not set"} model = model or DEFAULT_MODEL payload = json.dumps({ "model": model, "messages": [{"role": "user", "content": prompt}], "max_tokens": max_tokens, "temperature": 0.1, }) try: # curl --http2 гарантирует HTTP/2 proc = subprocess.run( [ "curl", "-s", "--http2", "-H", f"Authorization: Bearer {api_key}", "-H", "Content-Type: application/json", "-H", "Accept: application/json", "--max-time", "120", "-d", payload, api_url, ], capture_output=True, text=True, timeout=130, ) if proc.returncode != 0: return {"error": f"curl exited with {proc.returncode}: {proc.stderr[:300]}"} data = json.loads(proc.stdout) # OpenAI-совместимый формат choice = data.get("choices", [{}])[0] message = choice.get("message", {}) text = message.get("content", "") return { "text": text, "model": data.get("model", model), "usage": data.get("usage", {}), } except subprocess.TimeoutExpired: return {"error": "LLM timeout (130s)"} except json.JSONDecodeError as e: return {"error": f"LLM bad JSON: {e}"} except Exception as e: return {"error": f"LLM unexpected: {e}"}