Provider 소스 코드 분석
모듈 개요
Provider는 OpenCode의 모델 추상화 계층이자 라우팅 허브로, packages/opencode/src/provider/에 위치합니다. TypeScript 버전에서는 더 이상 HTTP 클라이언트를 직접 구축하지 않고, Vercel AI SDK(ai 패키지)를 사용하여 30개 이상의 LLM Provider와 통일되게 연동합니다. 상위 모듈(Agent, Session)은 Provider.getLanguage()를 통해 표준화된 LanguageModel 객체를 획득한 다음 streamText / generateText를 호출하여, 기반 API 차이와 완전히 분리됩니다.
핵심 설계 선택:
- Vercel AI SDK를 통합 어댑터 계층으로 사용하여, 각 Provider에 대한 수작업 HTTP 프로토콜을 피함
- Effect Schema 브랜디드 타입(
ProviderID,ModelID)으로 타입 레벨에서 서로 다른 엔티티를 구분 - models.dev를 모델 메타데이터의 외부 데이터 소스로 사용하여 런타임에 동적으로 로딩
CUSTOM_LOADERS사전이 각 Provider의 특수 로직을 처리(Bedrock 리전 접두사, Copilot SDK 적응, Vertex 인증 등)- **
Instance.state**가 프로세스 레벨 싱글톤 캐시를 제공하여 Provider 초기화가 한 번만 실행됨
주요 파일
| 파일 | 책임 |
|---|---|
src/provider/provider.ts | 핵심 네임스페이스: 상태 관리, Provider 라우팅, SDK 팩토리, 모델 조회, CUSTOM_LOADERS |
src/provider/models.ts | models.dev 데이터 소스 로딩: Zod 스키마 정의, JSON 파싱, 주기적 새로고침, 스냅샷 폴백 |
src/provider/schema.ts | Effect Schema 브랜디드 타입 ProviderID / ModelID, Zod 브릿지 포함 |
타입 시스템
ProviderID / ModelID — 브랜디드 타입
// schema.ts — Effect Schema 브랜디드 타입
const ProviderID = Schema.String.pipe(Schema.brand("ProviderID"))
const ModelID = Schema.String.pipe(Schema.brand("ModelID"))
브랜디드 타입의 가치는 컴파일 타임에 서로 다른 ID를 혼용할 수 없게 만드는 것입니다. ProviderID와 ModelID는 모두 string 기반이지만, TypeScript는 ModelID가 필요한 곳에 ProviderID를 전달하는 것을 거부합니다. 이것이 Provider 모듈에서 가장 자주 발생하는 버그 유형(잘못된 ID 전달)을 컴파일 타임에 차단합니다.
Provider.Info — Provider 설정 스키마
const Info = z.object({
id: ProviderID.Schema,
name: z.string(),
models: z.record(z.string(), Model.Info),
installed: z.boolean().optional(), // SDK가 설치되어 있는지 여부
})
Model.Info — 모델 메타데이터 스키마
const Info = z.object({
id: ModelID.Schema,
name: z.string(),
providerID: ProviderID.Schema,
limit: z.object({
input: z.number().optional(), // 최대 입력 토큰
output: z.number().optional(), // 최대 출력 토큰
context: z.number().optional(), // 컨텍스트 윈도우 크기
}).optional(),
pricing: z.object({
input: z.number().optional(), // 입력 토큰당 가격
output: z.number().optional(), // 출력 토큰당 가격
}).optional(),
options: z.record(z.any()).optional(), // Provider별 특수 옵션
})
코어 플로우
Provider 해석: getLanguage()
getLanguage()는 상위 모듈이 LLM을 호출하는 기본 진입점으로, providerID/modelID 형식의 문자열을 AI SDK 호환 LanguageModel 객체로 변환합니다:
async function getLanguage(model: { providerID: string; modelID: string }): Promise<LanguageModel> {
// 1. CUSTOM_LOADERS에서 Provider 특수 로더 조회
const loader = CUSTOM_LOADERS[model.providerID]
if (loader) return loader(model.modelID)
// 2. 표준 로딩: AI SDK createProvider 호출
const provider = await getProvider(model.providerID)
return provider.languageModel(model.modelID)
}
CUSTOM_LOADERS — Provider 특수 로직
일부 Provider는 표준 AI SDK 로딩으로 처리할 수 없는 특수 로직이 필요합니다:
| Provider | 특수 로직 |
|---|---|
| Bedrock | 리전 접두사 처리, AWS 자격 증명 체인 구성 |
| Vertex | Google Cloud 인증, 프로젝트/리전 설정 |
| Copilot | 커스텀 SDK 어댑터, 인증 토큰 주입 |
| Venice | 커스텀 API 엔드포인트, 특수 헤더 |
models.dev — 모델 메타데이터 동적 로딩
모델 메타데이터(컨텍스트 윈도우, 가격 책정, 기능)는 코드에 하드코딩되지 않고 models.dev에서 동적으로 로딩됩니다:
// models.ts — 데이터 소스 로딩
async function loadModels() {
const response = await fetch("https://models.dev/api.json")
const data = await response.json()
return ModelsSchema.parse(data) // Zod 검증
}
스냅샷 폴백: 네트워크를 사용할 수 없는 경우, 모듈은 마지막으로 성공적으로 로딩한 데이터의 스냅샷을 사용합니다. 이를 통해 오프라인 환경에서도 모델 목록을 사용할 수 있습니다.
InstanceState 캐시
Provider 초기화는 Instance.state()를 통해 캐시됩니다:
const state = Instance.state(async () => {
// models.dev에서 모델 데이터 로딩
const models = await loadModels()
// CUSTOM_LOADERS 초기화
// Provider 레지스트리 구축
return buildProviderRegistry(models)
})
캐시는 프로젝트 인스턴스의 수명 주기에 바인딩되어, Instance.dispose() 호출 시 자동으로 정리됩니다.
호출 체인 예제
체인 1: 모델 선택 → LLM 호출
1. Agent가 모델 선택: "anthropic/claude-sonnet-4"
│
▼
2. Provider.getLanguage({ providerID: "anthropic", modelID: "claude-sonnet-4" })
├─ CUSTOM_LOADERS["anthropic"] → undefined (표준 로딩)
├─ getProvider("anthropic")
│ ├─ Config에서 API 키 획득 ({env:ANTHROPIC_API_KEY} 치환 후)
│ └─ AI SDK createAnthropic({ apiKey }) 반환
└─ provider.languageModel("claude-sonnet-4") → LanguageModel 반환
3. Session에서 streamText({ model: languageModel, messages, tools }) 호출
└─ Vercel AI SDK가 Anthropic API와 통신
설계 트레이드오프
| 결정 | 근거 |
|---|---|
| Vercel AI SDK를 통합 어댑터로 사용 | 각 Provider에 대한 HTTP 클라이언트를 수작업으로 작성하는 대신 AI SDK가 프로토콜 차이를 추상화합니다. 30개 이상의 Provider를 단일 languageModel() 인터페이스로 통합 |
| 브랜디드 타입 | ProviderID와 ModelID의 혼용으로 인한 런타임 버그를 컴파일 타임에 방지합니다. 이는 Provider 모듈에서 가장 흔한 버그 유형입니다 |
| models.dev 외부 데이터 소스 | 모델 메타데이터를 코드에 하드코딩하지 않고 외부에서 동적으로 로딩합니다. 새로운 모델이 출시되면 코드 수정 없이 자동으로 인식됩니다 |
| CUSTOM_LOADERS 패턴 | 표준 AI SDK 로딩으로는 커버할 수 없는 Provider 특수 로직을 사전에 등록하여, 메인 로직을 깔끔하게 유지합니다 |
| 스냅샷 폴백 | 네트워크 장애 시에도 모델 목록을 사용할 수 있어 오프라인 개발 환경에서 중단 없는 경험을 제공합니다 |
다른 모듈과의 관계
- Agent/Session:
LLM.stream()은Provider.getLanguage()를 통해 AI SDK 호환LanguageModel을 획득합니다.Provider.getModel()을 통해 모델 메타데이터(컨텍스트 윈도우, 가격 책정 등)를 획득합니다 - Config:
Config.get().provider가 Provider 설정과 API 키를 제공합니다.{env:VAR}변수 치환이 적용된 실제 값입니다 - Auth: Provider 인증은
Auth.get(providerID)를 통해 관리됩니다. API 키, OAuth 토큰 등의 인증 정보를 제공합니다 - MCP: MCP 도구의 실행 결과는
dynamicTool을 통해 Agent에 반환되어 Provider 모듈의 LLM 호출 흐름에 통합됩니다 - Instance: Provider 캐시는
Instance.state()를 통해 관리되며,Instance.dispose()시 자동으로 정리됩니다