title: content-pipeline OpenRouter 전환 플랜 L0
date: 2026-02-26
status: draft
reviewed_by: "jarvis"
approved_by: ""
okr_target: "O3 콘텐츠 — AI 생성 언블로킹"
content-pipeline OpenRouter 전환 L0 기획서
1. 목표
왜 전환하는가
- CEO 확정 정책: 모든 AI API 호출은 OpenRouter 단일 엔드포인트 경유 (openrouter-policy.md)
- CEO 블로킹 해소: 현재
GOOGLE_API_KEY 미발급으로 AI 생성이 mock 모드. OpenRouter 전환 시 OPENROUTER_API_KEY 하나만 발급받으면 모든 AI 호출 언블로킹
- 통합 관리: 개별 API 키(Google, Anthropic, OpenAI) 각각 관리 대신 OpenRouter에서 한도/비용 통합 관리
- SDK 단순화:
@google/generative-ai 제거 → openai SDK 하나로 통일 (OpenRouter는 OpenAI 호환 API)
기대 효과
| 항목 | 전환 전 | 전환 후 |
|---|
| API 키 관리 | GOOGLE_API_KEY (미발급) | OPENROUTER_API_KEY (1개) |
| SDK | @google/generative-ai | openai (OpenAI 호환) |
| 모델 | gemini-2.0-flash (직접) | google/gemini-2.0-flash-exp (OpenRouter 경유) |
| AI 생성 상태 | mock 모드 (블로킹) | 실제 생성 가능 |
| 비용 통제 | Google 콘솔 개별 관리 | OpenRouter 대시보드 한도 설정 |
2. 현황: 현재 코드 구조
Google Generative AI SDK 사용 파일 (3개)
| 파일 | 역할 | SDK 사용 방식 |
|---|
src/pipeline/generate.ts | 뉴스레터 생성 | GoogleGenerativeAI → getGenerativeModel("gemini-2.0-flash") → generateContent() |
src/pipeline/generate-blog.ts | 블로그 포스트 생성 | GoogleGenerativeAI → getGenerativeModel("gemini-2.0-flash", systemInstruction) → generateContent() |
src/pipeline/stage-generate.ts | 생성 스테이지 오케스트레이터 | 직접 SDK 미사용. generate-blog.ts 호출 + GOOGLE_API_KEY 유무로 모델명 로깅 |
현재 의존성
// package.json
"@google/generative-ai": "^0.21.0"
Mock 모드 동작
generate.ts L251: if (!process.env.GOOGLE_API_KEY) → generateMockNewsletter() 반환
generate-blog.ts L392: if (!process.env.GOOGLE_API_KEY) → generateMockBlogPost() 반환
stage-generate.ts L192: 로그에 GOOGLE_API_KEY ? 'gemini-2.0-flash' : 'mock' 기록
API 호출 패턴 (Google SDK)
// generate.ts (뉴스레터)
const genAI = new GoogleGenerativeAI(process.env.GOOGLE_API_KEY);
const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash" });
const result = await model.generateContent(fullPrompt);
const text = result.response.text();
// generate-blog.ts (블로그)
const genAI = new GoogleGenerativeAI(process.env.GOOGLE_API_KEY!);
const model = genAI.getGenerativeModel({
model: "gemini-2.0-flash",
systemInstruction: systemPrompt,
});
const result = await model.generateContent(userPrompt);
const text = result.response.text();
3. 전환 범위
수정 파일 목록 (5개)
| 파일 | 변경 내용 | 난이도 |
|---|
src/pipeline/generate.ts | Google SDK → OpenAI SDK (OpenRouter), 환경변수 변경, mock fallback 유지 | 중 |
src/pipeline/generate-blog.ts | Google SDK → OpenAI SDK (OpenRouter), systemInstruction → messages 변환, mock fallback 유지 | 중 |
src/pipeline/stage-generate.ts | 환경변수 참조 변경 (GOOGLE_API_KEY → OPENROUTER_API_KEY), 모델명 로깅 변경 | 하 |
package.json | @google/generative-ai 제거, openai 추가 | 하 |
.env.example / Vercel env | 환경변수 키 변경 | 하 |
변경하지 않는 파일
src/pipeline/collect.ts — RSS 수집 (AI API 미사용)
src/pipeline/publish.ts / publish-blog.ts — 발행 (AI API 미사용)
src/pipeline/run.ts / run-blog-pipeline.ts — 오케스트레이터 (generate 함수 호출만, SDK 미사용)
src/lib/ — 유틸리티 (AI API 미사용)
- 프롬프트 파일들 (
prompts/) — 변경 불필요
4. 환경변수 변경
제거
GOOGLE_API_KEY=AIza... (더 이상 사용 안 함)
추가
OPENROUTER_API_KEY=sk-or-... # CEO 발급 필요
OPENROUTER_BASE_URL=https://openrouter.ai/api/v1 # 고정값 (코드 내 default 가능)
적용 위치
- 로컬:
.env.local
- Vercel: content-pipeline 프로젝트 환경변수
- 향후: apppro.kr 등 다른 프로젝트도 동일 키 공유
5. 모델 매핑
| 용도 | 현재 (Google 직접) | 전환 후 (OpenRouter) |
|---|
| 뉴스레터 생성 | gemini-2.0-flash | google/gemini-2.0-flash-exp |
| 블로그 포스트 생성 | gemini-2.0-flash | google/gemini-2.0-flash-exp |
OpenRouter 모델명은 openrouter-policy.md에 CEO 확정된 매핑 기준.
향후 고품질 콘텐츠용 anthropic/claude-sonnet-4-6도 OpenRouter 경유로 전환 가능 (이번 범위 아님).
6. 코드 전환 패턴
Before (Google SDK)
import { GoogleGenerativeAI } from "@google/generative-ai";
const genAI = new GoogleGenerativeAI(process.env.GOOGLE_API_KEY);
const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash" });
const result = await model.generateContent(prompt);
const text = result.response.text();
After (OpenAI SDK via OpenRouter)
import OpenAI from "openai";
const client = new OpenAI({
baseURL: process.env.OPENROUTER_BASE_URL || "https://openrouter.ai/api/v1",
apiKey: process.env.OPENROUTER_API_KEY,
});
const response = await client.chat.completions.create({
model: "google/gemini-2.0-flash-exp",
messages: [
{ role: "system", content: systemPrompt }, // generate-blog.ts만 해당
{ role: "user", content: userPrompt },
],
});
const text = response.choices[0]?.message?.content || "";
generate-blog.ts 특이사항
- Google SDK의
systemInstruction 파라미터 → OpenAI SDK의 messages[0].role = "system" 으로 전환
- 이 변환은 자연스럽게 매핑됨 (OpenRouter가 system message 지원)
Mock 모드 유지
// 전환 후에도 동일 패턴 유지
if (!process.env.OPENROUTER_API_KEY) {
console.log("[generate] OPENROUTER_API_KEY not set. Generating mock.");
return generateMockNewsletter(news); // 또는 generateMockBlogPost()
}
7. 테스트 계획
로컬 테스트
-
Mock 모드 테스트: OPENROUTER_API_KEY 미설정 상태에서 기존 mock 동작 확인
npm run generate:blog "테스트 주제" AI도구리뷰
npm run pipeline:generate
-
실제 API 테스트 (CEO 키 발급 후):
OPENROUTER_API_KEY=sk-or-... npm run generate:blog "소상공인 ChatGPT 활용법" AI도구리뷰
-
품질 검증: validateQuality() 통과 확인 (6/8 이상)
Vercel 배포 테스트
- Vercel 환경변수에
OPENROUTER_API_KEY, OPENROUTER_BASE_URL 설정
/api/pipeline/generate 엔드포인트 호출
- content_queue에 draft 정상 생성 확인
- 로그에
google/gemini-2.0-flash-exp 모델명 기록 확인
빌드 테스트
npm run build # TypeScript 컴파일 에러 없는지 확인
8. CEO 블로킹
| 항목 | 상태 | 액션 |
|---|
OPENROUTER_API_KEY 발급 | CEO 발급 필요 | https://openrouter.ai 가입 → API 키 생성 → 한도 설정 |
| Vercel env 설정 | CEO 키 발급 후 | OPENROUTER_API_KEY + OPENROUTER_BASE_URL 추가 |
코드 전환 자체는 CEO 블로킹 없이 진행 가능. 키 없어도 mock 모드로 동작.
CEO 키 발급 후 즉시 실제 AI 생성 활성화.
9. 예상 소요
| 단계 | 소요 시간 | 설명 |
|---|
| L1 상세 설계 | 15분 | 파일별 변경 사항 상세 명세 |
| L2 구현 | 30~45분 | SDK 교체, 테스트, 빌드 확인 |
| Vercel 배포 + 검증 | 15분 | push → 배포 → mock 모드 검증 |
| 합계 | 약 1~1.5시간 | CEO 키 발급 대기 시간 제외 |
10. 리스크 및 대응
| 리스크 | 확률 | 대응 |
|---|
| OpenRouter 경유 시 응답 지연 | 낮음 | Gemini Flash 자체가 빠름. 타임아웃 30s 설정 |
| OpenRouter 모델명 변경 | 낮음 | 환경변수로 모델명 주입 (하드코딩 최소화) |
| JSON 파싱 실패 (모델 차이) | 중간 | 기존 robust 파싱 로직 유지 (escapeNewlinesInJsonStrings, extractField 등) |
| mock 모드 퇴행 | 낮음 | 환경변수 미설정 시 mock fallback 로직 그대로 보존 |
11. 다음 단계
- VP 검수 → 이 L0 승인
- L1 상세 설계 → 파일별 변경 diff 명세
- L2 구현 + 테스트 → PL 스폰하여 실행
- CEO 키 발급 요청 → Telegram으로 OPENROUTER_API_KEY 요청
- Vercel env 설정 + 실제 AI 생성 활성화