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(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`. 게이트웨이가 헤더를 대신 얹는다. 백엔드가 떠 있어야 하고 부하가 거기 걸린다.