LLM 프로바이더 관리
모듈 구조
packages/core/src/llm/ 디렉토리 (6 파일):
| 파일 |
역할 |
types.ts |
공유 타입 정의 (LLMProvider, LLMModelId, TokenUsage, CostEstimate, ActualCost) |
factory.ts |
팩토리 함수 (createLLMClient), 기본 모델 결정, 가용 프로바이더 탐색 |
gemini.ts |
Gemini 어댑터 (GeminiAdapter) - @google/genai SDK 사용 |
openai.ts |
OpenAI 어댑터 (OpenAIAdapter) - Responses API + JSON 안정성 보호 |
pricing.ts |
가격표 (MODEL_PRICING_TABLE), 비용 계산, 예산 가드레일 |
index.ts |
Barrel export |
LLMProvider 인터페이스
types.ts에서 정의. 모든 어댑터가 구현하는 공통 인터페이스:
interface LLMProvider {
readonly name: LLMProviderName; // "gemini" | "openai"
generateContent(prompt: string, options: LLMGenerationOptions): Promise<LLMGenerationResult>;
countTokens(text: string, model?: string): Promise<number>;
}
LLMGenerationOptions: systemPrompt, responseMimeType, maxOutputTokens, model
LLMGenerationResult: text, tokenUsage, modelId, actualCost?
LLMModelId: { provider: LLMProviderName, model: string }
Factory 패턴
factory.ts의 createLLMClient(provider):
adapterCache (Map)로 어댑터 싱글톤 캐싱
"gemini" -> GeminiAdapter, "openai" -> OpenAIAdapter
- 지원하지 않는 provider ->
Error throw
기본 모델 결정 (getDefaultModelId()):
ANKI_SPLITTER_DEFAULT_LLM_PROVIDER 환경변수 확인
- 해당 provider가 미가용이면 가용 provider로 graceful fallback
ANKI_SPLITTER_DEFAULT_LLM_MODEL 환경변수 또는 provider별 기본 모델
가용 프로바이더 확인 (getAvailableProviders()):
GEMINI_API_KEY 설정 여부 -> gemini
OPENAI_API_KEY 설정 여부 -> openai
Adapter 패턴
GeminiAdapter (gemini.ts)
- SDK:
@google/genai (GoogleGenAI)
- 클라이언트: lazy 싱글톤 (
genAI 변수)
- 기본 모델:
gemini-3-flash-preview
countTokens: SDK 네이티브 client.models.countTokens() 사용
- 토큰 사용량:
response.usageMetadata에서 추출
OpenAIAdapter (openai.ts)
- SDK:
openai (dynamic import)
- API: Responses API (
client.responses.create())
- 기본 모델:
gpt-5.4-mini
- 시스템 프롬프트 role:
developer (OpenAI Responses API에서 system 대신 developer 사용)
- JSON 모드:
text.format.type = "json_object" + markdown code fence 제거
- JSON 파싱 실패 시 temperature 0.1로 1회 재시도 (토큰 누적 합산)
- refusal 체크:
content.type === "refusal" 감지 시 에러 throw
countTokens: 휴리스틱 추정 (한국어 보정 x1.5, safety x1.3)
getClient() 차이: GeminiAdapter의 getClient()는 동기(sync), OpenAIAdapter의 getClient()는 비동기(async) -- dynamic import로 번들 최적화하기 때문.
가격 시스템 (pricing.ts)
가격표, 비용 계산, 예산 가드 상세는 references/pricing.md 참조.
핵심 함수:
getModelPricing(provider, model) -- 가격 조회
estimateCost() / computeCost() -- 사전/사후 비용 계산
checkBudget(estimatedCostUsd, clientBudgetCapUsd?) -- 이중 예산 가드
getServerBudgetCapUsd() -- 서버 사이드 예산 캡 조회 (env 기본 $1.0)
API 엔드포인트
GET /api/llm/models (packages/server/src/routes/llm.ts)
응답:
{
"models": [
{
"provider": "gemini",
"model": "gemini-3-flash-preview",
"displayName": "Gemini 3 Flash Preview",
"inputPricePerMillionTokens": 0.15,
"outputPricePerMillionTokens": 0.6
}
],
"defaultModelId": { "provider": "gemini", "model": "gemini-3-flash-preview" },
"budgetCapUsd": 1.0,
"availableProviders": ["gemini", "openai"]
}
- API 키가 설정된 provider의 모델만 반환
- 기본 모델이 가용 목록에 없으면 첫 번째 가용 모델로 대체
budgetCapUsd는 서버에서 getServerBudgetCapUsd() 호출 결과
프론트엔드 컴포넌트
ModelBadge (packages/web/src/components/ui/model-badge.tsx)
- provider별 스타일: gemini(파란색, "G"), openai(에메랄드, "O"), fallback("?")
- props:
provider, model?, className?
- 사용처:
SplitWorkspace.tsx, SplitHistory.tsx
formatCostUsd (model-badge.tsx에서 export)
< $0.001 -> 소수점 6자리
< $0.01 -> 소수점 4자리
- 그 외 -> 소수점 2자리
환경변수
| 변수 |
용도 |
기본값 |
GEMINI_API_KEY |
Gemini API 인증 |
(필수) |
OPENAI_API_KEY |
OpenAI API 인증 |
(선택) |
ANKI_SPLITTER_DEFAULT_LLM_PROVIDER |
기본 프로바이더 |
gemini |
ANKI_SPLITTER_DEFAULT_LLM_MODEL |
기본 모델 |
provider별 기본값 |
ANKI_SPLITTER_BUDGET_CAP_USD |
서버 예산 상한 |
1.0 |
상세 참조
references/architecture.md -- LLM 모듈 아키텍처 상세, 새 프로바이더 추가 가이드
references/pricing.md -- MODEL_PRICING_TABLE 전체, 비용 계산/예산 가드 로직
references/troubleshooting.md -- API 키 미설정, 예산 초과, 지원되지 않는 모델 시 동작
1---2name: managing-llm3description: LLM 추상화 계층, 프로바이더 어댑터, 가격표, 예산 가드 등 LLM 관련 작업이면 무조건 이 스킬. 모델 변경, 프로바이더 추가, 비용 계산, 토큰 카운트 등 모든 LLM 인프라를 다룬다. Triggers: "LLM 모델 변경", "프로바이더 추가", "비용 추정", "예산 가드", "pricing table", "모델 비교", "LLM 비용", "토큰 사용량", "모델 추가", "LLM 설정", "Gemini", "OpenAI", "API key", "model pricing", "budget cap", "token count", "adapter", "factory".4---56# LLM 프로바이더 관리78## 모듈 구조910`packages/core/src/llm/` 디렉토리 (6 파일):1112| 파일 | 역할 |13|------|------|14| `types.ts` | 공유 타입 정의 (`LLMProvider`, `LLMModelId`, `TokenUsage`, `CostEstimate`, `ActualCost`) |15| `factory.ts` | 팩토리 함수 (`createLLMClient`), 기본 모델 결정, 가용 프로바이더 탐색 |16| `gemini.ts` | Gemini 어댑터 (`GeminiAdapter`) - `@google/genai` SDK 사용 |17| `openai.ts` | OpenAI 어댑터 (`OpenAIAdapter`) - Responses API + JSON 안정성 보호 |18| `pricing.ts` | 가격표 (`MODEL_PRICING_TABLE`), 비용 계산, 예산 가드레일 |19| `index.ts` | Barrel export |2021## LLMProvider 인터페이스2223`types.ts`에서 정의. 모든 어댑터가 구현하는 공통 인터페이스:2425```typescript26interface LLMProvider {27 readonly name: LLMProviderName; // "gemini" | "openai"28 generateContent(prompt: string, options: LLMGenerationOptions): Promise<LLMGenerationResult>;29 countTokens(text: string, model?: string): Promise<number>;30}31```3233- `LLMGenerationOptions`: `systemPrompt`, `responseMimeType`, `maxOutputTokens`, `model`34- `LLMGenerationResult`: `text`, `tokenUsage`, `modelId`, `actualCost?`35- `LLMModelId`: `{ provider: LLMProviderName, model: string }`3637## Factory 패턴3839`factory.ts`의 `createLLMClient(provider)`:40- `adapterCache` (Map)로 어댑터 싱글톤 캐싱41- `"gemini"` -> `GeminiAdapter`, `"openai"` -> `OpenAIAdapter`42- 지원하지 않는 provider -> `Error` throw4344기본 모델 결정 (`getDefaultModelId()`):451. `ANKI_SPLITTER_DEFAULT_LLM_PROVIDER` 환경변수 확인462. 해당 provider가 미가용이면 가용 provider로 graceful fallback473. `ANKI_SPLITTER_DEFAULT_LLM_MODEL` 환경변수 또는 provider별 기본 모델4849가용 프로바이더 확인 (`getAvailableProviders()`):50- `GEMINI_API_KEY` 설정 여부 -> gemini51- `OPENAI_API_KEY` 설정 여부 -> openai5253## Adapter 패턴5455### GeminiAdapter (`gemini.ts`)5657- SDK: `@google/genai` (`GoogleGenAI`)58- 클라이언트: lazy 싱글톤 (`genAI` 변수)59- 기본 모델: `gemini-3-flash-preview`60- `countTokens`: SDK 네이티브 `client.models.countTokens()` 사용61- 토큰 사용량: `response.usageMetadata`에서 추출6263### OpenAIAdapter (`openai.ts`)6465- SDK: `openai` (dynamic import)66- API: Responses API (`client.responses.create()`)67- 기본 모델: `gpt-5.4-mini`68- **시스템 프롬프트 role**: `developer` (OpenAI Responses API에서 `system` 대신 `developer` 사용)69- JSON 모드: `text.format.type = "json_object"` + markdown code fence 제거70- JSON 파싱 실패 시 temperature 0.1로 1회 재시도 (토큰 누적 합산)71- refusal 체크: `content.type === "refusal"` 감지 시 에러 throw72- `countTokens`: 휴리스틱 추정 (한국어 보정 x1.5, safety x1.3)7374> **getClient() 차이**: GeminiAdapter의 `getClient()`는 동기(sync), OpenAIAdapter의 `getClient()`는 비동기(async) -- dynamic import로 번들 최적화하기 때문.7576## 가격 시스템 (`pricing.ts`)7778가격표, 비용 계산, 예산 가드 상세는 `references/pricing.md` 참조.7980핵심 함수:81- `getModelPricing(provider, model)` -- 가격 조회82- `estimateCost()` / `computeCost()` -- 사전/사후 비용 계산83- `checkBudget(estimatedCostUsd, clientBudgetCapUsd?)` -- 이중 예산 가드84- `getServerBudgetCapUsd()` -- 서버 사이드 예산 캡 조회 (env 기본 $1.0)8586## API 엔드포인트8788### `GET /api/llm/models` (`packages/server/src/routes/llm.ts`)8990응답:91```json92{93 "models": [94 {95 "provider": "gemini",96 "model": "gemini-3-flash-preview",97 "displayName": "Gemini 3 Flash Preview",98 "inputPricePerMillionTokens": 0.15,99 "outputPricePerMillionTokens": 0.6100 }101 ],102 "defaultModelId": { "provider": "gemini", "model": "gemini-3-flash-preview" },103 "budgetCapUsd": 1.0,104 "availableProviders": ["gemini", "openai"]105}106```107108- API 키가 설정된 provider의 모델만 반환109- 기본 모델이 가용 목록에 없으면 첫 번째 가용 모델로 대체110- `budgetCapUsd`는 서버에서 `getServerBudgetCapUsd()` 호출 결과111112## 프론트엔드 컴포넌트113114### ModelBadge (`packages/web/src/components/ui/model-badge.tsx`)115116- provider별 스타일: gemini(파란색, "G"), openai(에메랄드, "O"), fallback("?")117- props: `provider`, `model?`, `className?`118- 사용처: `SplitWorkspace.tsx`, `SplitHistory.tsx`119120### formatCostUsd (`model-badge.tsx`에서 export)121122- `< $0.001` -> 소수점 6자리123- `< $0.01` -> 소수점 4자리124- 그 외 -> 소수점 2자리125126## 환경변수127128| 변수 | 용도 | 기본값 |129|------|------|--------|130| `GEMINI_API_KEY` | Gemini API 인증 | (필수) |131| `OPENAI_API_KEY` | OpenAI API 인증 | (선택) |132| `ANKI_SPLITTER_DEFAULT_LLM_PROVIDER` | 기본 프로바이더 | `gemini` |133| `ANKI_SPLITTER_DEFAULT_LLM_MODEL` | 기본 모델 | provider별 기본값 |134| `ANKI_SPLITTER_BUDGET_CAP_USD` | 서버 예산 상한 | `1.0` |135136## 상세 참조137138- `references/architecture.md` -- LLM 모듈 아키텍처 상세, 새 프로바이더 추가 가이드139- `references/pricing.md` -- MODEL_PRICING_TABLE 전체, 비용 계산/예산 가드 로직140- `references/troubleshooting.md` -- API 키 미설정, 예산 초과, 지원되지 않는 모델 시 동작