From 099edbcb613939f575d6170f3d7254f3efca2b76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sun, 14 Jun 2026 07:40:12 +0400 Subject: [PATCH] =?UTF-8?q?fix(llm):=20curl=20--http2=20=D0=B2=D0=BC=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=BE=20httpx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit httpx требует h2, который не ставится в облаке. curl есть везде и гарантированно держит HTTP/2. subprocess.run(['curl','-s','--http2',...]) --- requirements.txt | 2 -- site/llm_client.py | 58 +++++++++++++++++++++++++--------------------- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/requirements.txt b/requirements.txt index 86af1dc..dbe9f25 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,5 +5,3 @@ requests psycopg2-binary python-dotenv pdfplumber -httpx[http2] -h2 diff --git a/site/llm_client.py b/site/llm_client.py index 4f9aed5..68f9863 100644 --- a/site/llm_client.py +++ b/site/llm_client.py @@ -4,7 +4,8 @@ llm_client.py — Тонкий HTTP-клиент к LLM API. Только отправить промпт → получить ответ. Никакой бизнес-логики. Бизнес-логика (промпты, парсинг ответа) → 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) @@ -12,7 +13,8 @@ llm_client.py — Тонкий HTTP-клиент к LLM API. """ 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: """ - Отправить промпт к LLM API. + Отправить промпт к LLM API через curl (HTTP/2). Args: prompt: текст промпта @@ -42,28 +44,34 @@ def ask(prompt: str, model: str = None, max_tokens: int = 8000) -> dict: model = model or DEFAULT_MODEL - payload = { + payload = json.dumps({ "model": model, - "messages": [ - {"role": "user", "content": prompt}, - ], + "messages": [{"role": "user", "content": prompt}], "max_tokens": max_tokens, - "temperature": 0.1, # низкая температура = стабильный формат ответа - } - - headers = { - "Authorization": f"Bearer {api_key}", - "Content-Type": "application/json", - } + "temperature": 0.1, + }) 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() + # 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, + ) - # 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] message = choice.get("message", {}) text = message.get("content", "") @@ -74,11 +82,9 @@ def ask(prompt: str, model: str = None, max_tokens: int = 8000) -> dict: "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 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}"}