Skip to content

Provider Source Code Analysis

Module Overview

Provider is OpenCode’s model abstraction layer and routing hub, located at packages/opencode/src/provider/. In the TypeScript version, it no longer builds HTTP clients directly, but instead uses the Vercel AI SDK (ai package) to uniformly interface with 30+ LLM Providers. Upper-layer modules (Agent, Session) obtain standardized LanguageModel objects via Provider.getLanguage() and then call streamText / generateText, completely decoupled from underlying API differences.

Core design choices:

  • Vercel AI SDK as the unified adapter layer, avoiding hand-written HTTP protocols for each Provider
  • Effect Schema branded types (ProviderID, ModelID) that distinguish different entities at the type level
  • models.dev as the external data source for model metadata, dynamically loaded at runtime
  • CUSTOM_LOADERS dictionary handling special logic for each Provider (Bedrock region prefix, Copilot SDK adaptation, Vertex authentication, etc.)
  • Instance.state providing process-level singleton cache, Provider initialization only executes once

Key Files

FileResponsibility
src/provider/provider.tsCore namespace: state management, Provider routing, SDK factory, model lookup, CUSTOM_LOADERS
src/provider/models.tsmodels.dev data source loading: Zod schema definition, JSON parsing, periodic refresh, snapshot fallback
src/provider/schema.tsEffect Schema branded types ProviderID / ModelID, including Zod bridge
src/provider/transform.tsRequest/response transformation: message normalization, cache markers, reasoning variants, temperature/topP tuning, error formatting
src/provider/auth.tsProvider authentication: OAuth flow, API Key management, Plugin authentication integration
src/provider/error.tsError classification: context overflow detection (12+ Provider error patterns), API error parsing, retryability determination
src/provider/sdk/copilot/Custom GitHub Copilot SDK adapter (chat/ + responses/ subdirectories, ~20 files)

sdk/copilot/ is the only Provider that does not depend on an official Vercel AI SDK package, because it needs to handle Copilot-specific device authentication, token refresh, and API compatibility issues. It is marked as a “temporary package” in the README and may be replaced by the official SDK in the future.

Type System

Branded Types: ProviderID and ModelID

// schema.ts — Effect Schema branded types
const providerIdSchema = Schema.String.pipe(Schema.brand("ProviderID"))
export type ProviderID = typeof providerIdSchema.Type

export const ProviderID = providerIdSchema.pipe(
  withStatics((schema) => ({
    make: (id: string) => schema.makeUnsafe(id),    // Runtime construction
    zod: z.string().pipe(z.custom()),                // Zod validation bridge
    // Built-in constants
    anthropic: schema.makeUnsafe("anthropic"),
    openai: schema.makeUnsafe("openai"),
    githubCopilot: schema.makeUnsafe("github-copilot"),
    amazonBedrock: schema.makeUnsafe("amazon-bedrock"),
    // ...
  })),
)

Branded types ensure that ProviderID and ModelID are not interchangeable at compile time — you cannot pass a model ID to a function expecting a Provider ID. makeUnsafe is used to construct from strings at runtime (e.g. parsing user configuration), while constant properties (ProviderID.anthropic) provide type-safe reference points.

Provider.Model Complete Model Definition

// provider.ts — Zod schema
export const Model = z.object({
  id: ModelID.zod,           // Branded type identifier
  providerID: ProviderID.zod,
  api: z.object({
    id: z.string(),          // Actual model name sent to the API
    url: z.string(),         // API base URL
    npm: z.string(),         // Vercel AI SDK package name (e.g. "@ai-sdk/anthropic")
  }),
  name: z.string(),          // Human-readable name
  family: z.string().optional(),  // Model family (e.g. "claude", "gpt")
  capabilities: z.object({
    temperature: z.boolean(),
    reasoning: z.boolean(),       // Whether thinking/reasoning mode is supported
    attachment: z.boolean(),      // Whether attachments are supported
    toolcall: z.boolean(),        // Whether tool calls are supported
    input: z.object({
      text: z.boolean(), audio: z.boolean(),
      image: z.boolean(), video: z.boolean(), pdf: z.boolean(),
    }),
    output: z.object({
      text: z.boolean(), audio: z.boolean(),
      image: z.boolean(), video: z.boolean(), pdf: z.boolean(),
    }),
    interleaved: z.union([       // Interleaved thinking mode
      z.boolean(),
      z.object({ field: z.enum(["reasoning_content", "reasoning_details"]) }),
    ]),
  }),
  cost: z.object({
    input: z.number(),      // $/M tokens (input)
    output: z.number(),     // $/M tokens (output)
    cache: z.object({
      read: z.number(),     // Cache read unit price
      write: z.number(),    // Cache write unit price
    }),
    experimentalOver200K: z.object({ /* ... */ }).optional(),  // Over 200K pricing
  }),
  limit: z.object({
    context: z.number(),    // Context window
    input: z.number().optional(),
    output: z.number(),     // Maximum output tokens
  }),
  status: z.enum(["alpha", "beta", "deprecated", "active"]),
  options: z.record(z.string(), z.any()),  // Provider-specific options
  headers: z.record(z.string(), z.string()), // Custom request headers
  release_date: z.string(),
  variants: z.record(/* reasoning effort variants */).optional(),
})

Model describes not only model capabilities but also complete cost information and reasoning variant configuration. The variants field is auto-generated by ProviderTransform.variants() based on the model and SDK package name (e.g. Anthropic’s high/max thinking budget, OpenAI’s none/minimal/low/medium/high/xhigh reasoning effort).

Provider.Info

export const Info = z.object({
  id: ProviderID.zod,
  name: z.string(),
  source: z.enum(["env", "config", "custom", "api"]),  // Source tracking
  env: z.string().array(),      // Environment variable name list (e.g. ["ANTHROPIC_API_KEY"])
  key: z.string().optional(),   // Resolved API Key
  options: z.record(z.string(), z.any()),  // Provider-level options
  models: z.record(z.string(), Model),     // All models under this Provider
})

The source field tracks the priority of Provider credential sources: env (environment variable) > api (Auth store) > config (configuration file) > custom (Plugin/Loader).

Core Flow

Provider State Initialization

Instance.state() creates a process-level singleton, assembling available Providers in the following order:

1. Config.get()           → Read user configuration
2. ModelsDev.get()        → Load models.dev data (cache → snapshot → network fetch)
3. fromModelsDevProvider() → Convert to Provider.Info database
4. Environment variable scan → Match env fields, set source: "env"
5. Auth.all()             → Read persisted API Key / OAuth token
6. Plugin.list()          → Process Plugin-provided authentication (e.g. Copilot device auth)
7. CUSTOM_LOADERS         → Execute custom logic for each Provider
8. Config merge           → Override model definitions, options, whitelist/blacklist
9. Filter                 → Remove disabled, alpha (non-experimental mode), deprecated Providers with empty model lists

The initialization result is cached in memory, and subsequent calls to Provider.list() / Provider.getModel() read directly from cache.

SDK Factory and 30+ Provider Routing

The getSDK() function creates Vercel AI SDK Provider instances for a given model:

// Built-in Provider mapping: 20 official SDK packages
const BUNDLED_PROVIDERS: Record<string, (...args: any[]) => SDK> = {
  "@ai-sdk/anthropic": createAnthropic,
  "@ai-sdk/openai": createOpenAI,
  "@ai-sdk/google": createGoogleGenerativeAI,
  "@ai-sdk/amazon-bedrock": createAmazonBedrock,
  "@ai-sdk/azure": createAzure,
  "@openrouter/ai-sdk-provider": createOpenRouter,
  "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,  // Custom adapter
  // ... also Groq, Mistral, DeepInfra, Cerebras, Cohere, Together, Perplexity, Vercel, GitLab, etc.
}

Routing logic:

  1. Look up BUNDLED_PROVIDERS[model.api.npm] — if found, call the factory function directly
  2. If not found, dynamically install the npm package via BunProc.install(), then import it
  3. Each instance is cached by the hash of { providerID, npm, options }, reusing the same configuration

getLanguage() further converts the SDK instance to a LanguageModel:

// If this Provider has a custom modelLoader, use it; otherwise call sdk.languageModel()
const language = s.modelLoaders[model.providerID]
  ? await s.modelLoaders[model.providerID](sdk, model.api.id, provider.options)
  : sdk.languageModel(model.api.id)

Model Discovery: models.dev Data Source

models.ts implements a three-level fallback data loading strategy:

Priority:
1. Local cache file (~/.cache/opencode/models.json)
2. Build-time snapshot (models-snapshot.ts, bundled in the binary)
3. Remote fetch (https://models.dev/api.json)

After startup, it refreshes in the background every 60 minutes (setInterval + unref(), not blocking process exit). Model data is validated through ModelsDev.Model Zod schema and then converted to Provider.Model, ensuring structural consistency.

Custom Provider Loaders (CUSTOM_LOADERS)

The CUSTOM_LOADERS dictionary provides custom logic for special Providers, each loader returning { autoload, getModel?, options?, vars? }:

ProviderSpecial Logic
anthropicAdds beta headers (claude-code-20250219, interleaved-thinking)
openaiUses Responses API (sdk.responses(modelID)) instead of Chat
github-copilotSelects responses() or chat() based on model ID
amazon-bedrockAWS credential chain, region inference, cross-region inference prefix (us./eu./apac.)
google-vertexGoogle ADC authentication, project/location parsing, custom fetch injection
azureDynamic baseURL template substitution, resourceName variable injection
gitlabAgentic Chat API, custom User-Agent and feature flags
cloudflare-ai-gatewayDynamically loads ai-gateway-provider package, Unified API format routing
opencodeDetects API Key availability, only keeps free models when no Key is present

Copilot Custom SDK Adapter

The provider/sdk/copilot/ directory contains a standalone Copilot SDK adapter with the following structure:

sdk/copilot/
├── index.ts                  # createOpenaiCompatible factory function
├── copilot-provider.ts       # Provider implementation
├── openai-compatible-error.ts # Error handling
├── chat/                     # Chat Completions API adapter
└── responses/                # Responses API adapter

This adapter does not depend on @ai-sdk/openai, but instead self-implements Copilot-specific device authentication flow (OAuth device code flow), token refresh, and API compatibility handling. It is marked as “temporary” in the README — it will be removed when Vercel AI SDK provides official Copilot support.

Custom fetch Injection

Each SDK instance is created with an injected custom fetch providing three key capabilities:

  1. SSE timeout wrapper (wrapSSE): sets a timeout for each chunk read, default 5 minutes, preventing silent Provider disconnections from causing connections to hang indefinitely
  2. OpenAI request body optimization: removes id fields from input arrays, reducing unnecessary network transmission
  3. Signal merging: combines the original signal + chunkAbort + timeout via AbortSignal.any() into a unified abort signal
// Signal merging illustration
const combined = AbortSignal.any([
  originalSignal,     // User cancellation
  chunkAbort,         // Chunk-level timeout
  timeoutSignal,      // Global timeout
])

This layered signal design ensures: user-initiated cancellation takes effect immediately, individual chunk timeouts trigger reconnection, and global timeout serves as the final fallback.

ProviderAuth OAuth Flow

The ProviderAuth namespace handles the full lifecycle of two authentication types:

API Key authentication (key type):

  • Get API Key from environment variables or configuration files
  • Set provider.key directly, mark as source: "env" or source: "config"

OAuth authentication (refresh type):

  • authorize phase: get auth configuration from Plugin hooks (e.g. Copilot’s device code flow URL)
  • callback phase: supports auto (automatic callback) and code (manual authorization code entry) callback methods
  • After authentication completes, the refresh token is persisted, and sessions are maintained through automatic refresh
// Authentication type determination
key → api type auth     // Use API Key directly
refresh → oauth type auth  // Use OAuth refresh token

Model Filtering and Whitelist/Blacklist

After Provider initialization completes, the model list goes through multiple layers of filtering:

  1. autoload check: Providers with autoload: false in CUSTOM_LOADERS are skipped (e.g. Providers without an API Key)
  2. disable flag: Models marked disable: true are skipped directly
  3. Free model fallback: When no API Key is present, only models marked as free are retained (e.g. opencode Provider’s free models)
  4. SDK instance cache: Cached by hash of { providerID, npm, options }, same configuration reuses the same instance

LiteLLM Proxy Compatibility

OpenCode detects LiteLLM proxies and automatically adapts message format:

  • Detects LiteLLM identifiers in response headers or request paths
  • When message history contains tool calls but the current message has no tool definitions, injects a dummy tool
  • This is because LiteLLM errors when messages contain tool_call roles but the request does not carry tools

This compatibility handling ensures OpenCode can transparently access various models through LiteLLM proxies.

Request Transformation Layer

The ProviderTransform namespace handles Provider-specific transformations of messages before they are sent. The entire transformation pipeline is invoked within the wrapLanguageModel middleware:

wrapLanguageModel Middleware

LLM.stream() injects message transformation middleware via wrapLanguageModel() before calling streamText():

model: wrapLanguageModel({
  model: language,
  middleware: [{
    specificationVersion: "v3" as const,
    async transformParams(args) {
      if (args.type === "stream") {
        args.params.prompt = ProviderTransform.message(args.params.prompt, input.model, options)
      }
      return args.params
    },
  }],
})

ProviderTransform.message() executes four transformation steps in order: unsupportedPartsnormalizeMessagesapplyCachingproviderOptions key remap. This middleware design completely decouples transformation logic from SDK calling logic.

Message Normalization (normalizeMessages)

  • Anthropic: Filters out empty content messages (API rejects empty strings)
  • Claude models: Non-alphanumeric characters in tool call IDs are replaced with _
  • Mistral: Tool call IDs normalized to exactly 9 alphanumeric characters; tool messages cannot be immediately followed by user messages (auto-inserts “Done.” assistant message)
  • Interleaved thinking models: Moves reasoning part from content to providerOptions.openaiCompatible.reasoning_content

Cache Markers (applyCaching)

Automatically adds cache markers for Providers that support Prompt Caching. The caching policy has clear rules:

  • First 2 system messages + last 2 messages get cache markers
  • Each Provider uses a different format:
// Anthropic
{ cacheControl: { type: "ephemeral" } }

// Amazon Bedrock
{ cachePoint: { type: "default" } }

Only takes effect for Providers known to support Prompt Caching such as Anthropic / OpenRouter / Bedrock; other Providers silently skip.

Reasoning Variant Generation (variants)

Automatically generates reasoning effort variants based on model.api.npm and model.id. Each Provider has different variant parameters:

  • Anthropic: { thinking: { type: "enabled", budgetTokens } } (high: 16K, max: 32K)
  • OpenAI: { reasoningEffort, reasoningSummary: "auto" } (none to xhigh)
  • Google: { thinkingConfig: { thinkingBudget, includeThoughts } }
  • Bedrock: Anthropic models use reasoningConfig.budgetTokens, Nova uses maxReasoningEffort

Model-specific Tuning

// transform.ts — temperature auto-adjusted based on model ID
function temperature(model: Provider.Model) {
  if (id.includes("qwen"))   return 0.55
  if (id.includes("claude")) return undefined  // Use default value
  if (id.includes("gemini")) return 1.0
  if (id.includes("kimi-k2")) return id.includes("thinking") ? 1.0 : 0.6
  return undefined
}

Error Handling

Context Overflow Detection

error.ts maintains an overflow pattern matching list covering 12+ Providers:

const OVERFLOW_PATTERNS = [
  /prompt is too long/i,                      // Anthropic
  /input is too long for requested model/i,   // Amazon Bedrock
  /exceeds the context window/i,              // OpenAI
  /input token count.*exceeds the maximum/i,  // Google Gemini
  /maximum prompt length is \d+/i,            // xAI Grok
  /exceeds the limit of \d+/i,                // GitHub Copilot
  /context[_ ]length[_ ]exceeded/i,           // Generic fallback
  // ... more
]

Errors are classified into two types:

  • context_overflow: context exceeds window limit, requires input truncation
  • api_error: generic API error, containing statusCode, isRetryable, responseBody

The OpenAI Provider has special retry logic: HTTP 404 is also considered retryable (some models return 404 on first request but are actually available).

Custom Error Types

// provider.ts
ModelNotFoundError  — Contains providerID, modelID, and fuzzysort fuzzy match suggestions
InitError           — SDK initialization failure (package install/load error)

ProviderError.parseAPICallError() is the unified error parsing entry point used by upper layers (Agent/Session).

SSE Timeout Protection

The wrapSSE() function adds per-chunk timeouts (default 5 minutes) to SSE streams, preventing connections from hanging indefinitely when the network disconnects:

function wrapSSE(res: Response, ms: number, ctl: AbortController) {
  // Set timeout for each chunk read
  // On timeout: abort connection + cancel reader
}

State Management and Provider Hot-Swapping

Provider state is managed via Instance.state() — a lazy singleton factory that initializes on first call and returns cached results on subsequent calls. The state contains:

{
  providers: Record<string, Provider.Info>,     // All available Providers
  models: Map<string, LanguageModel>,            // Instantiated LanguageModel cache
  sdk: Map<string, SDK>,                         // Instantiated SDK Provider cache
  modelLoaders: Record<string, CustomModelLoader>, // Custom model loaders
  varsLoaders: Record<string, CustomVarsLoader>,   // Variable injectors (e.g. Azure resourceName)
}

When switching models (user selects a different model), the call chain is:

  1. Provider.getModel(newProviderID, newModelID) — look up model info
  2. Provider.getLanguage(model) — get or create LanguageModel instance
  3. getSDK(model) — get or create SDK Provider instance
  4. Instances cached by { providerID, npm, options } hash, same configuration is reused

The defaultModel() function selects the default model in the following priority order:

  1. model field in the configuration file
  2. Most recently used model (recent list in model.json)
  3. First available model by priority order (gpt-5 > claude-sonnet-4 > gemini-3-pro)

Call Chain Examples

User selects model to streaming response

User selects "anthropic/claude-sonnet-4"


Provider.getModel("anthropic", "claude-sonnet-4")
    │  ← Look up state.providers.anthropic.models["claude-sonnet-4"]
    │  ← Not found? fuzzysort fuzzy match and throw ModelNotFoundError

Provider.getLanguage(model)
    │  ← Look up modelLoaders["anthropic"]
    │  ← Has custom loader? Call loader(sdk, model.api.id, options)
    │  ← Otherwise sdk.languageModel(model.api.id)

getSDK(model)
    │  ← Compute hash key = hash({ providerID, npm: "@ai-sdk/anthropic", options })
    │  ← Cache hit? Return directly
    │  ← Cache miss: BUNDLED_PROVIDERS["@ai-sdk/anthropic"] → createAnthropic(options)
    │  ← Inject custom fetch (SSE timeout + request body optimization)

Agent calls streamText({ model: languageModel, messages, tools })
    │  ← Vercel AI SDK handles actual HTTP request and SSE parsing

Streaming response returned to upper layer

State update on model switch

User switches from claude-sonnet-4 to gpt-5


parseModel("openai/gpt-5") → { providerID: "openai", modelID: "gpt-5" }


Provider.getModel("openai", "gpt-5")
    │  ← state.providers["openai"] exists?
    │  ← state.providers["openai"].models["gpt-5"] exists?

Provider.getLanguage(model)
    │  ← getSDK: BUNDLED_PROVIDERS["@ai-sdk/openai"] → createOpenAI(options)
    │  ← modelLoaders["openai"]: sdk.responses("gpt-5")  (Responses API)

New LanguageModel instance cached in state.models

Design Tradeoffs

DecisionRationale
Vercel AI SDK instead of direct HTTPProtocol differences across 30+ Providers are handled uniformly by the SDK, avoiding maintenance of large amounts of HTTP client code. OpenCode focuses on business logic (message transformation, caching, error classification)
Effect Schema branded typesCompile-time prevention of ProviderID / ModelID / string mixing, zero runtime overhead (branded types erase to string)
models.dev external data sourceModel information (pricing, context window, capabilities) is maintained by the community, OpenCode doesn’t need to hardcode it. Three-level fallback ensures offline availability
CUSTOM_LOADERS dictionary instead of subclass inheritanceEach Provider’s differing logic (authentication method, API selection, region handling) varies too much for an inheritance hierarchy to express elegantly. Dictionary + functions are more flexible
Custom Copilot SDK adapterCopilot’s device authentication and token refresh flow differs too much from standard OAuth, and the official SDK doesn’t support it, requiring independent implementation
SSE per-chunk timeoutPrevents silent Provider disconnections from causing connections to hang indefinitely. Default 5 minutes, adjustable via chunkTimeout option
fuzzysort fuzzy match suggestionsModel IDs are frequently misspelled or renamed; fuzzy matching provides 3 suggestions instead of a hard error
BunProc dynamic install for unbundled SDKsCommunity Providers (e.g. @mymediset/sap-ai-provider) don’t need to be bundled in the main package, installed on demand

Relationships with Other Modules

  • Agent: Obtains LanguageModel via Provider.getLanguage(), passes it to Vercel AI SDK’s streamText() / generateText() to drive the conversation loop
  • Config: Provider initialization reads config.model (default model), config.provider (Provider configuration overrides), config.disabled_providers, config.enabled_providers
  • Auth: Auth.get(providerID) reads persisted API Key / OAuth token; ProviderAuth namespace handles OAuth authorization flow
  • Plugin: Plugin.list() gets registered authentication methods (e.g. Copilot device auth), injected into Provider options via plugin.auth.loader
  • Instance: Instance.state() provides singleton management for Provider state, bound to the project lifecycle
  • Transform: Before calling streamText, Agent normalizes messages and injects cache markers via ProviderTransform.message()
  • Error: Agent converts Vercel AI SDK’s APICallError to structured ParsedAPICallError via ProviderError.parseAPICallError()