fix(llm): curl --http2 вместо httpx

httpx требует h2, который не ставится в облаке.
curl есть везде и гарантированно держит HTTP/2.
subprocess.run(['curl','-s','--http2',...])
This commit is contained in:
2026-06-14 07:40:12 +04:00
parent b457de4bb3
commit 099edbcb61
2 changed files with 32 additions and 28 deletions
-2
View File
@@ -5,5 +5,3 @@ requests
psycopg2-binary psycopg2-binary
python-dotenv python-dotenv
pdfplumber pdfplumber
httpx[http2]
h2
+32 -26
View File
@@ -4,7 +4,8 @@ llm_client.py — Тонкий HTTP-клиент к LLM API.
Только отправить промпт → получить ответ. Никакой бизнес-логики. Только отправить промпт → получить ответ. Никакой бизнес-логики.
Бизнес-логика (промпты, парсинг ответа) → extractor.py. Бизнес-логика (промпты, парсинг ответа) → extractor.py.
Использует httpx (HTTP/2) — api.aillm.ru за ddos-guard, требует HTTP/2. Использует 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_URL — URL API (по умолчанию https://api.aillm.ru/v1/chat/completions)
@@ -12,7 +13,8 @@ llm_client.py — Тонкий HTTP-клиент к LLM API.
""" """
import os import os
import httpx import json
import subprocess
# ── Конфигурация ──────────────────────────────────────────────── # ── Конфигурация ────────────────────────────────────────────────
@@ -22,7 +24,7 @@ DEFAULT_MODEL = "gpt-oss-120b" # 120B модель через LiteLLM-прок
def ask(prompt: str, model: str = None, max_tokens: int = 8000) -> dict: def ask(prompt: str, model: str = None, max_tokens: int = 8000) -> dict:
""" """
Отправить промпт к LLM API. Отправить промпт к LLM API через curl (HTTP/2).
Args: Args:
prompt: текст промпта prompt: текст промпта
@@ -42,28 +44,34 @@ def ask(prompt: str, model: str = None, max_tokens: int = 8000) -> dict:
model = model or DEFAULT_MODEL model = model or DEFAULT_MODEL
payload = { payload = json.dumps({
"model": model, "model": model,
"messages": [ "messages": [{"role": "user", "content": prompt}],
{"role": "user", "content": prompt},
],
"max_tokens": max_tokens, "max_tokens": max_tokens,
"temperature": 0.1, # низкая температура = стабильный формат ответа "temperature": 0.1,
} })
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
try: try:
# httpx с HTTP/2 — обязательно для ddos-guard # curl --http2 гарантирует HTTP/2
with httpx.Client(http2=True, timeout=120) as client: proc = subprocess.run(
resp = client.post(api_url, json=payload, headers=headers) [
resp.raise_for_status() "curl", "-s", "--http2",
data = resp.json() "-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,
)
# OpenAI-совместимый формат ответа 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] choice = data.get("choices", [{}])[0]
message = choice.get("message", {}) message = choice.get("message", {})
text = message.get("content", "") text = message.get("content", "")
@@ -74,11 +82,9 @@ def ask(prompt: str, model: str = None, max_tokens: int = 8000) -> dict:
"usage": data.get("usage", {}), "usage": data.get("usage", {}),
} }
except httpx.TimeoutException: except subprocess.TimeoutExpired:
return {"error": "LLM timeout (120s)"} return {"error": "LLM timeout (130s)"}
except httpx.HTTPStatusError as e: except json.JSONDecodeError as e:
return {"error": f"LLM HTTP {e.response.status_code}: {e.response.text[:500]}"} return {"error": f"LLM bad JSON: {e}"}
except httpx.RequestError as e:
return {"error": f"LLM request failed: {e}"}
except Exception as e: except Exception as e:
return {"error": f"LLM unexpected: {e}"} return {"error": f"LLM unexpected: {e}"}