From 4dd0c604eef4827d334e514acff7dc2c35399905 Mon Sep 17 00:00:00 2001 From: hbyang Date: Mon, 21 Sep 2026 17:19:29 +0900 Subject: [PATCH] =?UTF-8?q?[fix]=20solution/backend:=20=ED=82=A4=20?= =?UTF-8?q?=EB=AF=B8=EC=84=A4=EC=A0=95=20=EB=AC=B8=EA=B5=AC=EA=B0=80=20?= =?UTF-8?q?=EC=97=89=EB=9A=B1=ED=95=9C=20=EA=B3=B5=EA=B8=89=EC=9E=90?= =?UTF-8?q?=EB=A5=BC=20=EA=B0=80=EB=A6=AC=ED=82=A4=EB=8D=98=20=EA=B2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 공급자를 openai 로 바꾼 뒤에도 호출측에 "GEMINI_API_KEY 미설정" 이 문자열로 박혀 있어서, **없는 것은 OPENAI_API_KEY 인데 화면은 Gemini 를 탓했다** (실측 2026-09-21: 로컬에서 소개문이 안 나와 Gemini 키를 한참 들여다봤다). 원인을 정확히 반대로 가리키는 종류다. - llm/provider: missing_key() 추가 — 활성 공급자에게 필요한 env 이름을 돌려준다 - copy_service·collect_service·song_service: 하드코딩 문구를 그 함수로 교체 - enums: GENERATOR_NOT_CONFIGURED 주석도 공급자 중립으로 관련 테스트 55 passed Co-Authored-By: Claude Opus 5 (1M context) --- solution/backend/common/enums.py | 2 +- solution/backend/services/collect_service.py | 3 ++- solution/backend/services/copy_service.py | 3 ++- solution/backend/services/llm/provider.py | 10 ++++++++++ solution/backend/services/song_service.py | 5 +++-- 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/solution/backend/common/enums.py b/solution/backend/common/enums.py index 2be71f0..af41736 100644 --- a/solution/backend/common/enums.py +++ b/solution/backend/common/enums.py @@ -85,7 +85,7 @@ class ErrorType(Enum): COLLECT_ALREADY_RUNNING = auto() # 생성(generator) 관련 에러 - GENERATOR_NOT_CONFIGURED = 1500 # GEMINI_API_KEY 미설정 + GENERATOR_NOT_CONFIGURED = 1500 # 활성 LLM 공급자의 키 미설정(llm/provider.missing_key) GENERATOR_CALL_FAILED = auto() GENERATOR_INVALID_OUTPUT = auto() # 구조화 출력 파싱 실패 GENERATOR_LOW_CONFIDENCE = auto() # 신뢰도 낮음 — 자동 반영 금지, 사람 확인 큐로 diff --git a/solution/backend/services/collect_service.py b/solution/backend/services/collect_service.py index 71bf285..b250ee4 100644 --- a/solution/backend/services/collect_service.py +++ b/solution/backend/services/collect_service.py @@ -18,6 +18,7 @@ from common.category_schema import get_schema from services.collector import AdapterDisabled, AdapterNotFound, REGISTRY from services.collector import yanolja_adapter from services.external import naver_place_lookup, perplexity, tour_lookup +from services.llm import provider from services.fact_service import FactService from router.v1.fact.protocol import Req_UpsertFact from common.job_errors import PermanentJobError @@ -752,7 +753,7 @@ async def _enqueue_vision(place_id: str, owner_user_id: str) -> str | None: from services.job_service import enqueue_job if not gemini.is_configured(): - LOG.i("[collect] GEMINI_API_KEY 미설정 — 사진 분석 건너뜀(사진은 확인 큐에 남는다)") + LOG.i(f"[collect] {provider.missing_key()} 미설정 — 사진 분석 건너뜀(사진은 확인 큐에 남는다)") return None job_id, _created = await enqueue_job( JobQueue(), JobType.VISION, diff --git a/solution/backend/services/copy_service.py b/solution/backend/services/copy_service.py index 976a1fd..3f8f0ae 100644 --- a/solution/backend/services/copy_service.py +++ b/solution/backend/services/copy_service.py @@ -2,6 +2,7 @@ from common.logger import LOG from services.copy_steps import CopyAborted, prepare_copy, generate_copy, save_copy, fill_faqs from services.external import gemini_text +from services.llm import provider from services.job_progress import JobProgress COPY_STEPS = ("prepare", "generate", "save", "faq_fill") @@ -16,7 +17,7 @@ async def run_copy(job: dict) -> dict: copy = None note = None if inputs.ungrounded or not gemini_text.is_configured(): - note = "근거로 쓸 확인된 fact 가 없다" if inputs.ungrounded else "GEMINI_API_KEY 미설정" + note = "근거로 쓸 확인된 fact 가 없다" if inputs.ungrounded else f"{provider.missing_key()} 미설정" await progress.skip("generate", "no_facts" if inputs.ungrounded else "not_configured") if inputs.catalog is None and not inputs.ungrounded: raise CopyAborted(note) diff --git a/solution/backend/services/llm/provider.py b/solution/backend/services/llm/provider.py index 4dcea41..3e9b23c 100644 --- a/solution/backend/services/llm/provider.py +++ b/solution/backend/services/llm/provider.py @@ -8,3 +8,13 @@ from services.llm import gemini, openai def active(): return openai if external_api_config.llm_provider == "openai" else gemini + + +def missing_key() -> str: + """지금 활성인 공급자에게 필요한 env 이름. 키가 없을 때 **그 공급자를** 가리키려고 쓴다. + + ★ 예전에는 호출측이 "GEMINI_API_KEY 미설정" 을 문자열로 박아 뒀다. 공급자를 openai 로 + 바꾼 뒤에도 그 문구가 그대로 나가서, **없는 것은 OPENAI_API_KEY 인데 화면은 Gemini 를 + 탓했다**(실측 2026-09-21: 로컬에서 소개문이 안 나와 Gemini 키를 한참 들여다봤다). + 원인을 정확히 반대로 가리키는 종류라, 문구를 공급자에서 끌어오게 바꿨다.""" + return "OPENAI_API_KEY" if active() is openai else "GEMINI_API_KEY" diff --git a/solution/backend/services/song_service.py b/solution/backend/services/song_service.py index 52291ad..d84a829 100644 --- a/solution/backend/services/song_service.py +++ b/solution/backend/services/song_service.py @@ -53,6 +53,7 @@ from crud.place_crud import PlaceCRUD from crud.song_crud import SongCRUD from services import place_research, site_payload from services.external import gemini_text, suno +from services.llm import provider from services.llm.gemini import GeminiError, GeminiInvalidOutput, GeminiNotConfigured from common.job_errors import PermanentJobError @@ -187,8 +188,8 @@ async def ensure_song(place_id: str, owner_user_id: str, *, force: bool = False) region=region, grounding=lines, intro=intro, client=client, ) except GeminiNotConfigured: - LOG.i(f"[song] place={place_id} 건너뜀 — GEMINI_API_KEY 미설정") - return {"place_id": place_id, "skipped": "GEMINI_API_KEY 미설정"} + LOG.i(f"[song] place={place_id} 건너뜀 — {provider.missing_key()} 미설정") + return {"place_id": place_id, "skipped": f"{provider.missing_key()} 미설정"} except GeminiInvalidOutput as ex: LOG.i(f"[song] place={place_id} 건너뜀 — {ex}") return {"place_id": place_id, "skipped": str(ex)}