OpenCode uses the AI SDK and Models.dev to support 75+ LLM providers and local models.[1] Provider API keys added via /connect are stored in ~/.local/share/opencode/auth.json.[1] OpenCode Zen is a curated, team-tested set of models accessed via /connect by selecting OpenCode Zen and authenticating at opencode.ai/zen; it is recommended for users new to LLM providers.[1][2] OpenCode Go is a low-cost subscription plan for popular open coding models provided and tested by the OpenCode team, accessed via /connect by selecting OpenCode Go.[1] As of OpenCode 1.3.0, bundled plugins for using Claude Pro/Max subscriptions were removed — Anthropic explicitly prohibits this use; ChatGPT Plus, GitHub Copilot, and GitLab Duo subscriptions are supported with zero additional setup.[1] The AI SDK is a TypeScript library by Vercel that provides a unified interface for calling LLMs across different providers, abstracting provider-specific API differences. Eden AI is a supported LLM provider in OpenCode, listed in the provider reference documentation. The Console UI model-selection routes in packages/console/app/src/routes/go/index.tsx and packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx list Muse Spark 1.3 and Gemini 3.8 Flash as selectable inference models. The dialog-connect-provider.tsx component in packages/app/src/components/ signals device type as 'desktop' during the Console device-authentication v1 flow, preventing auth-flow branching errors and token-grant failures.
The provider baseURL option in opencode.json redirects any provider to a proxy service or custom endpoint.[1] The provider blacklist option removes specific model IDs from the /models picker; whitelist hides every model except those listed — both accept an array of model IDs identical to those shown in the picker, and the two options can be combined: whitelist narrows the set, then blacklist removes entries from it.[1] Custom OpenAI-compatible local providers are configured in opencode.json using an npm package (e.g., @ai-sdk/openai-compatible), a baseURL pointing to the local server, and a models map whose IDs must match the id values returned by GET /v1/models.[1]
Example: Configuring Atomic Chat as a custom OpenAI-compatible local provider in opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"atomic-chat": {
"npm": "@ai-sdk/openai-compatible",
"name": "Atomic Chat (local)",
"options": {
"baseURL": "http://127.0.0.1:1337/v1"
},
"models": {
"<your-model-id>": {
"name": "<your-model-name>"
}
}
}
}
}
An OpenAI-compatible provider is a local or third-party LLM server that implements the OpenAI REST API shape — including GET /v1/models and chat-completion endpoints — allowing OpenCode to communicate with it via the standard OpenAI adapter. Model normalization in packages/stats/core/src/domain/model-normalization.ts merges DeepSeek Flash API-level sub-variants under a single canonical name, preventing them from appearing as separate line items in inference statistics. The inference proxy in packages/console/app/src/lib/inference-proxy.ts routes model discovery requests for migrated models to the v1 endpoint, ensuring correct discovery in the Console UI.
Amazon Bedrock in opencode.json supports provider-level options keys region, profile, and endpoint (an alias for baseURL using AWS terminology); when both endpoint and baseURL are specified, endpoint takes precedence.[1] Amazon Bedrock authentication priority: a bearer token (AWS_BEARER_TOKEN_BEDROCK environment variable or token from /connect) takes precedence over the full AWS credential chain — profile, access keys, shared credentials, IAM roles, Web Identity Tokens, and instance metadata.[1] Amazon Bedrock supports Web Identity Tokens for EKS IRSA via AWS_WEB_IDENTITY_TOKEN_FILE / AWS_ROLE_ARN, which Kubernetes automatically injects when service account annotations are used.[1] For Amazon Bedrock custom inference profiles, set the models key under the amazon-bedrock provider using any model/provider name as the key, and set id to the profile ARN to ensure correct caching.[1]
The azure custom provider in packages/opencode/src/provider/provider.ts resolves the resource name in priority order from: provider options, API auth metadata, OAuth account ID, or the AZURE_RESOURCE_NAME environment variable; if none is found and no baseURL is set, all model calls throw a descriptive error.[3] For Azure OpenAI, the deployment name in Azure AI Foundry must match the model name for OpenCode to work properly.[1] If Azure OpenAI returns "I'm sorry, but I cannot assist with that request" errors, the fix is to change the Azure content filter from DefaultV2 to Default.[1] Azure model auto-discovery has been removed from packages/opencode/src/plugin/azure.ts; Azure-backed providers now require explicit model enumeration in opencode.json configuration.
The anthropic custom provider entry in packages/opencode/src/provider/provider.ts always sets autoload: false and injects the beta headers interleaved-thinking-2025-05-14 and fine-grained-tool-streaming-2025-05-14 on every request.[3] The openai custom provider in packages/opencode/src/provider/provider.ts uses the responses endpoint by default (via sdk.responses(modelID)) and sets headerTimeout to 300_000 ms (5 minutes).[3] The github-copilot custom provider in packages/opencode/src/provider/provider.ts selects the responses endpoint for GPT-5 and higher (excluding gpt-5-mini), the chat endpoint for earlier GPT models, and honours a model-level api.endpoint override when present.[3] The @ai-sdk/github-copilot bundled provider in packages/opencode/src/provider/provider.ts is loaded from the internal @opencode-ai/core/github-copilot/copilot-provider module rather than a true @ai-sdk/github-copilot npm package.[3] When no API key or auth is found for the opencode provider in packages/opencode/src/provider/provider.ts, all paid models (those with non-zero cost.input) are removed from the model list, and options: { apiKey: "public" } is set as a fallback, enabling access to the free tier only.[3] The Bedrock Mantle model selector in packages/opencode/src/provider/provider.ts uses the chat endpoint for openai.gpt-oss-safeguard-20b and openai.gpt-oss-safeguard-120b, and the responses endpoint for all other models.[3] The patched @ai-sdk/amazon-bedrock dependency accepts none as a valid reasoning-effort value, correcting a rejection by the upstream SDK. Patched @ai-sdk/anthropic and @ai-sdk/amazon-bedrock dependencies carry reasoning and replay fixes that are load-bearing for extended-thinking and replay-dependent sessions routed through Anthropic or Bedrock. The provider transform layer in packages/opencode/src/provider/transform.ts and the session processor in packages/opencode/src/session/processor.ts tolerate Anthropic thinking-block bindings that previously caused hard failures. The thinking-block binding in packages/opencode/src/provider/transform.ts is gated to Claude 5.1 and later; Claude 3.x and 4.x models do not receive thinking-block injection, preventing API errors on older versions. opencode.json configuration can opt out of thinking-block injection even on eligible Claude 5.1+ models.
The googleVertexEndpoint helper in packages/opencode/src/provider/provider.ts maps "global" to aiplatform.googleapis.com, the continental multi-regions "eu"/"us" to REP domains (aiplatform.{loc}.rep.googleapis.com), and all other locations to regional domains ({location}-aiplatform.googleapis.com).[3] The googleVertexAnthropicBaseURL helper in packages/opencode/src/provider/provider.ts generates a Regional Endpoint Platform (REP) base URL only for the eu and us continental multi-regions; all other locations return undefined.[3]
timeoutController in packages/opencode/src/provider/provider.ts returns an AbortController that automatically fires a ProviderError.HeaderTimeoutError after ms milliseconds; callers must invoke the returned clear() to cancel the timeout on success.[3] wrapSSE in packages/opencode/src/provider/provider.ts wraps an SSE (text/event-stream) response body with a per-chunk read timeout; if a chunk is not received within ms milliseconds, it aborts the controller with a ProviderError.ResponseStreamError and cancels the reader.[3] wrapSSE in packages/opencode/src/provider/provider.ts is a no-op (returns the original response) when ms is not a positive number, when the response has no body, or when the content-type header does not include text/event-stream.[3] The provider integration layer in packages/opencode/src/provider/provider.ts and the AI SDK wrapper in packages/core/src/aisdk.ts catch and suppress cancel-triggered rejections from the SSE reader, preventing unhandled promise rejections when aborting in-flight LLM streams. The header timeout (time-to-first-token limit) for LLM provider connections defaults to 300 seconds (5 minutes), defined across packages/core/src/v1/config/provider.ts, packages/opencode/src/provider/provider.ts, packages/sdk/openapi.json, and packages/sdk/js/src/v2/gen/types.gen.ts. The per-chunk streaming timeout for LLM provider connections defaults to 300 seconds (5 minutes), defined across packages/core/src/v1/config/provider.ts, packages/opencode/src/provider/provider.ts, packages/sdk/openapi.json, and packages/sdk/js/src/v2/gen/types.gen.ts.
The LLM service in packages/opencode/src/session/llm.ts is registered under the Effect context tag "@opencode/LLM" and exposes a single stream method that accepts a StreamInput and returns Stream.Stream<LLMEvent, unknown>.[4] StreamInput in packages/opencode/src/session/llm.ts requires user, sessionID, model, agent, system, messages, and tools; optional fields include parentSessionID, permission, small, retries, and toolChoice ("auto" | "required" | "none").[4] The live LLM layer in packages/opencode/src/session/llm.ts depends on Auth.Service, Config.Service, Provider.Service, Plugin.Service, Permission.Service, EventV2Bridge.Service, LLMClientService, and RuntimeFlags.Service.[4] packages/opencode/src/session/llm.ts re-exports OUTPUT_TOKEN_MAX from ProviderTransform.OUTPUT_TOKEN_MAX.[4]
packages/opencode/src/session/llm.ts calls LLMRequestPrep.prepare to assemble the request (messages, tools, system prompt, params, headers) before dispatching to either runtime.[4] packages/opencode/src/session/llm.ts calls LLMNativeRuntime.stream to attempt native execution, and falls back to LLMAISDK / streamText when the result is not "supported".[4] OpenTelemetry tracing in packages/opencode/src/session/llm.ts is opt-in via cfg.experimental?.openTelemetry; when enabled, the tracer proxy injects session.id onto every span via setAttribute.[4] In packages/opencode/src/session/llm.ts, tools that pass the permission ruleset without an "ask" action are pre-approved for GitLab Workflow sessions via workflowModel.sessionPreapprovedTools, preventing repeated approval prompts for server-side MCP tools.[4] The GitLab Workflow approvalHandler in packages/opencode/src/session/llm.ts auto-approves tools already approved within the same session (tracked in approvedToolsForSession) to prevent infinite approval loops.[4]
Configure the OpenAI provider with an API key and generation defaults, then select a model
const model = OpenAI.configure({
apiKey,
generation: { maxTokens: 160 },
providerOptions: {
openai: { store: false },
},
}).model("gpt-4o-mini")
Build a provider-neutral LLMRequest with generation options and provider-native options via LLM.request
const request = LLM.request({
model,
system: "You are concise and practical.",
prompt: "Tell me a joke",
generation: { maxTokens: 80, temperature: 0.7 },
providerOptions: {
openai: { promptCacheKey: "tutorial-joke" },
},
})
Generate a single LLM response and access the collected text and usage via LLM.generate
const response = yield* LLM.generate(request)
console.log("generated text:", response.text)
console.log("usage", Formatter.formatJson(response.usage, { space: 2 }))
Stream LLM output as incremental LLMEvents using LLM.stream, handling text-delta and finish events
const streamText = LLM.stream(request).pipe(
Stream.tap((event) =>
Effect.sync(() => {
if (event.type === "text-delta") process.stdout.write(`\ntext: ${event.text}`)
if (event.type === "finish") process.stdout.write(`\nfinish: ${event.reason}\n`)
}),
),
Stream.runDrain,
)
Generate a typed structured object from a Schema using LLM.generateObject; falls back to synthetic tool call for cross-provider compatibility
const WeatherReport = Schema.Struct({
city: Schema.String,
forecast: Schema.String,
highFahrenheit: Schema.Number,
})
const response = yield* LLM.generateObject({
model,
system: "Return only structured weather data.",
prompt: "Give me today's weather for San Francisco.",
schema: WeatherReport,
generation: { maxTokens: 120, temperature: 0 },
})
console.log(Formatter.formatJson(response.object, { space: 2 }))
Inspect the compiled request pipeline (route, body, URL, auth) without sending a network request using LLMClient.prepare
const prepared = yield* LLMClient.prepare(
LLM.request({
model: FakeEcho.configure().model("tiny-echo"),
prompt: "Show me the provider pipeline.",
}),
)
console.log("route:", prepared.route)
console.log("body:", Formatter.formatJson(prepared.body, { space: 2 }))
Provide the LLM runtime layer (HTTP + WebSocket executors) to an Effect program
const requestExecutorLayer = RequestExecutor.fetchLayer
const llmDeps = Layer.mergeAll(requestExecutorLayer, WebSocketExecutor.layer)
const llmClientLayer = LLMClient.layer.pipe(Layer.provide(llmDeps))
const program = Effect.gen(function* () {
yield* streamWithTools
}).pipe(Effect.provide(Layer.mergeAll(llmDeps, llmClientLayer)))
Effect.runPromise(program)
Sources