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
+62
View File
@@ -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()