title: content-pipeline OpenRouter 전환 L1 설계 플랜 date: 2026-02-26 status: draft reviewed_by: "jarvis" approved_by: "" okr_target: "O3 콘텐츠 — AI 생성 언블로킹" parent_plan: "openrouter-migration-l0.md"
content-pipeline OpenRouter 전환 L1 설계 플랜
개요
L0 기획서 기반, 5개 파일의 정확한 변경 사항을 라인 단위로 명세한다.
핵심: @google/generative-ai SDK → openai SDK (OpenRouter 경유) 전환.
1. package.json
경로: projects/content-pipeline/package.json
변경 내용: 의존성 교체
Before (L24):
"@google/generative-ai": "^0.21.0",
After:
"openai": "^4.77.0",
변경 설명:
@google/generative-ai제거openaiSDK 추가 (OpenRouter는 OpenAI 호환 API)- 버전
^4.77.0은 현재 안정 버전 (chat.completions 지원)
실행 명령
cd projects/content-pipeline
npm uninstall @google/generative-ai
npm install openai
2. src/pipeline/generate.ts (뉴스레터 생성)
경로: projects/content-pipeline/src/pipeline/generate.ts
총 457줄, 변경 대상 4곳
2-1. import 변경 (L1)
Before:
import { GoogleGenerativeAI } from "@google/generative-ai";
After:
import OpenAI from "openai";
2-2. Mock fallback 조건 변경 (L251~L253)
Before:
if (!process.env.GOOGLE_API_KEY) {
console.log("[generate] GOOGLE_API_KEY not set. Generating mock newsletter.");
return generateMockNewsletter(news);
}
After:
if (!process.env.OPENROUTER_API_KEY) {
console.log("[generate] OPENROUTER_API_KEY not set. Generating mock newsletter.");
return generateMockNewsletter(news);
}
2-3. AI 호출 로직 변경 (L257~L262)
Before:
console.log("[generate] Calling Gemini Flash API...");
const genAI = new GoogleGenerativeAI(process.env.GOOGLE_API_KEY);
const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash" });
const geminiResult = await model.generateContent(fullPrompt);
const responseText = geminiResult.response.text();
After:
console.log("[generate] Calling OpenRouter API (google/gemini-2.0-flash-exp)...");
const client = new OpenAI({
baseURL: process.env.OPENROUTER_BASE_URL || "https://openrouter.ai/api/v1",
apiKey: process.env.OPENROUTER_API_KEY,
});
const completion = await client.chat.completions.create({
model: "google/gemini-2.0-flash-exp",
messages: [{ role: "user", content: fullPrompt }],
});
const responseText = completion.choices[0]?.message?.content || "";
변경 설명:
- Google SDK의
generateContent(prompt)단일 프롬프트 방식 → OpenAI SDK의chat.completions.createmessages 배열 방식 - 뉴스레터 생성은 system prompt 없이 user message만 사용 (기존과 동일 동작)
baseURL은 환경변수 또는 하드코딩 기본값
2-4. 에러 로그 변경 (L267, L284)
Before:
console.error("[generate] Failed to parse JSON from Gemini response");
console.error("[generate] Gemini API error:", err);
After:
console.error("[generate] Failed to parse JSON from OpenRouter response");
console.error("[generate] OpenRouter API error:", err);
2-5. Mock 뉴스레터 안내문 변경 (L375)
Before:
이 뉴스레터는 GOOGLE_API_KEY가 설정되면 AI가 자동으로 고품질 콘텐츠를 생성합니다. 현재는 미리보기 버전입니다.
After:
이 뉴스레터는 OPENROUTER_API_KEY가 설정되면 AI가 자동으로 고품질 콘텐츠를 생성합니다. 현재는 미리보기 버전입니다.
3. src/pipeline/generate-blog.ts (블로그 포스트 생성)
경로: projects/content-pipeline/src/pipeline/generate-blog.ts
총 615줄, 변경 대상 4곳
3-1. import 변경 (L1)
Before:
import { GoogleGenerativeAI } from "@google/generative-ai";
After:
import OpenAI from "openai";
3-2. Mock fallback 조건 변경 (L374, L392)
Before (L374, mock 함수 내 로그):
console.log("[generate-blog] GOOGLE_API_KEY 미설정. Mock 블로그 포스트를 생성합니다.");
After:
console.log("[generate-blog] OPENROUTER_API_KEY 미설정. Mock 블로그 포스트를 생성합니다.");
Before (L392, generateBlogPost 함수):
if (!process.env.GOOGLE_API_KEY) {
return generateMockBlogPost(topic, pillar, newsContext);
}
After:
if (!process.env.OPENROUTER_API_KEY) {
return generateMockBlogPost(topic, pillar, newsContext);
}
3-3. AI 호출 로직 변경 (L415~L423)
이 파일이 가장 중요한 변경. Google SDK의 systemInstruction 파라미터를 OpenAI SDK의 messages[0].role = "system"으로 전환해야 한다.
Before:
console.log(`[generate-blog] Gemini Flash API 호출 중... (필라: ${pillar || "미지정"})`);
const genAI = new GoogleGenerativeAI(process.env.GOOGLE_API_KEY!);
const model = genAI.getGenerativeModel({
model: "gemini-2.0-flash",
systemInstruction: systemPrompt,
});
const geminiResult = await model.generateContent(userPrompt);
const responseText = geminiResult.response.text();
After:
console.log(`[generate-blog] OpenRouter API 호출 중 (google/gemini-2.0-flash-exp)... (필라: ${pillar || "미지정"})`);
const client = new OpenAI({
baseURL: process.env.OPENROUTER_BASE_URL || "https://openrouter.ai/api/v1",
apiKey: process.env.OPENROUTER_API_KEY,
});
const completion = await client.chat.completions.create({
model: "google/gemini-2.0-flash-exp",
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userPrompt },
],
});
const responseText = completion.choices[0]?.message?.content || "";
변경 설명:
systemInstruction→messages[0].role = "system": OpenRouter/OpenAI SDK 표준 방식userPrompt→messages[1].role = "user": 기존 동작과 의미적으로 동일responseText추출 방식 변경:.response.text()→.choices[0].message.content
3-4. 에러 로그 변경 (L428~L433, L580)
Before (L428~L433):
console.error(
"[generate-blog] Gemini 응답에서 JSON을 파싱할 수 없습니다."
);
console.error(
"[generate-blog] 응답 (처음 500자):",
responseText.slice(0, 500)
);
After:
console.error(
"[generate-blog] OpenRouter 응답에서 JSON을 파싱할 수 없습니다."
);
console.error(
"[generate-blog] 응답 (처음 500자):",
responseText.slice(0, 500)
);
Before (L580):
console.error("[generate-blog] Gemini API 오류:", err);
After:
console.error("[generate-blog] OpenRouter API 오류:", err);
4. src/pipeline/stage-generate.ts (생성 스테이지 오케스트레이터)
경로: projects/content-pipeline/src/pipeline/stage-generate.ts
총 209줄, 변경 대상 1곳
4-1. 모델명 로깅 변경 (L192)
Before:
model: process.env.GOOGLE_API_KEY ? 'gemini-2.0-flash' : 'mock',
After:
model: process.env.OPENROUTER_API_KEY ? 'google/gemini-2.0-flash-exp' : 'mock',
변경 설명:
- 환경변수 참조:
GOOGLE_API_KEY→OPENROUTER_API_KEY - 모델명:
gemini-2.0-flash→google/gemini-2.0-flash-exp(OpenRouter 모델명) - 이 파일은 SDK를 직접 사용하지 않으므로 import 변경 없음
5. .env.example (환경변수 템플릿)
경로: projects/content-pipeline/.env.example
변경 내용: Google API 섹션 → OpenRouter 섹션
Before (L17~L24):
# -----------------------------------------------------------------------------
# Anthropic Claude API (선택 - 없으면 mock 모드)
# -----------------------------------------------------------------------------
# 뉴스레터 콘텐츠 자동 생성에 사용됩니다.
# 미설정 시: 수집된 뉴스를 기반으로 mock 뉴스레터를 생성합니다.
# 설정 시: Claude Sonnet이 고품질 한국어 뉴스레터를 자동 생성합니다.
# 발급: https://console.anthropic.com > API Keys
# 예상 비용: ~$0.05/뉴스레터 (월 4회 = ~$0.20/월)
ANTHROPIC_API_KEY=sk-ant-api03-...
After:
# -----------------------------------------------------------------------------
# OpenRouter API (선택 - 없으면 mock 모드)
# -----------------------------------------------------------------------------
# AI 콘텐츠 생성에 사용됩니다 (OpenRouter 경유, OpenAI 호환 API).
# 미설정 시: mock 콘텐츠를 생성합니다.
# 설정 시: google/gemini-2.0-flash-exp로 고품질 한국어 콘텐츠를 자동 생성합니다.
# 발급: https://openrouter.ai > Settings > API Keys
# 정책: openrouter-policy.md 참조 (CEO 확정 — 모든 AI API는 OpenRouter 경유)
OPENROUTER_API_KEY=sk-or-v1-...
OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
6. 변경하지 않는 파일 목록 (확인)
| 파일 | 이유 |
|---|---|
src/pipeline/collect.ts | RSS 수집, AI API 미사용 |
src/pipeline/publish.ts | 발행, AI API 미사용 |
src/pipeline/publish-blog.ts | 블로그 발행, AI API 미사용 |
src/pipeline/run.ts | 오케스트레이터, generate 함수 호출만 |
src/pipeline/run-blog-pipeline.ts | 오케스트레이터, SDK 미사용 |
src/lib/ | 유틸리티, AI API 미사용 |
prompts/ | 프롬프트 템플릿, 변경 불필요 |
7. 테스트 시나리오 (L2 실행 시 검증 기준)
7-1. 빌드 성공 확인
cd projects/content-pipeline
npm run build
기대 결과: TypeScript 컴파일 에러 0건. @google/generative-ai import 없음.
7-2. Mock 모드 정상 동작 확인
OPENROUTER_API_KEY 미설정 상태에서:
# 블로그 포스트 mock 생성
npx tsx src/pipeline/generate-blog.ts "소상공인 ChatGPT 활용법" AI도구리뷰
기대 결과:
[generate-blog] OPENROUTER_API_KEY 미설정. Mock 블로그 포스트를 생성합니다.로그 출력- Mock 포스트 JSON 정상 출력
- 퀄리티 검증 6/8 이상
7-3. import 잔재 확인
grep -r "@google/generative-ai" projects/content-pipeline/src/
grep -r "GOOGLE_API_KEY" projects/content-pipeline/src/
기대 결과: 매칭 0건 (schema/backup 등 제외)
7-4. (CEO 키 발급 후) 실제 API 테스트
OPENROUTER_API_KEY=sk-or-... npx tsx src/pipeline/generate-blog.ts "소상공인 AI 마케팅" AI도구리뷰
기대 결과:
[generate-blog] OpenRouter API 호출 중로그 출력- 실제 AI 생성 콘텐츠 반환
- 퀄리티 검증 6/8 이상
8. 변경 요약 매트릭스
| 파일 | import | 환경변수 | API 호출 | 로그/기타 | 난이도 |
|---|---|---|---|---|---|
package.json | - | - | - | 의존성 교체 | 하 |
generate.ts | L1 | L251~253 | L257~262 | L267,284,375 | 중 |
generate-blog.ts | L1 | L374,392 | L415~423 | L428~433,580 | 중 |
stage-generate.ts | - | L192 | - | - | 하 |
.env.example | - | L17~24 | - | - | 하 |
총 변경 라인: 약 40~50줄 (기존 코드 구조 유지, SDK 교체만)
9. JSON 파싱 호환성
기존 코드의 robust JSON 파싱 로직은 변경 없이 그대로 유지:
generate.tsL265:responseText.match(/```json\s*([\s\S]*?)\s*```/)— 동일 동작generate-blog.tsL426~L545:escapeNewlinesInJsonStrings(),extractField()— 동일 동작
OpenRouter 경유 Gemini Flash 응답은 동일한 형식으로 JSON을 반환하므로 파싱 로직 변경 불필요.
10. 리스크 및 대응
| 리스크 | 대응 |
|---|---|
openai SDK 버전 호환 | ^4.77.0 안정 버전 지정, chat.completions.create 표준 API |
| OpenRouter 응답 형식 차이 | JSON 코드블록 파싱 로직 유지, 기존 fallback 로직이 커버 |
| 환경변수 누락 | mock fallback이 모든 경우 동작 (기존과 동일) |
| TypeScript 타입 오류 | openai SDK는 완전한 TS 타입 제공 |