# LiteLLM + Opik 리팩토링 달성 내용

## 목표

기존 개별 LLM 클라이언트(OpenAIClient, GeminiTextClient, BaseLLMClient)를 LiteLLM Router + Opik Cloud로 통합하고,
프롬프트 로딩을 중앙집중화하며, 파이프라인 관찰성(observability)과 보안을 강화한다.

---

## 달성 항목 (#3 ~ #15, #14 제외)

### #3. GPT Vision LiteLLM — Codex FAIL 3건 수정

**파일**: `ref_image_pipeline.py`, `scene_image_pipeline.py`

- `_call_gpt_lvm()`에 `opik_tags: Optional[List[str]]` 파라미터 추가
  - 기존: 모든 호출이 `["ref_validation", schema_name]`으로 하드코딩 → scene 호출도 ref로 오분류
  - 수정: 각 호출자가 명시적 태그 전달 (`scene_validation`, `scene_comparison`, `final_select` 등 7개)
- `timeout=settings.llm_timeout_validation` (120초) 추가
  - 기존: Router 기본값 `llm_timeout_text` (600초) 사용 — Vision 호출에 과도
- 빈 응답 guard: `response.choices[0].message.content`가 None/빈 문자열이면 `RuntimeError` 발생

**검증 기준**: Opik에서 ref_validation과 scene_validation 태그가 분리되어 표시되는가

---

### #4. prompt_loader 파이프라인 연결 (5개 모듈)

**파일**: `entity_extractor_v3.py`, `outlook_extractor.py`, `outlook_merger.py`, `scene_extractor_v2.py`, `ref_image_pipeline.py`

각 모듈의 로컬 `PROMPT_DIR` + 수동 버전 스캔 → `prompt_loader.load_prompt(module, name)` 위임

| 모듈 | module 키 | 비고 |
|------|-----------|------|
| entity_extractor_v3 | `entity_extractor_v2` | v2 프롬프트 디렉토리 재사용 |
| outlook_extractor | `outlook_extractor` | |
| outlook_merger | `outlook_merger` | 확장자 스트리핑 (.md/.json) |
| scene_extractor_v2 | `scene_extractor_v2` | `_load_schema()` FileNotFoundError→None 보존 |
| ref_image_pipeline | `lvm_prompts`, `ref_image_prompts` | 2개 모듈 디렉토리 사용 |

미사용 임포트 정리: `Path`, `socket`, `urllib.error`, `urllib.request`

**검증 기준**: `load_prompt(module, name)` 호출 시 DB 우선 → 파일 fallback 경로가 동작하는가

---

### #5. Config key 마이그레이션 (entity_detail → entity_t2i)

**파일**: `llm_client.py`, `analysis_steps.py`, `entity_extractor_v3.py`, `llm_router.py`

- Step manifest의 `entity_t2i` (order 4)와 코드의 `step="entity_detail"` 불일치 해소
- `call_structured(step="entity_detail")` → `step="entity_t2i"` (2곳)
- Opik 태그도 `entity_detail` → `entity_t2i`
- PIPELINE_STEPS에서 고아 키 `entity_detail` 제거
- 구 파일 `llm_router.py`에서도 `entity_detail` → `entity_t2i` 동기화

**검증 기준**: UI에서 `entity_t2i` 모델 변경 시 실제 T2I 생성에 반영되는가

---

### #6. webbook_generator LLM 등록

**파일**: `webbook_generator.py`, `llm_client.py`, `export_service.py`

- `OpenAIClient.generate_structured()` (Responses API) → `call_structured(step="webbook_gen")` (Chat Completions)
- `PIPELINE_STEPS`에 `"webbook_gen"` 추가 (default: "gpt", category: "auxiliary")
- `call_structured()`에 `max_tokens: Optional[int]` 파라미터 추가 (webbook은 60000 토큰 필요)
- `WebbookGenerator.__init__`: `llm_client: OpenAIClient` → `project_llm_config: Optional[Dict]`
- `export_service.py`: `OpenAIClient()` 인스턴스 생성 제거
- `if True:` dead code 제거
- 프롬프트 로딩: `PROMPTS_DIR / filename` → `load_prompt("prototype_prompts", stem)`

**검증 기준**: 웹북 생성 시 LiteLLM Router 경유 + Opik에 `webbook_gen` 태그 기록

---

### #7. style_rules_generator LLM 등록

**파일**: `style_rules_generator.py`, `llm_client.py`

- `BaseLLMClient.generate_structured()` → `call_structured(step="style_rules")`
- `PIPELINE_STEPS`에 `"style_rules"` 추가 (default: "gemini-pro", category: "analysis")
- 함수 시그니처: `llm_client: BaseLLMClient` 제거 → `project_llm_config: Optional[Dict]`
- 프롬프트 로딩: `PROMPT_DIR / "style_rules_generator.md"` → `load_prompt("scene_generator", "style_rules_generator")`

**검증 기준**: 스타일 규칙 생성이 LiteLLM Router 경유로 동작하는가

---

### #8. Frontend catch {} 에러 로깅 (26개 블록)

**파일**: `PipelineStepsPanel.tsx`, `PromptManager.tsx`, `Entities.tsx`, `ImageReview.tsx`, `ProjectDetail.tsx`, `EpisodeDetail.tsx`

빈 `catch {}` 블록에 `console.error('context:', err)` 추가

| 파일 | 수정 수 | 예시 |
|------|---------|------|
| Entities.tsx | 12 | `console.error('Failed to fetch entities:', err)` |
| PromptManager.tsx | 5 | `console.error('Failed to load prompts:', err)` |
| ImageReview.tsx | 4 | `console.error('Failed to save review:', err)` |
| EpisodeDetail.tsx | 3 | `console.error('Failed to fetch episode:', err)` |
| PipelineStepsPanel.tsx | 1 | `console.error('Failed to fetch steps:', err)` |
| ProjectDetail.tsx | 1 | `console.error('Failed to fetch project summary:', err)` |

예외: 인라인 JSON.parse catch (월드가이드 에디터), SSE 파싱 catch — 이들은 의도적 무시

**검증 기준**: 브라우저 DevTools Console에서 API 실패 시 에러 메시지가 출력되는가

---

### #9. Authorization (require_admin)

**파일**: `prompts.py`

프롬프트 쓰기 API에 `require_admin` 적용:
- `POST /api/v1/prompts/` (create) — `get_current_user` → `require_admin`
- `PUT /api/v1/prompts/{id}` (update) — `get_current_user` → `require_admin`
- `POST /api/v1/prompts/{id}/activate` — `get_current_user` → `require_admin`

읽기 API (`GET /`, `GET /{id}`, `GET /compare`)는 기존 `get_current_user` 유지

**검증 기준**: 비 admin 사용자가 프롬프트 생성/수정/활성화 시 403 에러 반환

---

### #10. Opik parallel span propagation — SKIP

LiteLLM `litellm.callbacks = ["opik"]` 글로벌 설정 + 호출별 `metadata["opik"]["tags"]` 전달로
ThreadPoolExecutor 내부 LLM 호출도 개별 Opik 스팬으로 자동 기록됨. 추가 작업 불필요.

---

### #11. VariationRecommender LiteLLM 전환

**파일**: `variation_recommender.py`, `image_service.py`

- `BaseLLMClient` → `call_structured(step="angle_recommend")`
- 생성자: `llm_client: Any` → `project_llm_config: Optional[Dict]`
- 프롬프트 로딩: `PROMPTS_DIR / filename` → `load_prompt("variation_recommender", stem)`
- 호출자: `VariationRecommender(llm_client=openai_client)` → `VariationRecommender(project_llm_config=...)`

**검증 기준**: 변형 추천 시 PIPELINE_STEPS["angle_recommend"] 모델 사용

---

### #12. T2IPromptComposer LiteLLM 전환

**파일**: `t2i_prompt_composer.py`, `image_service.py`

- `self._llm.generate_structured()` → `call_structured(step="prompt_translation")`
- 생성자: `llm_client: Any` → `project_llm_config: Optional[Dict]`
- 프롬프트 로딩: `PROMPTS_DIR / filename` → `load_prompt("t2i_composer", stem)`
- 호출자: `T2IPromptComposer(llm_client=openai_client, ...)` → `T2IPromptComposer(project_llm_config=..., ...)`

**검증 기준**: T2I 프롬프트 변환 시 PIPELINE_STEPS["prompt_translation"] 모델 사용

---

### #13. outlook_merger LiteLLM 전환

**파일**: `outlook_merger.py`, `llm_client.py`

- `OpenAIClient().generate_structured()` → `call_structured(step="outlook_merge")`
- PIPELINE_STEPS에 `"outlook_merge"` 추가 (default: "gpt", category: "analysis")
- `project_llm_config` 파라미터 추가로 프로젝트별 모델 설정 지원
- 기존 `step="entity_review"` 공유 → 전용 키 `"outlook_merge"` 분리 (Codex 리뷰 피드백)

**검증 기준**: 아웃룩 병합 시 `outlook_merge` 키로 모델 라우팅

---

### #15. ref/scene GPT Vision — #3에서 완료

`_call_gpt_lvm()`이 LiteLLM Router(`_get_router().completion()`)를 경유하도록 이미 전환 완료.
#3에서 Opik 태그/타임아웃/빈 응답 guard까지 보강.

---

## 변경된 PIPELINE_STEPS 전체 (19개)

```
entity_style        → gemini-pro     (analysis)
entity_review       → gpt            (analysis)
entity_detail_batch → gpt            (analysis)
entity_t2i          → gemini-flash   (analysis)  ← #5 entity_detail에서 변경
scene_segmentation  → gemini-lite    (analysis)
scene_split         → gemini-flash   (analysis)
scene_dependency    → gemini-pro     (analysis)
outlook_extraction  → gemini-pro     (analysis)
scene_detail        → gpt            (analysis)
world_guide         → gpt            (image)
prompt_translation  → gemini-flash   (image)
prompt_sanitize     → gpt            (image)
project_summary     → gemini-pro     (analysis)
scene_verify        → gemini-pro     (analysis)
angle_recommend     → gpt            (image)
final_select        → gpt            (image)
webbook_gen         → gpt            (auxiliary)  ← #6 신규
style_rules         → gemini-pro     (analysis)  ← #7 신규
outlook_merge       → gpt            (analysis)  ← #13 신규
```

## 변경 파일 요약

### Backend (16개 파일)
- `llm_client.py` — PIPELINE_STEPS 20개, call_structured에 max_tokens 추가
- `ref_image_pipeline.py` — opik_tags, timeout, empty guard, prompt_loader
- `scene_image_pipeline.py` — 7개 _call_gpt_lvm 호출에 opik_tags 추가
- `entity_extractor_v3.py` — prompt_loader + entity_t2i 키 수정
- `outlook_extractor.py` — prompt_loader
- `outlook_merger.py` — call_structured + prompt_loader + 전용 step 키
- `scene_extractor_v2.py` — prompt_loader
- `webbook_generator.py` — call_structured + prompt_loader + dead code 제거
- `style_rules_generator.py` — call_structured + prompt_loader
- `variation_recommender.py` — call_structured + prompt_loader
- `t2i_prompt_composer.py` — call_structured + prompt_loader
- `image_service.py` — VariationRecommender/T2IPromptComposer 호출자 수정
- `export_service.py` — WebbookGenerator 호출자 수정, OpenAIClient 임포트 제거
- `analysis_steps.py` — entity_detail → entity_t2i
- `llm_router.py` — entity_detail → entity_t2i (구 파일 동기화)
- `prompts.py` — require_admin 적용 (3개 쓰기 엔드포인트)

### Frontend (6개 파일)
- `PipelineStepsPanel.tsx` — catch 에러 로깅
- `PromptManager.tsx` — catch 에러 로깅 (5개)
- `Entities.tsx` — catch 에러 로깅 (12개)
- `ImageReview.tsx` — catch 에러 로깅 (4개)
- `EpisodeDetail.tsx` — catch 에러 로깅 (3개)
- `ProjectDetail.tsx` — catch 에러 로깅 (1개)
