Files
contracts-app/site/llm_client.py
T
naeel 59048308e4 fix(llm): httpx с HTTP/2 вместо requests
api.aillm.ru за ddos-guard — требует HTTP/2. requests (HTTP/1.1) не работал.
httpx[http2] — поддержка HTTP/2, работает с ddos-guard.
2026-06-14 07:27:16 +04:00

85 lines
2.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
llm_client.py — Тонкий HTTP-клиент к LLM API.
Только отправить промпт → получить ответ. Никакой бизнес-логики.
Бизнес-логика (промпты, парсинг ответа) → extractor.py.
Использует httpx (HTTP/2) — api.aillm.ru за ddos-guard, требует HTTP/2.
Переменные окружения:
LLM_API_URL — URL API (по умолчанию https://api.aillm.ru/v1/chat/completions)
LLM_API_KEY — ключ авторизации
"""
import os
import httpx
# ── Конфигурация ────────────────────────────────────────────────
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.
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 = {
"model": model,
"messages": [
{"role": "user", "content": prompt},
],
"max_tokens": max_tokens,
"temperature": 0.1, # низкая температура = стабильный формат ответа
}
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
try:
# httpx с HTTP/2 — обязательно для ddos-guard
with httpx.Client(http2=True, timeout=120) as client:
resp = client.post(api_url, json=payload, headers=headers)
resp.raise_for_status()
data = resp.json()
# 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 httpx.TimeoutException:
return {"error": "LLM timeout (120s)"}
except httpx.HTTPStatusError as e:
return {"error": f"LLM HTTP {e.response.status_code}: {e.response.text[:500]}"}
except httpx.RequestError as e:
return {"error": f"LLM request failed: {e}"}
except Exception as e:
return {"error": f"LLM unexpected: {e}"}