httpx требует h2, который не ставится в облаке. curl есть везде и гарантированно держит HTTP/2. subprocess.run(['curl','-s','--http2',...])
91 lines
3.1 KiB
Python
91 lines
3.1 KiB
Python
"""
|
|
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}"}
|