fix(llm): curl --http2 вместо httpx
httpx требует h2, который не ставится в облаке. curl есть везде и гарантированно держит HTTP/2. subprocess.run(['curl','-s','--http2',...])
This commit is contained in:
@@ -5,5 +5,3 @@ requests
|
|||||||
psycopg2-binary
|
psycopg2-binary
|
||||||
python-dotenv
|
python-dotenv
|
||||||
pdfplumber
|
pdfplumber
|
||||||
httpx[http2]
|
|
||||||
h2
|
|
||||||
|
|||||||
+32
-26
@@ -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}"}
|
||||||
|
|||||||
Reference in New Issue
Block a user