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:
co-authored by
Claude Fable 5.1
parent
89deb108b0
commit
0f70c0d245
@@ -15,6 +15,19 @@ LLM_BACKOFF_BASE=2.0
|
||||
LLM_BACKOFF_MAX=60.0
|
||||
LLM_TIMEOUT_S=180
|
||||
|
||||
# --- 호출 규격 (docs/llm-provider-plan.md). 기본은 openai(Authorization: Bearer LLM_API_KEY)
|
||||
# 고객사 사내 LLM(FabriX/Gauss)은 아래처럼. 확인: python -m summarize.ping
|
||||
#LLM_PROVIDER=fabrix
|
||||
#LLM_BASE_URL=https://<고객사 LLM 호스트>/<경로> # 완성 주소면 LLM_CHAT_PATH= (빈값), 아니면 /chat/completions 가 붙는다
|
||||
#LLM_CHAT_PATH=/chat/completions
|
||||
#LLM_MODEL=339 # x-llm-model-id (모델 번호)
|
||||
#LLM_BODY_MODEL=/mnt/models # body 의 model (FabriX 는 고정값)
|
||||
#LLM_JSON_MODE=1 # response_format 을 서버가 거부(400)하면 0
|
||||
#LLM_MAX_TOKENS=0 # 0 이면 안 보냄
|
||||
#FABRIX_CLIENT_KEY= # x-generative-ai-client
|
||||
#FABRIX_OPENAPI_TOKEN= # x-openapi-token (Bearer 는 코드가 붙임)
|
||||
#FABRIX_USER_EMAIL= # x-generative-ai-user-email (신원 정보)
|
||||
|
||||
# /ingest 가 뒤이어 돌리는 요약의 백엔드: api | file | off
|
||||
SUMMARIZE_BACKEND=api
|
||||
|
||||
|
||||
@@ -16,6 +16,22 @@ class Settings(BaseSettings):
|
||||
llm_base_url: str = ""
|
||||
llm_api_key: str = ""
|
||||
llm_model: str = "glm-5.2"
|
||||
# 호출 규격 (docs/llm-provider-plan.md)
|
||||
# openai — Authorization: Bearer <LLM_API_KEY> (OpenRouter 등)
|
||||
# fabrix — 고객사 사내 LLM(FabriX/Gauss). 키 대신 커스텀 헤더 4종 (아래 fabrix_*)
|
||||
llm_provider: str = "openai"
|
||||
# base_url 뒤에 붙일 경로. 고객사 주소가 이미 완성형이면 빈 문자열로 둔다
|
||||
llm_chat_path: str = "/chat/completions"
|
||||
# 요청 body 의 model 값. 비우면 LLM_MODEL. (FabriX 는 body 는 "/mnt/models" 고정, 모델은 헤더가 고름)
|
||||
llm_body_model: str = ""
|
||||
# response_format={"type":"json_object"} 전송 여부. 서버가 400 을 주면 끈다 (파서가 본문에서 JSON 을 뽑는다)
|
||||
llm_json_mode: bool = True
|
||||
# 출력 토큰 상한. 0 이면 보내지 않음 (OpenRouter 는 잔액 견적에 쓰고, 추론 폭주 방어도 됨)
|
||||
llm_max_tokens: int = 0
|
||||
# FabriX 헤더 값 — x-generative-ai-client / x-openapi-token(Bearer 접두는 코드가 붙임) / x-generative-ai-user-email
|
||||
fabrix_client_key: str = ""
|
||||
fabrix_openapi_token: str = ""
|
||||
fabrix_user_email: str = ""
|
||||
# 요약 배치의 동시 LLM 호출 수. DB 쓰기는 메인 스레드에 남기고 호출만 병렬로 돈다.
|
||||
llm_concurrency: int = 4
|
||||
llm_timeout_s: int = 180
|
||||
@@ -53,5 +69,13 @@ class Settings(BaseSettings):
|
||||
wiki_search_backend: str = "pg"
|
||||
qmd_url: str = "http://127.0.0.1:8181"
|
||||
|
||||
def llm_enabled(self) -> bool:
|
||||
"""api 백엔드로 호출할 수 있는 상태인가 — 규격별 필수값이 다 있는지."""
|
||||
if not self.llm_base_url:
|
||||
return False
|
||||
if self.llm_provider == "fabrix":
|
||||
return bool(self.fabrix_client_key and self.fabrix_openapi_token)
|
||||
return bool(self.llm_api_key)
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# 고객사 사내 LLM(FabriX/Gauss) 호출 규격 맞추기 — 계획과 적용 (2026-09-21)
|
||||
|
||||
고객사 PC 에서는 외부 LLM(OpenRouter) 을 못 쓴다(보안). Stage 3(로직 조각 추출)을 고객사 전용 LLM 으로
|
||||
돌리려면 호출 규격을 바꿔야 한다. 코드는 이미 바꿨고, **고객사 PC 에서는 `.env` 만 채우면 된다.**
|
||||
|
||||
## 1. 무엇이 다른가
|
||||
|
||||
| | 지금까지 (OpenRouter) | 고객사 LLM (serving_llm.py 예시 + 팀원 실측) |
|
||||
|---|---|---|
|
||||
| 인증 | `Authorization: Bearer <키>` 헤더 1개 | 커스텀 헤더 4개: `x-openapi-token`(Bearer 접두 필수) · `x-generative-ai-client` · `x-llm-model-id` · `x-generative-ai-user-email`(신원) |
|
||||
| 주소 | `https://openrouter.ai/api/v1` + `/chat/completions` | 예시는 `https://genai-openapi.sec.samsung.net/dxhq/prod/api-llm` (완성 주소인지, 뒤에 `/chat/completions` 가 붙는지 **미확인**) |
|
||||
| 모델 지정 | body `model: "z-ai/glm-5.2"` | 헤더 `x-llm-model-id: 581`(예시) 또는 `339`(팀원). body 의 `model` 은 팀원 실측상 `/mnt/models` 고정 |
|
||||
| 응답 형식 강제 | `response_format: json_object` | 받는지 **미확인** — 400 이면 빼야 함 |
|
||||
| 응답 모양 | OpenAI 규격 `choices[0].message.content` | 같다고 가정 (팀원 가이드도 같은 전제). 다르면 파서 추가 |
|
||||
|
||||
바뀌는 건 **헤더와 body 의 model 값**뿐이고 프롬프트·파이프라인은 그대로다.
|
||||
|
||||
## 2. 적용한 코드 변경
|
||||
|
||||
| 파일 | 내용 |
|
||||
|---|---|
|
||||
| `config/settings.py` | `LLM_PROVIDER`(openai/fabrix), `LLM_CHAT_PATH`, `LLM_BODY_MODEL`, `LLM_JSON_MODE`, `LLM_MAX_TOKENS`, `FABRIX_CLIENT_KEY/OPENAPI_TOKEN/USER_EMAIL`. `llm_enabled()` 가 규격별 필수값을 본다 |
|
||||
| `summarize/llm_client.py` | `OpenAICompatClient` 가 `headers()`/`body()` 를 규격에 따라 만든다. fabrix 면 Bearer 접두를 코드가 붙인다 |
|
||||
| `summarize/ping.py` | **접속 점검 명령.** `python -m summarize.ping` 한 번으로 주소·헤더·body 와 서버 응답을 확인 |
|
||||
| `query/api.py` | "LLM 켜짐" 판정을 `settings.llm_enabled()` 로 |
|
||||
| `.env.example` | fabrix 블록(주석) |
|
||||
|
||||
기본값은 전부 openai 라 기존 OpenRouter 동작은 그대로다.
|
||||
|
||||
## 3. 고객사 PC 에서 할 일 (순서대로)
|
||||
|
||||
### 3.1 `.env` 에 추가
|
||||
|
||||
```ini
|
||||
LLM_PROVIDER=fabrix
|
||||
LLM_BASE_URL=https://genai-openapi.sec.samsung.net/dxhq/prod/api-llm # 고객사 파일(serving_llm.py)의 ENDPOINT_URL 그대로
|
||||
LLM_CHAT_PATH= # 위 주소가 완성 주소면 빈값. 404 나면 /chat/completions 로
|
||||
LLM_MODEL=581 # 고객사 파일의 YOUR_MODEL. 팀원은 339(GaussO Flash) 를 씀 — 텍스트 모델이면 됨
|
||||
LLM_BODY_MODEL=/mnt/models # 400 나면 LLM_MODEL 과 같은 값으로 바꿔 본다
|
||||
LLM_JSON_MODE=1 # 400 나면 0
|
||||
FABRIX_CLIENT_KEY=<YOUR_CLIENT_KEY 값>
|
||||
FABRIX_OPENAPI_TOKEN=<YOUR_PASS_KEY 값. "Bearer " 는 있어도 없어도 됨>
|
||||
FABRIX_USER_EMAIL=<YOUR_EMAIL 값>
|
||||
LLM_CONCURRENCY=2
|
||||
LLM_API_KEY= # 안 씀. 비워도 됨
|
||||
```
|
||||
|
||||
### 3.2 점검 (Stage 3 돌리기 전에 반드시)
|
||||
|
||||
```powershell
|
||||
.venv\Scripts\python -m summarize.ping --show # 보낼 내용만 출력 (비밀값 가림)
|
||||
.venv\Scripts\python -m summarize.ping # 실제 한 번 호출
|
||||
```
|
||||
|
||||
| 결과 | 뜻 | 조치 |
|
||||
|---|---|---|
|
||||
| `성공` + `JSON 파싱: OK` | 규격 맞음 | 3.3 으로 |
|
||||
| HTTP 401 / 403 | 헤더 문제 | 토큰·클라이언트키·이메일 값 확인. 토큰이 `Bearer ` 로 시작하는지 서버 응답 본문을 본다 |
|
||||
| HTTP 404 | 주소 문제 | `LLM_CHAT_PATH=/chat/completions` 로 바꾸거나, 고객사 파일의 실제 `requests.post(...)` URL 을 확인 |
|
||||
| HTTP 400 | body 문제 | 먼저 `LLM_JSON_MODE=0`, 그래도면 `LLM_BODY_MODEL` 을 비우거나 `LLM_MODEL` 값으로 |
|
||||
| `응답 형태가 OpenAI 규격이 아닙니다` | 응답 모양이 다름 | 출력된 JSON 을 가져오면 파서를 맞춘다 (코드 수정 필요) |
|
||||
| `JSON 파싱: 실패` | 모델이 JSON 을 안 지킴 | `LLM_JSON_MODE=1` 로 켜 보고, 안 되면 모델 번호를 바꾼다 |
|
||||
|
||||
### 3.3 Stage 3 실행
|
||||
|
||||
```powershell
|
||||
.venv\Scripts\python -m summarize.runner --llm api --program <프로그램> --limit 3 # 작게
|
||||
.venv\Scripts\python -m summarize.jobs stats # failed 0 인지
|
||||
.venv\Scripts\python -m summarize.runner --llm api --program <프로그램> # 한 본
|
||||
.venv\Scripts\python -m summarize.runner --llm api # 전체
|
||||
.venv\Scripts\python -m wiki_out.run --all
|
||||
```
|
||||
|
||||
## 4. 확인이 필요한 것 (고객사 파일 원본을 보면 바로 답이 나온다)
|
||||
|
||||
1. `requests.post(...)` 에 넣는 **URL 이 ENDPOINT_URL 그대로인지, 뒤에 경로가 붙는지** → `LLM_CHAT_PATH`
|
||||
2. **body 의 `model` 값** — 예시 파일이 `"model": ...` 을 뭐로 보내는지 → `LLM_BODY_MODEL`
|
||||
3. **응답 JSON 모양** — `choices[0].message.content` 인지
|
||||
4. **동시 호출·일일 토큰 한도** — 55본에 조각 3,212개(LLM 호출 약 5,600회)였다. 1만 본이면 호출 수십만 회라 한도를 미리 물어야 한다
|
||||
5. `tools`(function calling) 는 예시에 있지만 **우리는 안 쓴다** — 무시해도 된다
|
||||
|
||||
## 5. 대안 (직결이 안 될 때)
|
||||
|
||||
팀원 가이드의 -12 컨테이너 게이트웨이(`/api/ito`) 경유: `LLM_PROVIDER=openai`, `LLM_BASE_URL=http://10.196.81.34:8914/api/ito`,
|
||||
`LLM_API_KEY=<AAF_GATEWAY_KEY>`, `LLM_MODEL=339`. 게이트웨이가 헤더를 대신 얹는다. 백엔드가 떠 있어야 하고 부하가 거기 걸린다.
|
||||
+3
-3
@@ -142,7 +142,7 @@ def ingest(payload: dict = Body(), background_tasks: BackgroundTasks = None) ->
|
||||
# SUMMARIZE_BACKEND=file 이면 키 없이 프롬프트만 큐에 쌓는다 (/summaries/jobs 로 확인).
|
||||
backend = settings.summarize_backend
|
||||
summarize = "disabled"
|
||||
if backend != "off" and (backend == "file" or (settings.llm_base_url and settings.llm_api_key)):
|
||||
if backend != "off" and (backend == "file" or settings.llm_enabled()):
|
||||
if status == "loaded" or pending:
|
||||
with _summarizing_lock:
|
||||
already = program in _summarizing
|
||||
@@ -328,7 +328,7 @@ def summaries_status() -> dict:
|
||||
running = sorted(_summarizing)
|
||||
return {
|
||||
"model": settings.llm_model,
|
||||
"llm_enabled": bool(settings.llm_base_url and settings.llm_api_key),
|
||||
"llm_enabled": settings.llm_enabled(),
|
||||
"running": running,
|
||||
"programs": per_program,
|
||||
"failures": failures,
|
||||
@@ -392,7 +392,7 @@ def _persist_env(key: str, value: str) -> None:
|
||||
def get_llm_settings() -> dict:
|
||||
return {
|
||||
"model": settings.llm_model,
|
||||
"enabled": bool(settings.llm_base_url and settings.llm_api_key),
|
||||
"enabled": settings.llm_enabled(),
|
||||
}
|
||||
|
||||
|
||||
|
||||
+47
-8
@@ -55,10 +55,27 @@ def _extract_json(content: str) -> dict:
|
||||
|
||||
|
||||
class OpenAICompatClient(LLMClient):
|
||||
"""OpenAI 호환 `/chat/completions` 호출. 규격(LLM_PROVIDER)에 따라 헤더만 다르다.
|
||||
|
||||
- openai : Authorization: Bearer <LLM_API_KEY>
|
||||
- fabrix : 고객사 사내 LLM. x-openapi-token(Bearer ...) / x-generative-ai-client /
|
||||
x-llm-model-id / x-generative-ai-user-email. body 의 model 은 LLM_BODY_MODEL.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
if not settings.llm_base_url or not settings.llm_api_key:
|
||||
raise RuntimeError("LLM_BASE_URL / LLM_API_KEY 환경변수가 필요합니다")
|
||||
if not settings.llm_base_url:
|
||||
raise RuntimeError("LLM_BASE_URL 환경변수가 필요합니다")
|
||||
self.provider = settings.llm_provider
|
||||
if self.provider == "fabrix":
|
||||
if not (settings.fabrix_client_key and settings.fabrix_openapi_token):
|
||||
raise RuntimeError("LLM_PROVIDER=fabrix 는 FABRIX_CLIENT_KEY / FABRIX_OPENAPI_TOKEN 이 필요합니다")
|
||||
elif self.provider == "openai":
|
||||
if not settings.llm_api_key:
|
||||
raise RuntimeError("LLM_BASE_URL / LLM_API_KEY 환경변수가 필요합니다")
|
||||
else:
|
||||
raise RuntimeError(f"알 수 없는 LLM_PROVIDER: {self.provider} (openai | fabrix)")
|
||||
self.base = settings.llm_base_url.rstrip("/")
|
||||
self.url = self.base + settings.llm_chat_path
|
||||
self.key = settings.llm_api_key
|
||||
self.model = settings.llm_model
|
||||
# 호출 누적 사용량 — runner가 stats에 실어 보고한다
|
||||
@@ -66,22 +83,44 @@ class OpenAICompatClient(LLMClient):
|
||||
"retries": 0}
|
||||
self._lock = threading.Lock() # 병렬 호출에서 usage 집계가 어긋나지 않게
|
||||
|
||||
def _post_once(self, system: str, user: str) -> dict:
|
||||
def headers(self) -> dict:
|
||||
h = {"Content-Type": "application/json"}
|
||||
if self.provider == "fabrix":
|
||||
token = settings.fabrix_openapi_token.strip()
|
||||
if not token.lower().startswith("bearer "):
|
||||
token = "Bearer " + token # 날것으로 보내면 401 (팀원 실측)
|
||||
h.update({
|
||||
"x-openapi-token": token,
|
||||
"x-generative-ai-client": settings.fabrix_client_key,
|
||||
"x-llm-model-id": self.model,
|
||||
"x-generative-ai-user-email": settings.fabrix_user_email,
|
||||
})
|
||||
else:
|
||||
h["Authorization"] = f"Bearer {self.key}"
|
||||
return h
|
||||
|
||||
def body(self, system: str, user: str) -> dict:
|
||||
body = {
|
||||
"model": self.model,
|
||||
"model": settings.llm_body_model or self.model,
|
||||
"temperature": 0.1,
|
||||
"messages": [
|
||||
{"role": "system", "content": system},
|
||||
{"role": "user", "content": user},
|
||||
],
|
||||
"response_format": {"type": "json_object"},
|
||||
}
|
||||
if settings.llm_json_mode:
|
||||
body["response_format"] = {"type": "json_object"}
|
||||
if settings.llm_max_tokens > 0:
|
||||
body["max_tokens"] = settings.llm_max_tokens
|
||||
if "openrouter" in self.base:
|
||||
body["usage"] = {"include": True} # OpenRouter 확장 — usage.cost(USD 크레딧) 포함
|
||||
return body
|
||||
|
||||
def _post_once(self, system: str, user: str) -> dict:
|
||||
req = urllib.request.Request(
|
||||
f"{self.base}/chat/completions",
|
||||
data=json.dumps(body).encode("utf-8"),
|
||||
headers={"Authorization": f"Bearer {self.key}", "Content-Type": "application/json"},
|
||||
self.url,
|
||||
data=json.dumps(self.body(system, user), ensure_ascii=False).encode("utf-8"),
|
||||
headers=self.headers(),
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=settings.llm_timeout_s) as res:
|
||||
|
||||
@@ -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()
|
||||
@@ -201,3 +201,65 @@ def test_ingest_backend_file_queues_without_key(api_client, monkeypatch):
|
||||
q = c.get("/summaries/jobs").json()
|
||||
assert q["pending"] >= 1, q
|
||||
assert any(n["task"] == "program_summary" for n in q["next"]), q
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ 호출 규격 (LLM_PROVIDER)
|
||||
|
||||
def _configure(monkeypatch, **kw):
|
||||
from config.settings import settings
|
||||
base = {"llm_base_url": "http://x", "llm_api_key": "", "llm_provider": "openai",
|
||||
"llm_chat_path": "/chat/completions", "llm_body_model": "", "llm_json_mode": True,
|
||||
"llm_max_tokens": 0, "llm_model": "m", "fabrix_client_key": "", "fabrix_openapi_token": "",
|
||||
"fabrix_user_email": ""}
|
||||
base.update(kw)
|
||||
for k, v in base.items():
|
||||
monkeypatch.setattr(settings, k, v)
|
||||
|
||||
|
||||
def test_openai_provider_headers_and_body(monkeypatch):
|
||||
from summarize import llm_client
|
||||
_configure(monkeypatch, llm_api_key="k", llm_max_tokens=800)
|
||||
c = llm_client.OpenAICompatClient()
|
||||
assert c.url == "http://x/chat/completions"
|
||||
assert c.headers() == {"Content-Type": "application/json", "Authorization": "Bearer k"}
|
||||
b = c.body("s", "u")
|
||||
assert b["model"] == "m" and b["response_format"] == {"type": "json_object"} and b["max_tokens"] == 800
|
||||
|
||||
|
||||
def test_fabrix_provider_headers_and_body(monkeypatch):
|
||||
from summarize import llm_client
|
||||
_configure(monkeypatch, llm_provider="fabrix", llm_base_url="https://h/dxhq/prod/api-llm",
|
||||
llm_chat_path="", llm_model="581", llm_body_model="/mnt/models", llm_json_mode=False,
|
||||
fabrix_client_key="CK", fabrix_openapi_token="TOK", fabrix_user_email="me@x.com")
|
||||
c = llm_client.OpenAICompatClient()
|
||||
assert c.url == "https://h/dxhq/prod/api-llm" # 완성 주소면 경로를 안 붙인다
|
||||
h = c.headers()
|
||||
assert h["x-openapi-token"] == "Bearer TOK" # Bearer 접두를 코드가 붙인다
|
||||
assert h["x-generative-ai-client"] == "CK"
|
||||
assert h["x-llm-model-id"] == "581"
|
||||
assert h["x-generative-ai-user-email"] == "me@x.com"
|
||||
assert "Authorization" not in h
|
||||
b = c.body("s", "u")
|
||||
assert b["model"] == "/mnt/models" and "response_format" not in b and "max_tokens" not in b
|
||||
|
||||
|
||||
def test_fabrix_keeps_existing_bearer_prefix(monkeypatch):
|
||||
from summarize import llm_client
|
||||
_configure(monkeypatch, llm_provider="fabrix", fabrix_client_key="CK", fabrix_openapi_token="Bearer TOK")
|
||||
assert llm_client.OpenAICompatClient().headers()["x-openapi-token"] == "Bearer TOK"
|
||||
|
||||
|
||||
def test_provider_requires_its_credentials(monkeypatch):
|
||||
from config.settings import settings
|
||||
from summarize import llm_client
|
||||
_configure(monkeypatch, llm_provider="fabrix") # 키 없음
|
||||
with pytest.raises(RuntimeError):
|
||||
llm_client.OpenAICompatClient()
|
||||
assert settings.llm_enabled() is False
|
||||
_configure(monkeypatch, llm_provider="fabrix", fabrix_client_key="a", fabrix_openapi_token="b")
|
||||
assert settings.llm_enabled() is True
|
||||
_configure(monkeypatch, llm_provider="openai") # api key 없음
|
||||
assert settings.llm_enabled() is False
|
||||
_configure(monkeypatch, llm_provider="bogus", llm_api_key="k")
|
||||
with pytest.raises(RuntimeError):
|
||||
llm_client.OpenAICompatClient()
|
||||
|
||||
Reference in New Issue
Block a user