LLM 호출 규격 전환: 고객사 사내 LLM(FabriX/Gauss) 헤더 지원 (LLM_PROVIDER=fabrix)

- settings: LLM_PROVIDER/LLM_CHAT_PATH/LLM_BODY_MODEL/LLM_JSON_MODE/LLM_MAX_TOKENS, FABRIX_* 3종, llm_enabled()
- llm_client: headers()/body() 를 규격별로 구성. fabrix 는 x-openapi-token(Bearer)/x-generative-ai-client/
  x-llm-model-id/x-generative-ai-user-email, body model 은 LLM_BODY_MODEL
- summarize/ping: 접속 점검 명령 (--show 로 요청만 확인)
- docs/llm-provider-plan.md: 계획·.env 값·오류별 조치. 기본값은 openai 라 기존 동작 불변

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
byeongwook.choi
2026-09-21 15:18:55 +09:00
co-authored by Claude Fable 5.1
parent 89deb108b0
commit 0f70c0d245
7 changed files with 319 additions and 11 deletions
+85
View File
@@ -0,0 +1,85 @@
"""LLM 접속 점검 — Stage 3 를 돌리기 전에 규격(주소·헤더·body)이 맞는지 한 번 호출해 본다.
python -m summarize.ping # .env 대로 한 번 호출
python -m summarize.ping --show # 보낼 주소·헤더(비밀값은 가림)·body 만 출력, 호출 안 함
성공하면 모델 답(JSON)과 usage 를, 실패하면 HTTP 상태와 서버 응답 본문을 그대로 찍는다.
400 이면 대개 body 규격(response_format / model) 문제 → LLM_JSON_MODE=0, LLM_BODY_MODEL 조정.
401/403 이면 헤더(토큰·클라이언트키·이메일) 문제. 404 면 주소(LLM_BASE_URL + LLM_CHAT_PATH).
"""
from __future__ import annotations
import argparse
import json
import sys
import time
import urllib.error
from config.settings import settings
from summarize.llm_client import OpenAICompatClient
SYSTEM = "너는 JSON 만 출력하는 도우미다."
USER = '다음 스키마로만 답해라: {"ok": true, "model_says": "<짧은 인사 한 줄>"}'
_SECRET_HEADERS = {"authorization", "x-openapi-token", "x-generative-ai-client"}
def _mask(v: str) -> str:
if len(v) <= 12:
return "*" * len(v)
return v[:8] + "" + v[-4:]
def main() -> None:
ap = argparse.ArgumentParser(description="LLM 접속 점검")
ap.add_argument("--show", action="store_true", help="요청만 출력하고 호출하지 않음")
args = ap.parse_args()
try:
client = OpenAICompatClient()
except RuntimeError as e:
print(f"설정 오류: {e}", file=sys.stderr)
sys.exit(2)
headers = {k: (_mask(v) if k.lower() in _SECRET_HEADERS else v) for k, v in client.headers().items()}
print(f"규격 : {client.provider}")
print(f"주소 : {client.url}")
print(f"헤더 : {json.dumps(headers, ensure_ascii=False)}")
body = client.body(SYSTEM, USER)
print(f"body : {json.dumps({k: v for k, v in body.items() if k != 'messages'}, ensure_ascii=False)}")
if args.show:
return
t0 = time.time()
try:
raw = client._post_once(SYSTEM, USER)
except urllib.error.HTTPError as e:
text = e.read().decode("utf-8", errors="replace")
print(f"\n실패 HTTP {e.code} ({int((time.time() - t0) * 1000)} ms)")
print(f"서버 응답: {text[:2000]}")
sys.exit(1)
except Exception as e: # noqa: BLE001
print(f"\n실패 {type(e).__name__}: {e}")
sys.exit(1)
ms = int((time.time() - t0) * 1000)
print(f"\n성공 ({ms} ms)")
try:
content = raw["choices"][0]["message"]["content"]
except (KeyError, IndexError, TypeError):
print("응답 형태가 OpenAI 규격(choices[0].message.content)이 아닙니다:")
print(json.dumps(raw, ensure_ascii=False)[:2000])
sys.exit(1)
print(f"모델 답 : {content[:500]}")
print(f"usage : {json.dumps(raw.get('usage') or {}, ensure_ascii=False)}")
from summarize.llm_client import _extract_json
try:
_extract_json(content)
print("JSON 파싱: OK")
except Exception as e: # noqa: BLE001
print(f"JSON 파싱: 실패 ({e}) — LLM_JSON_MODE 와 프롬프트 확인")
sys.exit(1)
if __name__ == "__main__":
main()