← 목록으로
2026-02-26plans

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 제거
  • openai SDK 추가 (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.create messages 배열 방식
  • 뉴스레터 생성은 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 || "";

변경 설명:

  • systemInstructionmessages[0].role = "system": OpenRouter/OpenAI SDK 표준 방식
  • userPromptmessages[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_KEYOPENROUTER_API_KEY
  • 모델명: gemini-2.0-flashgoogle/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.tsRSS 수집, 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.tsL1L251~253L257~262L267,284,375
generate-blog.tsL1L374,392L415~423L428~433,580
stage-generate.ts-L192--
.env.example-L17~24--

총 변경 라인: 약 40~50줄 (기존 코드 구조 유지, SDK 교체만)


9. JSON 파싱 호환성

기존 코드의 robust JSON 파싱 로직은 변경 없이 그대로 유지:

  • generate.ts L265: responseText.match(/```json\s*([\s\S]*?)\s*```/) — 동일 동작
  • generate-blog.ts L426~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 타입 제공
plans/2026/02/26/openrouter-migration-l1.md