컨텐츠로 건너뛰기

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.tsmodels.dev 데이터 소스 로딩: Zod 스키마 정의, JSON 파싱, 주기적 새로고침, 스냅샷 폴백
src/provider/schema.tsEffect 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를 혼용할 수 없게 만드는 것입니다. ProviderIDModelID는 모두 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 자격 증명 체인 구성
VertexGoogle 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() 시 자동으로 정리됩니다