[fix] solution/backend: 키 미설정 문구가 엉뚱한 공급자를 가리키던 것

공급자를 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) <noreply@anthropic.com>
This commit is contained in:
hbyang 2026-09-21 17:19:29 +09:00
parent b1a34ba58d
commit 4dd0c604ee
5 changed files with 18 additions and 5 deletions

View File

@ -85,7 +85,7 @@ class ErrorType(Enum):
COLLECT_ALREADY_RUNNING = auto() COLLECT_ALREADY_RUNNING = auto()
# 생성(generator) 관련 에러 # 생성(generator) 관련 에러
GENERATOR_NOT_CONFIGURED = 1500 # GEMINI_API_KEY 미설정 GENERATOR_NOT_CONFIGURED = 1500 # 활성 LLM 공급자의 키 미설정(llm/provider.missing_key)
GENERATOR_CALL_FAILED = auto() GENERATOR_CALL_FAILED = auto()
GENERATOR_INVALID_OUTPUT = auto() # 구조화 출력 파싱 실패 GENERATOR_INVALID_OUTPUT = auto() # 구조화 출력 파싱 실패
GENERATOR_LOW_CONFIDENCE = auto() # 신뢰도 낮음 — 자동 반영 금지, 사람 확인 큐로 GENERATOR_LOW_CONFIDENCE = auto() # 신뢰도 낮음 — 자동 반영 금지, 사람 확인 큐로

View File

@ -18,6 +18,7 @@ from common.category_schema import get_schema
from services.collector import AdapterDisabled, AdapterNotFound, REGISTRY from services.collector import AdapterDisabled, AdapterNotFound, REGISTRY
from services.collector import yanolja_adapter from services.collector import yanolja_adapter
from services.external import naver_place_lookup, perplexity, tour_lookup from services.external import naver_place_lookup, perplexity, tour_lookup
from services.llm import provider
from services.fact_service import FactService from services.fact_service import FactService
from router.v1.fact.protocol import Req_UpsertFact from router.v1.fact.protocol import Req_UpsertFact
from common.job_errors import PermanentJobError 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 from services.job_service import enqueue_job
if not gemini.is_configured(): if not gemini.is_configured():
LOG.i("[collect] GEMINI_API_KEY 미설정 — 사진 분석 건너뜀(사진은 확인 큐에 남는다)") LOG.i(f"[collect] {provider.missing_key()} 미설정 — 사진 분석 건너뜀(사진은 확인 큐에 남는다)")
return None return None
job_id, _created = await enqueue_job( job_id, _created = await enqueue_job(
JobQueue(), JobType.VISION, JobQueue(), JobType.VISION,

View File

@ -2,6 +2,7 @@
from common.logger import LOG from common.logger import LOG
from services.copy_steps import CopyAborted, prepare_copy, generate_copy, save_copy, fill_faqs from services.copy_steps import CopyAborted, prepare_copy, generate_copy, save_copy, fill_faqs
from services.external import gemini_text from services.external import gemini_text
from services.llm import provider
from services.job_progress import JobProgress from services.job_progress import JobProgress
COPY_STEPS = ("prepare", "generate", "save", "faq_fill") COPY_STEPS = ("prepare", "generate", "save", "faq_fill")
@ -16,7 +17,7 @@ async def run_copy(job: dict) -> dict:
copy = None copy = None
note = None note = None
if inputs.ungrounded or not gemini_text.is_configured(): 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") await progress.skip("generate", "no_facts" if inputs.ungrounded else "not_configured")
if inputs.catalog is None and not inputs.ungrounded: if inputs.catalog is None and not inputs.ungrounded:
raise CopyAborted(note) raise CopyAborted(note)

View File

@ -8,3 +8,13 @@ from services.llm import gemini, openai
def active(): def active():
return openai if external_api_config.llm_provider == "openai" else gemini 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"

View File

@ -53,6 +53,7 @@ from crud.place_crud import PlaceCRUD
from crud.song_crud import SongCRUD from crud.song_crud import SongCRUD
from services import place_research, site_payload from services import place_research, site_payload
from services.external import gemini_text, suno from services.external import gemini_text, suno
from services.llm import provider
from services.llm.gemini import GeminiError, GeminiInvalidOutput, GeminiNotConfigured from services.llm.gemini import GeminiError, GeminiInvalidOutput, GeminiNotConfigured
from common.job_errors import PermanentJobError 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, region=region, grounding=lines, intro=intro, client=client,
) )
except GeminiNotConfigured: except GeminiNotConfigured:
LOG.i(f"[song] place={place_id} 건너뜀 — GEMINI_API_KEY 미설정") LOG.i(f"[song] place={place_id} 건너뜀 — {provider.missing_key()} 미설정")
return {"place_id": place_id, "skipped": "GEMINI_API_KEY 미설정"} return {"place_id": place_id, "skipped": f"{provider.missing_key()} 미설정"}
except GeminiInvalidOutput as ex: except GeminiInvalidOutput as ex:
LOG.i(f"[song] place={place_id} 건너뜀 — {ex}") LOG.i(f"[song] place={place_id} 건너뜀 — {ex}")
return {"place_id": place_id, "skipped": str(ex)} return {"place_id": place_id, "skipped": str(ex)}